Compare commits
471 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
106
LICENSE.md
Normal file
106
LICENSE.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# CC0 1.0 Universal
|
||||
|
||||
## Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||
subsequent owner(s) (each and all, an “owner”) of an original work of
|
||||
authorship and/or a database (each, a “Work”).
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||
purpose of contributing to a commons of creative, cultural and scientific works
|
||||
(“Commons”) that the public can reliably and without fear of later claims of
|
||||
infringement build upon, modify, incorporate in other works, reuse and
|
||||
redistribute as freely as possible in any form whatsoever and for any purposes,
|
||||
including without limitation commercial purposes. These owners may contribute
|
||||
to the Commons to promote the ideal of a free culture and the further
|
||||
production of creative, cultural and scientific works, or to gain reputation or
|
||||
greater distribution for their Work in part through the use and efforts of
|
||||
others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any expectation of
|
||||
additional consideration or compensation, the person associating CC0 with a
|
||||
Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
|
||||
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
|
||||
publicly distribute the Work under its terms, with knowledge of his or her
|
||||
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||
effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights (“Copyright and
|
||||
Related Rights”). Copyright and Related Rights include, but are not limited
|
||||
to, the following:
|
||||
1. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
2. moral rights retained by the original author(s) and/or performer(s);
|
||||
3. publicity and privacy rights pertaining to a person’s image or likeness
|
||||
depicted in a Work;
|
||||
4. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(i), below;
|
||||
5. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
6. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
7. other similar, equivalent or corresponding rights throughout the world
|
||||
based on applicable law or treaty, and any national implementations
|
||||
thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||
unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright
|
||||
and Related Rights and associated claims and causes of action, whether now
|
||||
known or unknown (including existing as well as future claims and causes of
|
||||
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||
duration provided by applicable law or treaty (including future time
|
||||
extensions), (iii) in any current or future medium and for any number of
|
||||
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||
commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes
|
||||
the Waiver for the benefit of each member of the public at large and to the
|
||||
detriment of Affirmer’s heirs and successors, fully intending that such Waiver
|
||||
shall not be subject to revocation, rescission, cancellation, termination, or
|
||||
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
||||
by the public as contemplated by Affirmer’s express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||
shall be preserved to the maximum extent permitted taking into account
|
||||
Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver
|
||||
is so judged Affirmer hereby grants to each affected person a royalty-free, non
|
||||
transferable, non sublicensable, non exclusive, irrevocable and unconditional
|
||||
license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in
|
||||
all territories worldwide, (ii) for the maximum duration provided by applicable
|
||||
law or treaty (including future time extensions), (iii) in any current or
|
||||
future medium and for any number of copies, and (iv) for any purpose
|
||||
whatsoever, including without limitation commercial, advertising or promotional
|
||||
purposes (the “License”). The License shall be deemed effective as of the date
|
||||
CC0 was applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder of the
|
||||
License, and in such case Affirmer hereby affirms that he or she will not (i)
|
||||
exercise any of his or her remaining Copyright and Related Rights in the Work
|
||||
or (ii) assert any associated claims and causes of action with respect to the
|
||||
Work, in either case contrary to Affirmer’s express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
1. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
2. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied, statutory
|
||||
or otherwise, including without limitation warranties of title,
|
||||
merchantability, fitness for a particular purpose, non infringement, or
|
||||
the absence of latent or other defects, accuracy, or the present or
|
||||
absence of errors, whether or not discoverable, all to the greatest
|
||||
extent permissible under applicable law.
|
||||
3. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person’s Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the Work.
|
||||
4. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
|
||||
For more information, please see
|
||||
http://creativecommons.org/publicdomain/zero/1.0/.
|
||||
42
README.md
42
README.md
@@ -1,3 +1,43 @@
|
||||
# Liberama
|
||||
|
||||
Свободный обмен книгами в формате fb2
|
||||
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
||||
|
||||
Читалка [OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||
|
||||

|
||||

|
||||
|
||||
## VPS
|
||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
|
||||
|
||||
## Сборка проекта
|
||||
Необходима версия node.js не ниже 10.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/bookpauk/liberama
|
||||
$ cd liberama
|
||||
$ npm i
|
||||
```
|
||||
|
||||
### Windows
|
||||
```
|
||||
$ npm run build:win
|
||||
```
|
||||
|
||||
### Linux
|
||||
```
|
||||
$ npm run build:linux
|
||||
```
|
||||
|
||||
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
|
||||
|
||||
### Разработка
|
||||
```
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
## Помочь проекту
|
||||
|
||||
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85
|
||||
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
|
||||
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
|
||||
|
||||
31
build/includer.js
Normal file
31
build/includer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
//пример в коде:
|
||||
// @@include('./test/testFile.inc');
|
||||
|
||||
function includeRecursive(self, parentFile, source, depth) {
|
||||
depth = (depth ? depth : 0);
|
||||
if (depth > 50)
|
||||
throw new Error('includer: stack too big');
|
||||
const lines = source.split('\n');
|
||||
let result = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
const m = trimmed.match(/^@@[\s]*?include[\s]*?\(['"](.*)['"]\)/);
|
||||
if (m) {
|
||||
const includedFile = path.resolve(path.dirname(parentFile), m[1]);
|
||||
self.addDependency(includedFile);
|
||||
|
||||
const fileContent = fs.readFileSync(includedFile, 'utf8');
|
||||
result = result.concat(includeRecursive(self, includedFile, fileContent, depth + 1));
|
||||
} else {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.default = function includer(source) {
|
||||
return includeRecursive(this, this.resourcePath, source).join('\n');
|
||||
}
|
||||
@@ -24,8 +24,8 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-linux-x64/node_sqlite3.node`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-linux-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
|
||||
@@ -16,6 +16,11 @@ module.exports = {
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader"
|
||||
},
|
||||
{
|
||||
test: /\.includer$/,
|
||||
resourceQuery: /^\?vue/,
|
||||
use: path.resolve('build/includer.js')
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
|
||||
@@ -55,6 +55,6 @@ module.exports = merge(baseWpConfig, {
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||
//new AppCachePlugin({})
|
||||
new AppCachePlugin({exclude: ['../index.html']})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-win32-x64/node_sqlite3.node`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-win32-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api'
|
||||
@@ -6,9 +7,23 @@ const api = axios.create({
|
||||
|
||||
class Misc {
|
||||
async loadConfig() {
|
||||
const response = await api.post('/config', {params: [
|
||||
|
||||
const query = {params: [
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||
]});
|
||||
]};
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||
if (config.error)
|
||||
throw new Error(config.error);
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
//если с WebSocket проблема, работаем по http
|
||||
const response = await api.post('/config', query);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import axios from 'axios';
|
||||
import {Buffer} from 'safe-buffer';
|
||||
|
||||
import * as utils from '../share/utils';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/reader'
|
||||
@@ -13,36 +11,57 @@ const workerApi = axios.create({
|
||||
});
|
||||
|
||||
class Reader {
|
||||
async loadBook(url, callback) {
|
||||
const refreshPause = 300;
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async getWorkerStateFinish(workerId, callback) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = await api.post('/load-book', {type: 'url', url});
|
||||
let response = {};
|
||||
try {
|
||||
await wsc.open();
|
||||
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
||||
|
||||
const workerId = response.data.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
let prevResponse = false;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
response = await wsc.message(requestId);
|
||||
|
||||
callback({totalSteps: 4});
|
||||
if (!response.state && prevResponse !== false) {//экономия траффика
|
||||
callback(prevResponse);
|
||||
} else {//были изменения worker state
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
callback(response);
|
||||
prevResponse = response;
|
||||
}
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
//если с WebSocket проблема, работаем по http
|
||||
const refreshPause = 500;
|
||||
let i = 0;
|
||||
response = {};
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
callback(response.data);
|
||||
const prevProgress = response.progress || 0;
|
||||
const prevState = response.state || 0;
|
||||
response = await workerApi.post('/get-state', {workerId});
|
||||
response = response.data;
|
||||
callback(response);
|
||||
|
||||
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||
callback({step: 4});
|
||||
const book = await this.loadCachedBook(response.data.path, callback);
|
||||
return Object.assign({}, response.data, {data: book.data});
|
||||
}
|
||||
if (response.data.state == 'error') {
|
||||
let errMes = response.data.error;
|
||||
if (errMes.indexOf('getaddrinfo') >= 0 ||
|
||||
errMes.indexOf('ECONNRESET') >= 0 ||
|
||||
errMes.indexOf('EINVAL') >= 0 ||
|
||||
errMes.indexOf('404') >= 0)
|
||||
errMes = `Ресурс не найден по адресу: ${response.data.url}`;
|
||||
throw new Error(errMes);
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
await utils.sleep(refreshPause);
|
||||
|
||||
@@ -51,30 +70,109 @@ class Reader {
|
||||
throw new Error('Слишком долгое время ожидания');
|
||||
}
|
||||
//проверка воркера
|
||||
const prevProgress = response.data.progress;
|
||||
const prevState = response.data.state;
|
||||
response = await workerApi.post('/get-state', {workerId});
|
||||
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
|
||||
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async loadBook(opts, callback) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = await api.post('/load-book', opts);
|
||||
|
||||
const workerId = response.data.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
callback({totalSteps: 4});
|
||||
callback(response.data);
|
||||
|
||||
response = await this.getWorkerStateFinish(workerId, callback);
|
||||
|
||||
if (response) {
|
||||
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||
callback({step: 4});
|
||||
const book = await this.loadCachedBook(response.path, callback, response.size);
|
||||
return Object.assign({}, response, {data: book.data});
|
||||
}
|
||||
|
||||
if (response.state == 'error') {
|
||||
let errMes = response.error;
|
||||
if (errMes.indexOf('getaddrinfo') >= 0 ||
|
||||
errMes.indexOf('ECONNRESET') >= 0 ||
|
||||
errMes.indexOf('EINVAL') >= 0 ||
|
||||
errMes.indexOf('404') >= 0)
|
||||
errMes = `Ресурс не найден по адресу: ${response.url}`;
|
||||
throw new Error(errMes);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Пустой ответ сервера');
|
||||
}
|
||||
}
|
||||
|
||||
async loadCachedBook(url, callback){
|
||||
const response = await axios.head(url);
|
||||
async checkCachedBook(url) {
|
||||
let estSize = -1;
|
||||
try {
|
||||
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||
|
||||
let estSize = 1000000;
|
||||
if (response.headers['content-length']) {
|
||||
estSize = response.headers['content-length'];
|
||||
if (response.headers['content-length']) {
|
||||
estSize = response.headers['content-length'];
|
||||
}
|
||||
} catch (e) {
|
||||
//восстановим при необходимости файл на сервере из удаленного облака
|
||||
let response = null
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
response = await api.post('/restore-cached-file', {path: url});
|
||||
response = response.data;
|
||||
}
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
const workerId = response.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
response = await this.getWorkerStateFinish(workerId);
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (response.size && estSize < 0) {
|
||||
estSize = response.size;
|
||||
}
|
||||
}
|
||||
|
||||
return estSize;
|
||||
}
|
||||
|
||||
async loadCachedBook(url, callback, estSize = -1) {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
callback({state: 'loading', progress: 0});
|
||||
|
||||
//получение размера файла
|
||||
if (estSize && estSize < 0) {
|
||||
estSize = await this.checkCachedBook(url);
|
||||
}
|
||||
|
||||
//получение файла
|
||||
estSize = (estSize > 0 ? estSize : 1000000);
|
||||
const options = {
|
||||
onDownloadProgress: progress => {
|
||||
onDownloadProgress: (progress) => {
|
||||
while (progress.loaded > estSize) estSize *= 1.5;
|
||||
|
||||
if (callback)
|
||||
callback({state: 'loading', progress: Math.round((progress.loaded*100)/estSize)});
|
||||
callback({progress: Math.round((progress.loaded*100)/estSize)});
|
||||
}
|
||||
}
|
||||
//загрузка
|
||||
|
||||
return await axios.get(url, options);
|
||||
}
|
||||
|
||||
@@ -111,13 +209,25 @@ class Reader {
|
||||
}
|
||||
|
||||
async storage(request) {
|
||||
let response = await api.post('/storage', request);
|
||||
let response = null;
|
||||
try {
|
||||
await wsc.open();
|
||||
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
response = await api.post('/storage', request);
|
||||
response = response.data;
|
||||
}
|
||||
|
||||
const state = response.data.state;
|
||||
const state = response.state;
|
||||
if (!state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
172
client/api/webSocketConnection.js
Normal file
172
client/api/webSocketConnection.js
Normal file
@@ -0,0 +1,172 @@
|
||||
const cleanPeriod = 60*1000;//1 минута
|
||||
|
||||
class WebSocketConnection {
|
||||
//messageLifeTime в минутах (cleanPeriod)
|
||||
constructor(messageLifeTime = 5) {
|
||||
this.ws = null;
|
||||
this.timer = null;
|
||||
this.listeners = [];
|
||||
this.messageQueue = [];
|
||||
this.messageLifeTime = messageLifeTime;
|
||||
this.requestId = 0;
|
||||
}
|
||||
|
||||
addListener(listener) {
|
||||
if (this.listeners.indexOf(listener) < 0)
|
||||
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
|
||||
}
|
||||
|
||||
//рассылаем сообщение и удаляем те обработчики, которые его получили
|
||||
emit(mes, isError) {
|
||||
const len = this.listeners.length;
|
||||
if (len > 0) {
|
||||
let newListeners = [];
|
||||
for (const listener of this.listeners) {
|
||||
let emitted = false;
|
||||
if (isError) {
|
||||
if (listener.onError)
|
||||
listener.onError(mes);
|
||||
emitted = true;
|
||||
} else {
|
||||
if (listener.onMessage) {
|
||||
if (listener.requestId) {
|
||||
if (listener.requestId === mes.requestId) {
|
||||
listener.onMessage(mes);
|
||||
emitted = true;
|
||||
}
|
||||
} else {
|
||||
listener.onMessage(mes);
|
||||
emitted = true;
|
||||
}
|
||||
} else {
|
||||
emitted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!emitted)
|
||||
newListeners.push(listener);
|
||||
}
|
||||
this.listeners = newListeners;
|
||||
}
|
||||
|
||||
return this.listeners.length != len;
|
||||
}
|
||||
|
||||
open(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
resolve(this.ws);
|
||||
} else {
|
||||
let protocol = 'ws:';
|
||||
if (window.location.protocol == 'https:') {
|
||||
protocol = 'wss:'
|
||||
}
|
||||
|
||||
url = url || `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
|
||||
let resolved = false;
|
||||
this.ws.onopen = (e) => {
|
||||
resolved = true;
|
||||
resolve(e);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (e) => {
|
||||
try {
|
||||
const mes = JSON.parse(e.data);
|
||||
this.messageQueue.push({regTime: Date.now(), mes});
|
||||
|
||||
let newMessageQueue = [];
|
||||
for (const message of this.messageQueue) {
|
||||
if (!this.emit(message.mes)) {
|
||||
newMessageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
this.messageQueue = newMessageQueue;
|
||||
} catch (e) {
|
||||
this.emit(e.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (e) => {
|
||||
this.emit(e.message, true);
|
||||
if (!resolved)
|
||||
reject(e);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//timeout в минутах (cleanPeriod)
|
||||
message(requestId, timeout = 2) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.addListener({
|
||||
requestId,
|
||||
timeout,
|
||||
onMessage: (mes) => {
|
||||
resolve(mes);
|
||||
},
|
||||
onError: (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
send(req) {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
const requestId = ++this.requestId;
|
||||
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||
return requestId;
|
||||
} else {
|
||||
throw new Error('WebSocket connection is not ready');
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
periodicClean() {
|
||||
try {
|
||||
this.timer = null;
|
||||
|
||||
const now = Date.now();
|
||||
//чистка listeners
|
||||
let newListeners = [];
|
||||
for (const listener of this.listeners) {
|
||||
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
|
||||
newListeners.push(listener);
|
||||
} else {
|
||||
if (listener.onError)
|
||||
listener.onError('Время ожидания ответа истекло');
|
||||
}
|
||||
}
|
||||
this.listeners = newListeners;
|
||||
|
||||
//чистка messageQueue
|
||||
let newMessageQueue = [];
|
||||
for (const message of this.messageQueue) {
|
||||
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
|
||||
newMessageQueue.push(message);
|
||||
}
|
||||
}
|
||||
this.messageQueue = newMessageQueue;
|
||||
} finally {
|
||||
if (this.ws.readyState == WebSocket.OPEN) {
|
||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketConnection();
|
||||
@@ -1,3 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /?*url=
|
||||
Disallow: /#/
|
||||
@@ -1,9 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside v-if="showAsideBar" :width="asideWidth">
|
||||
<!--q-layout view="lhr lpr lfr">
|
||||
<q-drawer v-model="showAsideBar" :width="asideWidth">
|
||||
<div class="app-name"><span v-html="appName"></span></div>
|
||||
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
|
||||
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
||||
<q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
|
||||
|
||||
<q-list>
|
||||
<q-item clickable v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="inbox" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>Inbox</q-item-section>
|
||||
</q-item>
|
||||
</q-list-->
|
||||
<!--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>
|
||||
@@ -32,24 +42,37 @@
|
||||
<i class="el-icon-question"></i>
|
||||
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
</el-menu-->
|
||||
<!--/q-drawer>
|
||||
|
||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
||||
<q-page-container>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</q-page-container>
|
||||
</q-layout-->
|
||||
<div class="fit row">
|
||||
<Notify ref="notify"/>
|
||||
<StdDialog ref="stdDialog"/>
|
||||
<keep-alive>
|
||||
<router-view class="col"></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Notify from './share/Notify.vue';
|
||||
import StdDialog from './share/StdDialog.vue';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Notify,
|
||||
StdDialog,
|
||||
},
|
||||
watch: {
|
||||
mode: function() {
|
||||
this.setAppTitle();
|
||||
@@ -75,6 +98,18 @@ class App extends Vue {
|
||||
this.uistate = this.$store.state.uistate;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
//root route
|
||||
let cachedRoute = '';
|
||||
let cachedPath = '';
|
||||
this.$root.rootRoute = () => {
|
||||
if (this.$route.path != cachedPath) {
|
||||
cachedPath = this.$route.path;
|
||||
const m = cachedPath.match(/^(\/[^/]*).*$/i);
|
||||
cachedRoute = (m ? m[1] : this.$route.path);
|
||||
}
|
||||
return cachedRoute;
|
||||
}
|
||||
|
||||
// set-app-title
|
||||
this.$root.$on('set-app-title', this.setAppTitle);
|
||||
|
||||
@@ -108,17 +143,16 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$root.notify = this.$refs.notify;
|
||||
this.$root.stdDialog = this.$refs.stdDialog;
|
||||
|
||||
this.dispatch('config/loadConfig');
|
||||
this.$watch('apiError', function(newError) {
|
||||
if (newError) {
|
||||
let mes = newError.message;
|
||||
if (newError.response && newError.response.config)
|
||||
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
||||
this.$notify.error({
|
||||
title: 'Ошибка API',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: mes
|
||||
});
|
||||
this.$root.notify.error(mes, 'Ошибка API');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,9 +171,9 @@ class App extends Vue {
|
||||
|
||||
get asideWidth() {
|
||||
if (this.uistate.asideBarCollapse) {
|
||||
return '64px';
|
||||
return 64;
|
||||
} else {
|
||||
return '170px';
|
||||
return 170;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +197,7 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
get rootRoute() {
|
||||
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
|
||||
this.$root.rootRoute = (m ? m[1] : this.$route.path);
|
||||
|
||||
return this.$root.rootRoute;
|
||||
return this.$root.rootRoute();
|
||||
}
|
||||
|
||||
setAppTitle(title) {
|
||||
@@ -193,12 +224,11 @@ class App extends Vue {
|
||||
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
|
||||
}
|
||||
|
||||
get isReaderActive() {
|
||||
return this.rootRoute == '/reader';
|
||||
set showAsideBar(value) {
|
||||
}
|
||||
|
||||
get showMain() {
|
||||
return (this.showAsideBar || this.isReaderActive);
|
||||
get isReaderActive() {
|
||||
return this.rootRoute == '/reader';
|
||||
}
|
||||
|
||||
redirectIfNeeded() {
|
||||
@@ -215,22 +245,6 @@ class App extends Vue {
|
||||
window.history.replaceState({}, '', '/');
|
||||
this.$router.replace({ path: '/reader', query: q });
|
||||
}
|
||||
|
||||
//yandex-метрика для omnireader
|
||||
if (this.config.branch == 'production' && this.mode == 'omnireader' && !this.yaMetricsDone) {
|
||||
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
||||
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");// eslint-disable-line no-unexpected-multiline
|
||||
|
||||
ym(52347334, "init", {// eslint-disable-line no-undef
|
||||
id:52347334,
|
||||
clickmap:true,
|
||||
trackLinks:true,
|
||||
accurateTrackBounce:true
|
||||
});
|
||||
|
||||
this.yaMetricsDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -244,68 +258,28 @@ class App extends Vue {
|
||||
line-height: 140%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
line-height: 1;
|
||||
background-color: #ccc;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 0;
|
||||
background-color: #E6EDF4;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-menu-vertical:not(.el-menu--collapse) {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
border: 0;
|
||||
}
|
||||
.el-menu-item {
|
||||
font-size: 85%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body, html, #app {
|
||||
body, html, #app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font: normal 12pt ReaderDefault;
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.dborder {
|
||||
border: 2px solid yellow !important;
|
||||
}
|
||||
|
||||
.icon-rotate {
|
||||
vertical-align: middle;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
.notify-button-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Book в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Card в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<el-container direction="vertical">
|
||||
<el-tabs type="border-card" style="height: 100%;" v-model="selectedTab">
|
||||
<el-tab-pane label="Поиск"></el-tab-pane>
|
||||
<el-tab-pane label="Автор"></el-tab-pane>
|
||||
<el-tab-pane label="Книга"></el-tab-pane>
|
||||
<el-tab-pane label="История"></el-tab-pane>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
<div>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -18,7 +12,7 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
const rootRoute = '/cardindex';
|
||||
const selfRoute = '/cardindex';
|
||||
const tab2Route = [
|
||||
'/cardindex/search',
|
||||
'/cardindex/card',
|
||||
@@ -51,7 +45,7 @@ class CardIndex extends Vue {
|
||||
if (t !== this.selectedTab)
|
||||
this.selectedTab = t.toString();
|
||||
} else {
|
||||
if (route == rootRoute && lastActiveTab !== null)
|
||||
if (route == selfRoute && lastActiveTab !== null)
|
||||
this.setRouteByTab(lastActiveTab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел History в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Search в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Help в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Income в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Страница не найдена
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Возможности читалки:</h4>
|
||||
<span class="text-h6 text-bold">Возможности читалки:</span>
|
||||
<ul>
|
||||
<li>загрузка любой страницы интернета</li>
|
||||
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||
<li>работа в автономном режиме (без связи)</li>
|
||||
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
||||
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
||||
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||
<li>кэширование файлов книг на клиенте и на сервере</li>
|
||||
<li>открытие книг с локального диска</li>
|
||||
<li>плавный скроллинг текста</li>
|
||||
@@ -13,7 +14,6 @@
|
||||
<li>поиск по тексту и копирование фрагмента</li>
|
||||
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
||||
<li>управление кликом и с клавиатуры</li>
|
||||
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
|
||||
<li>регистрация не требуется</li>
|
||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||
</ul>
|
||||
@@ -22,18 +22,17 @@
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||
|
||||
<p>Для автономной загрузки читалки (без интернета):<br>
|
||||
В Google Chrome можно установить флаг <span class="clickable" @click="copyText('chrome://flags/#show-saved-copy', 'Ссылка на флаг успешно скопирована в буфер обмена. Можно открыть ее в новой вкладке.')">chrome://flags/#show-saved-copy</span>
|
||||
в значение "Primary". В этом случае на стандартной странице "нет соединения" появится кнопка для автономной загрузки сайта из кэша.<br>
|
||||
В Mozilla Firefox в автономном режиме сайт загружается из кэша автоматически. Если этого не происходит, можно установить опцию
|
||||
"Веб-разработка" -> "Работать автономно".</p>
|
||||
|
||||
<div v-show="mode == 'omnireader'">
|
||||
<p>Вы также можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
<strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||
</span>
|
||||
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.
|
||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<br>или перетащив на панель закладок следующую ссылку:
|
||||
<br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
|
||||
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
||||
</p>
|
||||
</div>
|
||||
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
||||
@@ -61,9 +60,9 @@ class CommonHelpPage extends Vue {
|
||||
const result = await copyTextToClipboard(text);
|
||||
const msg = (result ? mes : 'Копирование не удалось');
|
||||
if (result)
|
||||
this.$notify.success({message: msg});
|
||||
this.$root.notify.success(msg);
|
||||
else
|
||||
this.$notify.error({message: msg});
|
||||
this.$root.notify.error(msg);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -71,20 +70,16 @@ class CommonHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
.copy-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,51 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="box">
|
||||
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
|
||||
<p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/yandex.png">
|
||||
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
|
||||
<div class="para">{{ yandexAddress }}</div>
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
|
||||
<div class="para">{{ yandexAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/paypal.png">
|
||||
<div class="para">{{ paypalAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/bitcoin.png">
|
||||
<el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
|
||||
<div class="para">{{ bitcoinAddress }}</div>
|
||||
<div class="para">{{ bitcoinAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/litecoin.png">
|
||||
<el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
|
||||
<div class="para">{{ litecoinAddress }}</div>
|
||||
<div class="para">{{ litecoinAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/monero.png">
|
||||
<el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
|
||||
<div class="para">{{ moneroAddress }}</div>
|
||||
<div class="para">{{ moneroAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,6 +61,7 @@ export default @Component({
|
||||
})
|
||||
class DonateHelpPage extends Vue {
|
||||
yandexAddress = '410018702323056';
|
||||
paypalAddress = 'bookpauk@gmail.com';
|
||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
||||
@@ -54,9 +76,9 @@ class DonateHelpPage extends Vue {
|
||||
async copyAddress(address, prefix) {
|
||||
const result = await copyTextToClipboard(address);
|
||||
if (result)
|
||||
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
|
||||
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
||||
else
|
||||
this.$notify.error({message: 'Копирование не удалось'});
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -64,12 +86,10 @@ class DonateHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.p {
|
||||
@@ -79,15 +99,10 @@ class DonateHelpPage extends Vue {
|
||||
}
|
||||
|
||||
.box {
|
||||
flex: 1;
|
||||
max-width: 550px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.address {
|
||||
padding-top: 10px;
|
||||
margin-top: 20px;
|
||||
@@ -97,13 +112,16 @@ h5 {
|
||||
margin: 10px 10px 10px 40px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 130px;
|
||||
position: relative;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -4,23 +4,20 @@
|
||||
Справка
|
||||
</template>
|
||||
|
||||
<el-tabs type="border-card" v-model="selectedTab">
|
||||
<el-tab-pane class="tab" label="Общее">
|
||||
<CommonHelpPage></CommonHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Клавиатура">
|
||||
<HotkeysHelpPage></HotkeysHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Мышь/тачпад">
|
||||
<MouseHelpPage></MouseHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="История версий" name="releases">
|
||||
<VersionHistoryPage></VersionHistoryPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Помочь проекту" name="donate">
|
||||
<DonateHelpPage></DonateHelpPage>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="col column" style="min-width: 600px">
|
||||
<q-btn-toggle
|
||||
v-model="selectedTab"
|
||||
toggle-color="primary"
|
||||
no-caps unelevated
|
||||
:options="buttons"
|
||||
/>
|
||||
<div class="separator"></div>
|
||||
|
||||
<keep-alive>
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
@@ -33,32 +30,54 @@ import Window from '../../share/Window.vue';
|
||||
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
|
||||
const pages = {
|
||||
'CommonHelpPage': CommonHelpPage,
|
||||
'HotkeysHelpPage': HotkeysHelpPage,
|
||||
'MouseHelpPage': MouseHelpPage,
|
||||
'VersionHistoryPage': VersionHistoryPage,
|
||||
'DonateHelpPage': DonateHelpPage,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
['CommonHelpPage', 'Общее'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
CommonHelpPage,
|
||||
HotkeysHelpPage,
|
||||
MouseHelpPage,
|
||||
DonateHelpPage,
|
||||
VersionHistoryPage,
|
||||
},
|
||||
components: Object.assign({ Window }, pages),
|
||||
})
|
||||
class HelpPage extends Vue {
|
||||
selectedTab = null;
|
||||
selectedTab = 'CommonHelpPage';
|
||||
|
||||
close() {
|
||||
this.$emit('help-toggle');
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
if (pages[this.selectedTab])
|
||||
return pages[this.selectedTab];
|
||||
return null;
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
let result = [];
|
||||
for (const tab of tabs)
|
||||
result.push({label: tab[1], value: tab[0]});
|
||||
return result;
|
||||
}
|
||||
|
||||
activateDonateHelpPage() {
|
||||
this.selectedTab = 'donate';
|
||||
this.selectedTab = 'DonateHelpPage';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
this.selectedTab = 'releases';
|
||||
this.selectedTab = 'VersionHistoryPage';
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
@@ -72,16 +91,8 @@ class HelpPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Управление с помощью горячих клавиш:</h4>
|
||||
<span class="text-h6 text-bold">Управление с помощью горячих клавиш:</span>
|
||||
<ul>
|
||||
<li><b>F1, H</b> - открыть справку</li>
|
||||
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
|
||||
@@ -20,6 +20,7 @@
|
||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
||||
<li><b>X</b> - открыть недавние</li>
|
||||
<li><b>O</b> - автономный режим</li>
|
||||
<li><b>S</b> - открыть окно настроек</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -41,14 +42,9 @@ class HotkeysHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Управление с помощью мыши/тачпада:</h4>
|
||||
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
|
||||
<ul>
|
||||
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
||||
<div class="click-map-page">
|
||||
<ClickMapPage ref="clickMapPage"></ClickMapPage>
|
||||
</div>
|
||||
|
||||
<li><b>ПКМ</b> - показать/скрыть панель управления</li>
|
||||
<li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
|
||||
<br>
|
||||
<li>Жесты для тачскрина:</li>
|
||||
<ul>
|
||||
<li style="list-style-type: square">от центра вверх: на весь экран</li>
|
||||
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
|
||||
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
|
||||
</ul>
|
||||
* Для управления с помощью мыши/тачпада необходимо установить галочку "Включить управление кликом" в настройках
|
||||
* Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -39,17 +49,12 @@ class MouseHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.click-map-page {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div id="versionHistoryPage" class="page">
|
||||
<span class="text-h6 text-bold">История версий:</span>
|
||||
<br><br>
|
||||
|
||||
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
|
||||
<p>
|
||||
{{ item }}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
<h4>История версий:</h4>
|
||||
<br>
|
||||
|
||||
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
||||
@@ -58,15 +59,11 @@ class VersionHistoryPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -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,42 +1,55 @@
|
||||
<template>
|
||||
<div ref="main" class="main">
|
||||
<div class="part">
|
||||
<span class="greeting bold-font">{{ title }}</span>
|
||||
<div class="space"></div>
|
||||
<div ref="main" class="column no-wrap" style="min-height: 500px">
|
||||
<div class="relative-position">
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
|
||||
</div>
|
||||
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
|
||||
<span class="greeting"><b>{{ title }}</b></span>
|
||||
<div class="q-my-sm"></div>
|
||||
<span class="greeting">Добро пожаловать!</span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
||||
</div>
|
||||
|
||||
<div class="part center">
|
||||
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
|
||||
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
|
||||
</el-input>
|
||||
<div class="space"></div>
|
||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
|
||||
<template v-slot:append>
|
||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
||||
<el-button size="mini" @click="loadFileClick">
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||
Загрузить файл с диска
|
||||
</el-button>
|
||||
<div class="space"></div>
|
||||
<el-button size="mini" @click="loadBufferClick">
|
||||
</q-btn>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
Из буфера обмена
|
||||
</el-button>
|
||||
<div class="space"></div>
|
||||
<div class="space"></div>
|
||||
<div v-if="mode == 'omnireader'" 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">
|
||||
</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="space"></div>
|
||||
<div class="q-my-sm"></div>
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
|
||||
</div>
|
||||
|
||||
<div class="part bottom">
|
||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||
<span class="bottom-span">{{ version }}</span>
|
||||
|
||||
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||
</div>
|
||||
|
||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||
@@ -47,10 +60,14 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||
|
||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
GithubCorner,
|
||||
PasteTextPage,
|
||||
},
|
||||
})
|
||||
@@ -85,16 +102,22 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
get version() {
|
||||
return `v${this.$store.state.config.version}`;
|
||||
return this.$store.state.config.version;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
if (this.bookUrl) {
|
||||
this.$emit('load-book', {url: this.bookUrl});
|
||||
this.$emit('load-book', {url: this.bookUrl, force: true});
|
||||
this.bookUrl = '';
|
||||
}
|
||||
}
|
||||
@@ -137,6 +160,10 @@ class LoaderPage extends Vue {
|
||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||
}
|
||||
|
||||
openOldVersion() {
|
||||
window.open('http://old.omnireader.ru', '_blank');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.pasteTextActive) {
|
||||
return this.$refs.pasteTextPage.keyHook(event);
|
||||
@@ -166,55 +193,19 @@ class LoaderPage extends Vue {
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.part {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 120%;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bottom-span {
|
||||
font-size: 70%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.space {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,14 +3,12 @@
|
||||
<template slot="header">
|
||||
<span style="position: relative; top: -3px">
|
||||
Вставьте текст и нажмите
|
||||
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
или F2
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
|
||||
</div>
|
||||
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
|
||||
<hr/>
|
||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||
</Window>
|
||||
@@ -70,7 +68,7 @@ class PasteTextPage extends Vue {
|
||||
}
|
||||
|
||||
loadBuffer() {
|
||||
this.$emit('load-buffer', {buffer: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
|
||||
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
<template>
|
||||
<div v-show="visible" class="main">
|
||||
<div class="center">
|
||||
<el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
|
||||
<p class="text">{{ text }}</p>
|
||||
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
||||
<div class="column justify-start items-center" style="height: 250px">
|
||||
<q-circular-progress
|
||||
show-value
|
||||
instant-feedback
|
||||
font-size="13px"
|
||||
:value="percentage"
|
||||
size="100px"
|
||||
:thickness="0.11"
|
||||
color="green-7"
|
||||
track-color="grey-4"
|
||||
class="q-ma-md"
|
||||
>
|
||||
<span class="text-yellow">{{ percentage }}%</span>
|
||||
</q-circular-progress>
|
||||
|
||||
<div>
|
||||
<span class="text-yellow">{{ text }}</span>
|
||||
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,11 +27,13 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ruMessage = {
|
||||
'start': ' ',
|
||||
'finish': ' ',
|
||||
'error': ' ',
|
||||
'queue': 'очередь',
|
||||
'download': 'скачивание',
|
||||
'decompress': 'распаковка',
|
||||
'convert': 'конвертирование',
|
||||
@@ -32,68 +50,51 @@ class ProgressPage extends Vue {
|
||||
step = 1;
|
||||
progress = 0;
|
||||
visible = false;
|
||||
iconStyle = '';
|
||||
|
||||
show() {
|
||||
this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
|
||||
this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
|
||||
this.text = '';
|
||||
this.totalSteps = 1;
|
||||
this.step = 1;
|
||||
this.progress = 0;
|
||||
this.iconAngle = 0;
|
||||
this.ani = false;
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visible = false;
|
||||
this.text = '';
|
||||
this.iconAngle = 0;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
if (state.state)
|
||||
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
||||
if (state.state) {
|
||||
if (state.state == 'queue') {
|
||||
this.text = (state.place ? 'Номер в очереди: ' + state.place : '');
|
||||
} else {
|
||||
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
||||
}
|
||||
}
|
||||
this.step = (state.step ? state.step : this.step);
|
||||
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
||||
this.progress = state.progress || 0;
|
||||
|
||||
if (!this.ani) {
|
||||
(async() => {
|
||||
this.ani = true;
|
||||
this.iconAngle += 30;
|
||||
this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
|
||||
await utils.sleep(150);
|
||||
this.ani = false;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
get percentage() {
|
||||
let circle = document.querySelector('path[class="el-progress-circle__path"]');
|
||||
if (circle)
|
||||
circle.style.transition = '';
|
||||
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
color: white;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.el-progress__text {
|
||||
color: lightgreen !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,54 +1,67 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-header v-show="toolBarActive" height='50px'>
|
||||
<div ref="header" class="header">
|
||||
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
|
||||
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
|
||||
</el-tooltip>
|
||||
<div class="column no-wrap">
|
||||
<div ref="header" class="header" v-show="toolBarActive">
|
||||
<div ref="buttons" class="row justify-between no-wrap">
|
||||
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
|
||||
<q-icon name="la la-arrow-left" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">Загрузить книгу</q-tooltip>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
|
||||
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
|
||||
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
|
||||
</el-tooltip>
|
||||
<button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
|
||||
<q-icon name="la la-angle-left" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Действие назад</q-tooltip>
|
||||
</button>
|
||||
<button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
|
||||
<q-icon name="la la-angle-right" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Действие вперед</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
|
||||
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
|
||||
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
|
||||
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
|
||||
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
|
||||
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
|
||||
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
|
||||
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||
</button>
|
||||
<button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
|
||||
<q-icon name="la la-film" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Плавный скроллинг</q-tooltip>
|
||||
</button>
|
||||
<button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
|
||||
<q-icon name="la la-angle-double-right" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На страницу</q-tooltip>
|
||||
</button>
|
||||
<button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
|
||||
<q-icon name="la la-search" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Найти в тексте</q-tooltip>
|
||||
</button>
|
||||
<button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
|
||||
<q-icon name="la la-copy" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Скопировать текст со страницы</q-tooltip>
|
||||
</button>
|
||||
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
||||
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Принудительно обновить книгу в обход кэша</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
|
||||
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
|
||||
</el-tooltip>
|
||||
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
|
||||
<q-icon name="la la-unlink" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Автономный режим (без интернета)</q-tooltip>
|
||||
</button>
|
||||
<button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
|
||||
<q-icon name="la la-book-open" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть недавние</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-tooltip content="Настроить" :open-delay="1000" effect="light">
|
||||
<el-button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')"><i class="el-icon-setting"></i></el-button>
|
||||
</el-tooltip>
|
||||
<button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
|
||||
<q-icon name="la la-cog" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">Настроить</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</el-header>
|
||||
</div>
|
||||
|
||||
<el-main>
|
||||
<div class="main col row relative-position">
|
||||
<keep-alive>
|
||||
<component ref="page" :is="activePage"
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
@load-book="loadBook"
|
||||
@load-file="loadFile"
|
||||
@book-pos-changed="bookPosChanged"
|
||||
@@ -69,108 +82,72 @@
|
||||
@stop-text-search="stopTextSearch">
|
||||
</SearchPage>
|
||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
|
||||
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
||||
<SettingsPage v-show="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
||||
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||
|
||||
<el-dialog
|
||||
title="Что нового:"
|
||||
:visible.sync="whatsNewVisible"
|
||||
width="80%">
|
||||
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||
<template slot="header">
|
||||
Что нового:
|
||||
</template>
|
||||
|
||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||
|
||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="whatsNewDisable">Больше не показывать</el-button>
|
||||
<span slot="footer">
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</Dialog>
|
||||
|
||||
<el-dialog
|
||||
title="Внимание!"
|
||||
:visible.sync="migrationVisible1"
|
||||
width="90%">
|
||||
<div>
|
||||
Появилась httpS-версия сайта по адресу <a href="https://omnireader.ru" target="_blank">https://omnireader.ru</a><br>
|
||||
Работа по httpS-протоколу, помимо безопасности соединения, позволяет воспользоваться всеми возможностями
|
||||
современных браузеров, а именно, применительно к нашему ресурсу:
|
||||
<Dialog ref="dialog2" v-model="donationVisible">
|
||||
<template slot="header">
|
||||
Здравствуйте, уважаемые читатели!
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
||||
|
||||
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
||||
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
||||
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
||||
|
||||
<ul>
|
||||
<li>возможность автономной работы с читалкой (без доступа к интернету), кеширование сайта через appcache</li>
|
||||
<li>безопасная передача на сервер данных о настройках и читаемых книгах при включенной синхронизации; все данные шифруются на стороне
|
||||
браузера ключом доступа и никто (в т.ч. администратор) не имеет возможности их прочитать
|
||||
<li>использование встроенных в JS функций шифрования и других</li>
|
||||
<li>непрерывно улучшаемой</li>
|
||||
<li>без рекламы</li>
|
||||
<li>без регистрации</li>
|
||||
<li>Open Source</li>
|
||||
</ul>
|
||||
|
||||
Для того, чтобы перейти на новую версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
|
||||
<ul>
|
||||
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
|
||||
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
|
||||
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||
после этого все данные будут автоматически сохранены на сервер
|
||||
</span>
|
||||
</li>
|
||||
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
|
||||
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||
произойдет переход на https-версию читалки и откроется окно для ввода ключа
|
||||
</span><br>
|
||||
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
|
||||
</span>
|
||||
</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>
|
||||
|
||||
Старая http-версия сайта будет доступна до конца 2019 года.<br>
|
||||
Приносим извинения за доставленные неудобства.
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
|
||||
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
|
||||
<span slot="footer">
|
||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||
<br>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</Dialog>
|
||||
|
||||
<el-dialog
|
||||
title="Внимание!"
|
||||
:visible.sync="migrationVisible2"
|
||||
width="90%">
|
||||
<div>
|
||||
Информация для пользователей старой версии читалки по адресу <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a><br>
|
||||
Для того, чтобы перейти на новую httpS-версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
|
||||
<ul>
|
||||
<li>перейти на старую версию ресурса <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a></li>
|
||||
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
|
||||
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
|
||||
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||
после этого все данные будут автоматически сохранены на сервер
|
||||
</span>
|
||||
</li>
|
||||
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
|
||||
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||
произойдет переход на https-версию читалки и откроется окно для ввода ключа
|
||||
</span><br>
|
||||
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
Старая http-версия сайта будет доступна до конца 2019 года.<br>
|
||||
Приносим извинения за доставленные неудобства.
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
|
||||
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
</el-main>
|
||||
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -197,6 +174,7 @@ import bookManager from './share/bookManager';
|
||||
import readerApi from '../../api/reader';
|
||||
import * as utils from '../../share/utils';
|
||||
import {versionHistory} from './versionHistory';
|
||||
import Dialog from '../share/Dialog.vue';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
@@ -212,19 +190,25 @@ export default @Component({
|
||||
HelpPage,
|
||||
ClickMapPage,
|
||||
ServerStorage,
|
||||
Dialog,
|
||||
},
|
||||
watch: {
|
||||
bookPos: function(newValue) {
|
||||
if (newValue !== undefined && this.activePage == 'TextPage') {
|
||||
const textPage = this.$refs.page;
|
||||
|
||||
if (textPage.bookPos != newValue) {
|
||||
textPage.bookPos = newValue;
|
||||
}
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
|
||||
if (!this.scrollingActive)
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
else
|
||||
this.scrollingSetRecentBook(newValue);
|
||||
}
|
||||
},
|
||||
routeParamPos: function(newValue) {
|
||||
if (newValue !== undefined && newValue != this.bookPos) {
|
||||
if (!this.paramPosIgnore && newValue !== undefined && newValue != this.bookPos) {
|
||||
this.bookPos = newValue;
|
||||
}
|
||||
},
|
||||
@@ -257,6 +241,7 @@ class Reader extends Vue {
|
||||
searchActive = false;
|
||||
copyTextActive = false;
|
||||
recentBooksActive = false;
|
||||
offlineModeActive = false;
|
||||
settingsActive = false;
|
||||
helpActive = false;
|
||||
clickMapActive = false;
|
||||
@@ -273,8 +258,7 @@ class Reader extends Vue {
|
||||
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
migrationVisible1 = false;
|
||||
migrationVisible2 = false;
|
||||
donationVisible = false;
|
||||
|
||||
created() {
|
||||
this.loading = true;
|
||||
@@ -287,10 +271,6 @@ class Reader extends Vue {
|
||||
|
||||
this.lastActivePage = false;
|
||||
|
||||
this.debouncedUpdateRoute = _.debounce(() => {
|
||||
this.updateRoute();
|
||||
}, 1000);
|
||||
|
||||
this.debouncedSetRecentBook = _.debounce(async(newValue) => {
|
||||
const recent = this.mostRecentBook();
|
||||
if (recent && (recent.bookPos != newValue || recent.bookPosSeen !== this.bookPosSeen)) {
|
||||
@@ -298,23 +278,23 @@ class Reader extends Vue {
|
||||
|
||||
if (this.actionCur < 0 || (this.actionCur >= 0 && this.actionList[this.actionCur] != newValue))
|
||||
this.addAction(newValue);
|
||||
|
||||
this.paramPosIgnore = true;
|
||||
this.updateRoute();
|
||||
await this.$nextTick();
|
||||
this.paramPosIgnore = false;
|
||||
}
|
||||
}, 500);
|
||||
}, 500, {'maxWait':5000});
|
||||
|
||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
}, 15000, {'maxWait':20000});
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||
});
|
||||
|
||||
this.loadSettings();
|
||||
|
||||
//TODO: убрать в будущем
|
||||
if (this.showToolButton['history']) {
|
||||
const newShowToolButton = Object.assign({}, this.showToolButton);
|
||||
newShowToolButton['recentBooks'] = true;
|
||||
delete newShowToolButton['history'];
|
||||
const newSettings = Object.assign({}, this.settings, { showToolButton: newShowToolButton });
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@@ -324,7 +304,7 @@ class Reader extends Vue {
|
||||
await bookManager.init(this.settings);
|
||||
bookManager.addEventListener(this.bookManagerEvent);
|
||||
|
||||
if (this.$root.rootRoute == '/reader') {
|
||||
if (this.$root.rootRoute() == '/reader') {
|
||||
if (this.routeParamUrl) {
|
||||
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
|
||||
} else {
|
||||
@@ -332,13 +312,15 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
await this.$refs.serverStorage.init();
|
||||
this.checkSetStorageAccessKey();
|
||||
this.checkActivateDonateHelpPage();
|
||||
this.loading = false;
|
||||
|
||||
await this.$refs.serverStorage.init();
|
||||
this.updateRoute();
|
||||
|
||||
await this.showWhatsNew();
|
||||
await this.showMigration();
|
||||
await this.showDonation();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -350,16 +332,22 @@ class Reader extends Vue {
|
||||
this.clickControl = settings.clickControl;
|
||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||
this.showMigrationDialog = settings.showMigrationDialog;
|
||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
|
||||
this.updateHeaderMinWidth();
|
||||
}
|
||||
|
||||
updateHeaderMinWidth() {
|
||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
||||
if (this.$refs.header)
|
||||
this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
|
||||
if (this.$refs.buttons)
|
||||
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
|
||||
(async() => {
|
||||
await utils.sleep(1000);
|
||||
if (this.$refs.header)
|
||||
this.$refs.header.style.overflowX = 'auto';
|
||||
})();
|
||||
}
|
||||
|
||||
checkSetStorageAccessKey() {
|
||||
@@ -415,31 +403,39 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async showMigration() {
|
||||
async showDonation() {
|
||||
await utils.sleep(3000);
|
||||
if (!this.settingsActive &&
|
||||
this.mode == 'omnireader' && this.showMigrationDialog && this.migrationRemindDate != utils.formatDate(new Date(), 'coDate')) {
|
||||
if (window.location.protocol == 'http:') {
|
||||
this.migrationVisible1 = true;
|
||||
} else if (window.location.protocol == 'https:') {
|
||||
this.migrationVisible2 = true;
|
||||
}
|
||||
const today = utils.formatDate(new Date(), 'coDate');
|
||||
|
||||
if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
||||
this.donationVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
migrationDialogDisable() {
|
||||
this.migrationVisible1 = false;
|
||||
this.migrationVisible2 = false;
|
||||
if (this.showMigrationDialog) {
|
||||
const newSettings = Object.assign({}, this.settings, { showMigrationDialog: false });
|
||||
donationDialogDisable() {
|
||||
this.donationVisible = false;
|
||||
if (this.showDonationDialog2020) {
|
||||
const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
}
|
||||
|
||||
migrationDialogRemind() {
|
||||
this.migrationVisible1 = false;
|
||||
this.migrationVisible2 = false;
|
||||
this.commit('reader/setMigrationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||
donationDialogRemind() {
|
||||
this.donationVisible = false;
|
||||
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||
}
|
||||
|
||||
openDonate() {
|
||||
this.donationVisible = false;
|
||||
this.donateToggle();
|
||||
}
|
||||
|
||||
async copyLink(link) {
|
||||
const result = await utils.copyTextToClipboard(link);
|
||||
if (result)
|
||||
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
||||
else
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
|
||||
openVersionHistory() {
|
||||
@@ -502,33 +498,44 @@ class Reader extends Vue {
|
||||
if (event.bookPosSeen !== undefined)
|
||||
this.bookPosSeen = event.bookPosSeen;
|
||||
this.bookPos = event.bookPos;
|
||||
this.debouncedUpdateRoute();
|
||||
}
|
||||
|
||||
async bookManagerEvent(eventName) {
|
||||
if (eventName == 'recent-changed') {
|
||||
if (this.recentBooksActive) {
|
||||
await this.$refs.recentBooksPage.updateTableData();
|
||||
}
|
||||
}
|
||||
|
||||
async bookManagerEvent(eventName, value) {
|
||||
if (eventName == 'set-recent' || eventName == 'recent-deleted') {
|
||||
const oldBook = this.mostRecentBookReactive;
|
||||
const oldBook = (this.textPage ? this.textPage.lastBook : null);
|
||||
const oldPos = (this.textPage ? this.textPage.bookPos : null);
|
||||
const newBook = bookManager.mostRecentBook();
|
||||
|
||||
if (!(oldBook && newBook && oldBook.key == newBook.key)) {
|
||||
this.mostRecentBook();
|
||||
}
|
||||
|
||||
if (oldBook && newBook) {
|
||||
if (oldBook.key != newBook.key) {
|
||||
if (oldBook.key != newBook.key || oldBook.path != newBook.path) {
|
||||
this.loadingBook = true;
|
||||
try {
|
||||
await this.loadBook(newBook);
|
||||
} finally {
|
||||
this.loadingBook = false;
|
||||
}
|
||||
} else if (oldBook.bookPos != newBook.bookPos) {
|
||||
} else if (oldPos != newBook.bookPos) {
|
||||
while (this.loadingBook) await utils.sleep(100);
|
||||
this.bookPosChanged({bookPos: newBook.bookPos});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eventName == 'recent-changed') {
|
||||
if (this.recentBooksActive) {
|
||||
await this.$refs.recentBooksPage.updateTableData();
|
||||
}
|
||||
|
||||
//сохранение в serverStorage
|
||||
if (value) {
|
||||
await utils.sleep(500);
|
||||
await this.$refs.serverStorage.saveRecent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get toolBarActive() {
|
||||
@@ -549,8 +556,8 @@ class Reader extends Vue {
|
||||
return this.$store.state.reader.whatsNewContentHash;
|
||||
}
|
||||
|
||||
get migrationRemindDate() {
|
||||
return this.$store.state.reader.migrationRemindDate;
|
||||
get donationRemindDate() {
|
||||
return this.$store.state.reader.donationRemindDate;
|
||||
}
|
||||
|
||||
addAction(pos) {
|
||||
@@ -571,22 +578,9 @@ class Reader extends Vue {
|
||||
fullScreenToggle() {
|
||||
this.fullScreenActive = !this.fullScreenActive;
|
||||
if (this.fullScreenActive) {
|
||||
const element = document.documentElement;
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
} else if (element.webkitrequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
} else if (element.mozRequestFullscreen) {
|
||||
element.mozRequestFullScreen();
|
||||
}
|
||||
this.$q.fullscreen.request();
|
||||
} else {
|
||||
if (document.cancelFullScreen) {
|
||||
document.cancelFullScreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitCancelFullScreen) {
|
||||
document.webkitCancelFullScreen();
|
||||
}
|
||||
this.$q.fullscreen.exit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,7 +603,8 @@ class Reader extends Vue {
|
||||
|
||||
setPositionToggle() {
|
||||
this.setPositionActive = !this.setPositionActive;
|
||||
if (this.setPositionActive && this.activePage == 'TextPage' && this.mostRecentBook()) {
|
||||
const page = this.$refs.page;
|
||||
if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
|
||||
this.closeAllTextPages();
|
||||
this.setPositionActive = true;
|
||||
|
||||
@@ -637,6 +632,10 @@ class Reader extends Vue {
|
||||
page.stopTextScrolling();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scrollingActive) {
|
||||
this.scrollingSetRecentBook.flush();
|
||||
}
|
||||
}
|
||||
|
||||
stopSearch() {
|
||||
@@ -685,6 +684,10 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
recentBooksClose() {
|
||||
this.recentBooksActive = false;
|
||||
}
|
||||
|
||||
recentBooksToggle() {
|
||||
this.recentBooksActive = !this.recentBooksActive;
|
||||
if (this.recentBooksActive) {
|
||||
@@ -696,6 +699,11 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
offlineModeToggle() {
|
||||
this.offlineModeActive = !this.offlineModeActive;
|
||||
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
||||
}
|
||||
|
||||
settingsToggle() {
|
||||
this.settingsActive = !this.settingsActive;
|
||||
if (this.settingsActive) {
|
||||
@@ -745,7 +753,7 @@ class Reader extends Vue {
|
||||
buttonClick(button) {
|
||||
const activeClass = this.buttonActiveClass(button);
|
||||
|
||||
this.$refs[button].$el.blur();
|
||||
this.$refs[button].blur();
|
||||
|
||||
if (activeClass['tool-button-disabled'])
|
||||
return;
|
||||
@@ -781,11 +789,14 @@ class Reader extends Vue {
|
||||
case 'copyText':
|
||||
this.copyTextToggle();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
break;
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
case 'offlineMode':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
case 'settings':
|
||||
this.settingsToggle();
|
||||
@@ -805,14 +816,16 @@ class Reader extends Vue {
|
||||
case 'scrolling':
|
||||
case 'search':
|
||||
case 'copyText':
|
||||
case 'refresh':
|
||||
case 'offlineMode':
|
||||
case 'recentBooks':
|
||||
case 'settings':
|
||||
if (this[`${button}Active`])
|
||||
if (this.progressActive) {
|
||||
classResult = classDisabled;
|
||||
} else if (this[`${button}Active`]) {
|
||||
classResult = classActive;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (button) {
|
||||
case 'undoAction':
|
||||
if (this.actionCur <= 0)
|
||||
classResult = classDisabled;
|
||||
@@ -890,6 +903,7 @@ class Reader extends Vue {
|
||||
this.updateRoute();
|
||||
const textPage = this.$refs.page;
|
||||
if (textPage.showBook) {
|
||||
this.textPage = textPage;
|
||||
textPage.lastBook = last;
|
||||
textPage.bookPos = (last.bookPos !== undefined ? last.bookPos : 0);
|
||||
|
||||
@@ -908,14 +922,19 @@ class Reader extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = opts.url;
|
||||
this.closeAllTextPages();
|
||||
|
||||
let url = encodeURI(decodeURI(opts.url));
|
||||
|
||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||
(url.indexOf('file://') != 0))
|
||||
url = 'http://' + url;
|
||||
|
||||
// уже просматривается сейчас
|
||||
const lastBook = (this.$refs.page ? this.$refs.page.lastBook : null);
|
||||
if (!opts.force && lastBook && lastBook.url == url && await bookManager.hasBookParsed(lastBook)) {
|
||||
const lastBook = (this.textPage ? this.textPage.lastBook : null);
|
||||
if (!opts.force && lastBook && lastBook.url == url &&
|
||||
(!opts.path || opts.path == lastBook.path) &&
|
||||
await bookManager.hasBookParsed(lastBook)) {
|
||||
this.loaderActive = false;
|
||||
return;
|
||||
}
|
||||
@@ -944,7 +963,7 @@ class Reader extends Vue {
|
||||
|
||||
if (!opts.force) {
|
||||
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
||||
const bookParsed = await bookManager.getBook({url}, (prog) => {
|
||||
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
|
||||
progress.setState({progress: prog});
|
||||
});
|
||||
|
||||
@@ -965,6 +984,7 @@ class Reader extends Vue {
|
||||
// иначе идем на сервер
|
||||
// пытаемся загрузить готовый файл с сервера
|
||||
if (wasOpened.path) {
|
||||
progress.setState({totalSteps: 5});
|
||||
try {
|
||||
const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => {
|
||||
progress.setState(state);
|
||||
@@ -980,7 +1000,7 @@ class Reader extends Vue {
|
||||
// не удалось, скачиваем книгу полностью с конвертацией
|
||||
let loadCached = true;
|
||||
if (!book) {
|
||||
book = await readerApi.loadBook(url, (state) => {
|
||||
book = await readerApi.loadBook({url, enableSitesFilter: this.enableSitesFilter}, (state) => {
|
||||
progress.setState(state);
|
||||
});
|
||||
loadCached = false;
|
||||
@@ -1010,7 +1030,7 @@ class Reader extends Vue {
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1034,7 +1054,7 @@ class Reader extends Vue {
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,7 +1070,7 @@ class Reader extends Vue {
|
||||
let page = this.$refs.page;
|
||||
while (this.blinkCount) {
|
||||
this.showRefreshIcon = !this.showRefreshIcon;
|
||||
if (page.blinkCachedLoadMessage)
|
||||
if (page && page.blinkCachedLoadMessage)
|
||||
page.blinkCachedLoadMessage(this.showRefreshIcon);
|
||||
await utils.sleep(500);
|
||||
if (this.stopBlink)
|
||||
@@ -1060,14 +1080,17 @@ class Reader extends Vue {
|
||||
}
|
||||
this.showRefreshIcon = true;
|
||||
this.inBlink = false;
|
||||
if (page.blinkCachedLoadMessage)
|
||||
if (page && page.blinkCachedLoadMessage)
|
||||
page.blinkCachedLoadMessage('finish');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.$root.rootRoute == '/reader') {
|
||||
if (this.$root.rootRoute() == '/reader') {
|
||||
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||
return;
|
||||
|
||||
let handled = false;
|
||||
if (!handled && this.helpActive)
|
||||
handled = this.$refs.helpPage.keyHook(event);
|
||||
@@ -1130,6 +1153,9 @@ class Reader extends Vue {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'KeyO':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
case 'KeyS':
|
||||
this.settingsToggle();
|
||||
break;
|
||||
@@ -1143,37 +1169,22 @@ class Reader extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
.header {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
background-color: #1B695F;
|
||||
color: #000;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow: hidden;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.main {
|
||||
background-color: #EBE2C9;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
margin: 0 2px 0 2px;
|
||||
margin: 0px 2px 0 2px;
|
||||
padding: 0;
|
||||
color: #3E843E;
|
||||
background-color: #E6EDF4;
|
||||
@@ -1181,15 +1192,14 @@ class Reader extends Vue {
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 3px 3px 5px black;
|
||||
}
|
||||
|
||||
.tool-button + .tool-button {
|
||||
margin: 0 2px 0 2px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.tool-button:hover {
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tool-button-active {
|
||||
@@ -1204,20 +1214,19 @@ class Reader extends Vue {
|
||||
.tool-button-active:hover {
|
||||
color: white;
|
||||
background-color: #81C581;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tool-button-disabled {
|
||||
color: lightgray;
|
||||
background-color: gray;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tool-button-disabled:hover {
|
||||
color: lightgray;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 200%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.space {
|
||||
@@ -1234,4 +1243,10 @@ i {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,96 +1,84 @@
|
||||
<template>
|
||||
<Window width="600px" ref="window" @close="close">
|
||||
<template slot="header">
|
||||
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
|
||||
<span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
|
||||
<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>
|
||||
|
||||
<el-table
|
||||
<a ref="download" style='display: none;'></a>
|
||||
|
||||
<q-table
|
||||
class="recent-books-table col"
|
||||
:data="tableData"
|
||||
style="width: 570px"
|
||||
size="mini"
|
||||
height="1px"
|
||||
stripe
|
||||
border
|
||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
||||
:header-cell-style = "headerCellStyle"
|
||||
:row-key = "rowKey"
|
||||
>
|
||||
:columns="columns"
|
||||
row-key="key"
|
||||
:pagination.sync="pagination"
|
||||
separator="cell"
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
dense
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
|
||||
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
|
||||
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
|
||||
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
type="index"
|
||||
width="35px"
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="touchDateTime"
|
||||
min-width="85px"
|
||||
sortable
|
||||
>
|
||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||
<span style="font-size: 90%">Время<br>просм.</span>
|
||||
</template>
|
||||
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||
<div class="desc" @click="loadBook(scope.row.url)">
|
||||
{{ scope.row.touchDate }}<br>
|
||||
{{ scope.row.touchTime }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<span v-html="props.cols[2].label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<el-table-column
|
||||
>
|
||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||
<!--el-input ref="input"
|
||||
:value="search" @input="search = $event"
|
||||
size="mini"
|
||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
|
||||
placeholder="Найти"/-->
|
||||
<div class="el-input el-input--mini">
|
||||
<input class="el-input__inner"
|
||||
ref="input"
|
||||
placeholder="Найти"
|
||||
style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
||||
:value="search" @input="search = $event.target.value"
|
||||
/>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="num" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 25px">
|
||||
{{ props.row.num }}
|
||||
</div>
|
||||
</template>
|
||||
</q-td>
|
||||
|
||||
<el-table-column
|
||||
min-width="280px"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div class="desc" @click="loadBook(scope.row.url)">
|
||||
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
||||
<span>{{ scope.row.desc.title }}</span>
|
||||
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<div class="break-word" style="width: 68px">
|
||||
{{ props.row.touchDate }}<br>
|
||||
{{ props.row.touchTime }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</q-td>
|
||||
|
||||
<el-table-column
|
||||
min-width="90px"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<div class="break-word" style="width: 332px; font-size: 90%">
|
||||
<div style="color: green">{{ props.row.desc.author }}</div>
|
||||
<div>{{ props.row.desc.title }}</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<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>
|
||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
||||
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
</el-table-column>
|
||||
<q-td key="close" :props="props" class="td-mp" auto-width>
|
||||
<div style="width: 38px">
|
||||
<q-btn
|
||||
dense
|
||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||
@click="handleDel(props.row.key)">
|
||||
<q-icon class="la la-times" size="14px" style="top: -6px"/>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-td>
|
||||
<q-td key="last" :props="props" class="no-mp">
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</el-table>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
@@ -104,6 +92,7 @@ import _ from 'lodash';
|
||||
import * as utils from '../../../share/utils';
|
||||
import Window from '../../share/Window.vue';
|
||||
import bookManager from '../share/bookManager';
|
||||
import readerApi from '../../../api/reader';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
@@ -119,52 +108,90 @@ class RecentBooksPage extends Vue {
|
||||
loading = false;
|
||||
search = null;
|
||||
tableData = [];
|
||||
columns = [];
|
||||
pagination = {};
|
||||
|
||||
created() {
|
||||
this.pagination = {rowsPerPage: 0};
|
||||
|
||||
this.columns = [
|
||||
{
|
||||
name: 'num',
|
||||
label: '#',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
field: 'num',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: 'Время<br>просм.',
|
||||
align: 'left',
|
||||
field: 'touchDateTime',
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
|
||||
},
|
||||
{
|
||||
name: 'desc',
|
||||
label: 'Название',
|
||||
align: 'left',
|
||||
field: 'descString',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'links',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'close',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'last',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$refs.window.init();
|
||||
|
||||
this.$nextTick(() => {
|
||||
//this.$refs.input.focus();
|
||||
//this.$refs.input.focus();//плохо на планшетах
|
||||
});
|
||||
(async() => {//отбражение подгрузки списка, иначе тормозит
|
||||
(async() => {//подгрузка списка
|
||||
if (this.initing)
|
||||
return;
|
||||
this.initing = true;
|
||||
|
||||
await this.updateTableData(3);
|
||||
await utils.sleep(200);
|
||||
|
||||
if (bookManager.loaded) {
|
||||
const t = Date.now();
|
||||
if (!bookManager.loaded) {
|
||||
await this.updateTableData(10);
|
||||
if (bookManager.getSortedRecent().length > 10)
|
||||
await utils.sleep(10*(Date.now() - t));
|
||||
} else {
|
||||
//для отзывчивости
|
||||
await utils.sleep(100);
|
||||
let i = 0;
|
||||
let j = 5;
|
||||
while (i < 500 && !bookManager.loaded) {
|
||||
if (i % j == 0) {
|
||||
bookManager.sortedRecentCached = null;
|
||||
await this.updateTableData(100);
|
||||
await this.updateTableData(20);
|
||||
j *= 2;
|
||||
}
|
||||
|
||||
await utils.sleep(100);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
//для отзывчивости
|
||||
await utils.sleep(100);
|
||||
}
|
||||
await this.updateTableData();
|
||||
this.initing = false;
|
||||
})();
|
||||
}
|
||||
|
||||
rowKey(row) {
|
||||
return row.key;
|
||||
}
|
||||
|
||||
async updateTableData(limit) {
|
||||
while (this.updating) await utils.sleep(100);
|
||||
this.updating = true;
|
||||
@@ -173,11 +200,13 @@ class RecentBooksPage extends Vue {
|
||||
this.loading = !!limit;
|
||||
const sorted = bookManager.getSortedRecent();
|
||||
|
||||
let num = 0;
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const book = sorted[i];
|
||||
if (book.deleted)
|
||||
continue;
|
||||
|
||||
num++;
|
||||
if (limit && result.length >= limit)
|
||||
break;
|
||||
|
||||
@@ -209,7 +238,7 @@ class RecentBooksPage extends Vue {
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
author = authorNames.join(', ');
|
||||
} else {
|
||||
} else {//TODO: убрать в будущем
|
||||
author = _.compact([
|
||||
fb2.lastName,
|
||||
fb2.firstName,
|
||||
@@ -219,19 +248,19 @@ class RecentBooksPage extends Vue {
|
||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
||||
|
||||
result.push({
|
||||
num,
|
||||
touchDateTime: book.touchTime,
|
||||
touchDate: t[0],
|
||||
touchTime: t[1],
|
||||
desc: {
|
||||
title: `${title}${perc}${textLen}`,
|
||||
author,
|
||||
title: `${title}${perc}${textLen}`,
|
||||
},
|
||||
descString: `${author}${title}${perc}${textLen}`,
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
key: book.key,
|
||||
});
|
||||
if (result.length >= 100)
|
||||
break;
|
||||
}
|
||||
|
||||
const search = this.search;
|
||||
@@ -243,32 +272,39 @@ class RecentBooksPage extends Vue {
|
||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||
});
|
||||
|
||||
/*for (let i = 0; i < result.length; i++) {
|
||||
if (!_.isEqual(this.tableData[i], result[i])) {
|
||||
this.$set(this.tableData, i, result[i]);
|
||||
await utils.sleep(10);
|
||||
}
|
||||
}
|
||||
if (this.tableData.length > result.length)
|
||||
this.tableData.splice(result.length);*/
|
||||
this.tableData = result;
|
||||
this.updating = false;
|
||||
}
|
||||
|
||||
headerCellStyle(cell) {
|
||||
let result = {margin: 0, padding: 0};
|
||||
if (cell.columnIndex > 0) {
|
||||
result['border-bottom'] = 0;
|
||||
wordEnding(num) {
|
||||
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
||||
const deci = num % 100;
|
||||
if (deci > 10 && deci < 20) {
|
||||
return '';
|
||||
} else {
|
||||
return endings[num % 10];
|
||||
}
|
||||
if (cell.rowIndex > 0) {
|
||||
result.height = '0px';
|
||||
result['border-right'] = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getFileNameFromPath(fb2Path) {
|
||||
return path.basename(fb2Path).substr(0, 10) + '.fb2';
|
||||
get header() {
|
||||
const len = (this.tableData ? this.tableData.length : 0);
|
||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||
}
|
||||
|
||||
async downloadBook(fb2path) {
|
||||
try {
|
||||
await readerApi.checkCachedBook(fb2path);
|
||||
|
||||
const d = this.$refs.download;
|
||||
d.href = fb2path;
|
||||
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, 'Ошибка', {type: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
openOriginal(url) {
|
||||
@@ -281,7 +317,7 @@ class RecentBooksPage extends Vue {
|
||||
|
||||
async handleDel(key) {
|
||||
await bookManager.delRecentBook({key});
|
||||
this.updateTableData();
|
||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||
|
||||
if (!bookManager.mostRecentBook())
|
||||
this.close();
|
||||
@@ -300,11 +336,11 @@ class RecentBooksPage extends Vue {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('recent-books-toggle');
|
||||
this.$emit('recent-books-close');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
@@ -314,7 +350,51 @@ class RecentBooksPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.desc {
|
||||
.recent-books-table {
|
||||
width: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
.td-mp {
|
||||
margin: 0 !important;
|
||||
padding: 4px 4px 4px 4px !important;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.no-mp {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0;
|
||||
border-left: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
line-height: 180%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.recent-books-table .q-table__middle {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.recent-books-table thead tr:first-child th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
background-color: #c1f4cd;
|
||||
}
|
||||
.recent-books-table tr:nth-child(even) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,15 +8,19 @@
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
|
||||
<div v-show="!initStep" class="input">
|
||||
<input ref="input" class="el-input__inner"
|
||||
<!--input ref="input"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/>
|
||||
:value="needle" @input="needle = $event.target.value"/-->
|
||||
<q-input ref="input" class="col" outlined dense
|
||||
placeholder="что ищем"
|
||||
v-model="needle" @keydown="inputKeyDown"
|
||||
/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
</div>
|
||||
<el-button-group v-show="!initStep" class="button-group">
|
||||
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
||||
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
||||
</el-button-group>
|
||||
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
|
||||
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
|
||||
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
@@ -39,7 +43,10 @@ export default @Component({
|
||||
|
||||
},
|
||||
foundText: function(newValue) {
|
||||
this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
|
||||
//недостатки сторонних ui
|
||||
const el = this.$refs.input.$el.querySelector('label div div div input');
|
||||
if (el)
|
||||
el.style.paddingRight = newValue.length*12 + 'px';
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -160,12 +167,13 @@ class SearchPage extends Vue {
|
||||
this.$emit('search-toggle');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
//недостатки сторонних ui
|
||||
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
|
||||
inputKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this.showNext();
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
this.close();
|
||||
}
|
||||
@@ -194,16 +202,14 @@ class SearchPage extends Vue {
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 150px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
.button {
|
||||
padding: 9px 17px 9px 17px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,11 @@ import readerApi from '../../../api/reader';
|
||||
import * as utils from '../../../share/utils';
|
||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||
|
||||
import localForage from 'localforage';
|
||||
const ssCacheStore = localForage.createInstance({
|
||||
name: 'ssCacheStore'
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
serverSyncEnabled: function() {
|
||||
@@ -44,10 +49,6 @@ class ServerStorage extends Vue {
|
||||
this.saveSettings();
|
||||
}, 500);
|
||||
|
||||
this.debouncedSaveRecent = _.debounce((itemKey) => {
|
||||
this.saveRecent(itemKey);
|
||||
}, 1000);
|
||||
|
||||
this.debouncedNotifySuccess = _.debounce(() => {
|
||||
this.success('Данные синхронизированы с сервером');
|
||||
}, 1000);
|
||||
@@ -58,37 +59,42 @@ class ServerStorage extends Vue {
|
||||
|
||||
async init() {
|
||||
try {
|
||||
this.cachedRecent = await ssCacheStore.getItem('recent');
|
||||
if (!this.cachedRecent)
|
||||
await this.setCachedRecent({rev: 0, data: {}});
|
||||
|
||||
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
||||
if (!this.cachedRecentPatch)
|
||||
await this.setCachedRecentPatch({rev: 0, data: {}});
|
||||
|
||||
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
||||
if (!this.cachedRecentMod)
|
||||
await this.setCachedRecentMod({rev: 0, data: {}});
|
||||
|
||||
if (!this.serverStorageKey) {
|
||||
//генерируем новый ключ
|
||||
await this.generateNewServerStorageKey();
|
||||
} else {
|
||||
await this.serverStorageKeyChanged();
|
||||
}
|
||||
bookManager.addEventListener(this.bookManagerEvent);
|
||||
} finally {
|
||||
this.inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
async bookManagerEvent(eventName, itemKey) {
|
||||
if (!this.serverSyncEnabled)
|
||||
return;
|
||||
async setCachedRecent(value) {
|
||||
await ssCacheStore.setItem('recent', value);
|
||||
this.cachedRecent = value;
|
||||
}
|
||||
|
||||
if (eventName == 'recent-changed') {
|
||||
if (itemKey) {
|
||||
if (!this.recentDeltaInited) {
|
||||
this.warning('Функции сохранения на сервер пока недоступны');
|
||||
return;
|
||||
}
|
||||
async setCachedRecentPatch(value) {
|
||||
await ssCacheStore.setItem('recent-patch', value);
|
||||
this.cachedRecentPatch = value;
|
||||
}
|
||||
|
||||
if (!this.recentDelta)
|
||||
this.recentDelta = {};
|
||||
|
||||
this.recentDelta[itemKey] = _.cloneDeep(bookManager.recent[itemKey]);
|
||||
|
||||
this.debouncedSaveRecent(itemKey);
|
||||
}
|
||||
}
|
||||
async setCachedRecentMod(value) {
|
||||
await ssCacheStore.setItem('recent-mod', value);
|
||||
this.cachedRecentMod = value;
|
||||
}
|
||||
|
||||
async generateNewServerStorageKey() {
|
||||
@@ -118,8 +124,8 @@ class ServerStorage extends Vue {
|
||||
await this.loadProfiles(force);
|
||||
this.checkCurrentProfile();
|
||||
await this.currentProfileChanged(force);
|
||||
await this.loadRecent();
|
||||
if (force)
|
||||
const loadSuccess = await this.loadRecent();
|
||||
if (loadSuccess && force)
|
||||
await this.saveRecent();
|
||||
}
|
||||
}
|
||||
@@ -171,17 +177,17 @@ class ServerStorage extends Vue {
|
||||
|
||||
success(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.success({message});
|
||||
this.$root.notify.success(message);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.warning({message});
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$root.notify.warning(message);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.error({message});
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$root.notify.error(message);
|
||||
}
|
||||
|
||||
async loadSettings(force = false, doNotifySuccess = true) {
|
||||
@@ -339,190 +345,187 @@ class ServerStorage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async initRecentDelta() {
|
||||
let recentDelta = null;
|
||||
try {
|
||||
recentDelta = await this.storageGet({recentDelta: {}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
|
||||
return;
|
||||
}
|
||||
|
||||
if (recentDelta.state == 'success') {
|
||||
recentDelta = recentDelta.items.recentDelta;
|
||||
|
||||
if (recentDelta.rev == 0)
|
||||
recentDelta.data = {};
|
||||
|
||||
this.recentDelta = recentDelta.data;
|
||||
this.recentDeltaInited = true;
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${recentDelta.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
async loadRecent(doNotifySuccess = true) {
|
||||
if (!this.keyInited || !this.serverSyncEnabled)
|
||||
return;
|
||||
|
||||
const oldRecentRev = bookManager.recentRev;
|
||||
const oldRecentDeltaRev = bookManager.recentDeltaRev;
|
||||
//проверим ревизию на сервере
|
||||
let revs = null;
|
||||
this.loadingRecent = true;
|
||||
try {
|
||||
revs = await this.storageCheck({recent: {}, recentDelta: {}});
|
||||
if (revs.state == 'success' && revs.items.recent.rev == oldRecentRev &&
|
||||
revs.items.recentDelta.rev == oldRecentDeltaRev) {
|
||||
if (!this.recentDeltaInited)
|
||||
await this.initRecentDelta();
|
||||
//проверим ревизию на сервере
|
||||
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;
|
||||
}
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let recent = null;
|
||||
try {
|
||||
if (revs.items.recent.rev != oldRecentRev) {
|
||||
recent = await this.storageGet({recent: {}, recentDelta: {}});
|
||||
} else {
|
||||
recent = await this.storageGet({recentDelta: {}});
|
||||
recent.items.recent = {data: _.cloneDeep(bookManager.recent), rev: oldRecentRev};
|
||||
}
|
||||
} 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 (recent.state == 'success') {
|
||||
let recentDelta = recent.items.recentDelta;
|
||||
recent = recent.items.recent;
|
||||
|
||||
if (recent.rev == 0)
|
||||
recent.data = {};
|
||||
|
||||
let newRecent = {};
|
||||
if (recentDelta && recentDelta.data) {
|
||||
if (recentDelta.data.diff) {
|
||||
newRecent = recent.data;
|
||||
const key = recentDelta.data.diff.key;
|
||||
if (newRecent[key])
|
||||
newRecent[key] = utils.applyObjDiff(newRecent[key], recentDelta.data.diff);
|
||||
} else {
|
||||
newRecent = Object.assign(recent.data, recentDelta.data);
|
||||
if (!newRecent) {
|
||||
newRecent = _.cloneDeep(this.cachedRecent);
|
||||
}
|
||||
this.recentDelta = recentDelta.data;
|
||||
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, 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 {
|
||||
newRecent = recent.data;
|
||||
this.recentDelta = {};
|
||||
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.recentDeltaInited = true;
|
||||
|
||||
if (!bookManager.loaded) {
|
||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||
while (!bookManager.loaded) await utils.sleep(100);
|
||||
}
|
||||
await bookManager.setRecent(newRecent);
|
||||
await bookManager.setRecentRev(recent.rev);
|
||||
await bookManager.setRecentDeltaRev(recentDelta.rev);
|
||||
} else {
|
||||
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
||||
if (doNotifySuccess)
|
||||
this.debouncedNotifySuccess();
|
||||
} finally {
|
||||
this.loadingRecent = false;
|
||||
}
|
||||
|
||||
if (doNotifySuccess)
|
||||
this.debouncedNotifySuccess();
|
||||
return true;
|
||||
}
|
||||
|
||||
async saveRecent(itemKey, recurse) {
|
||||
while (!this.inited || this.savingRecent)
|
||||
await utils.sleep(100);
|
||||
|
||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
||||
return;
|
||||
|
||||
const bm = bookManager;
|
||||
this.savingRecent = true;
|
||||
try {
|
||||
const bm = bookManager;
|
||||
|
||||
//вычисление критерия сохранения целиком
|
||||
if (!this.sameKeyCount)
|
||||
this.sameKeyCount = 0;
|
||||
if (this.prevItemKey == itemKey) {
|
||||
this.sameKeyCount++;
|
||||
} else {
|
||||
this.sameKeyCount = 0;
|
||||
}
|
||||
let needSaveRecent = false;
|
||||
let needSaveRecentPatch = false;
|
||||
let needSaveRecentMod = false;
|
||||
|
||||
const l = Object.keys(this.recentDelta).length - (1*(!!this.recentDelta.diff));
|
||||
this.makeDeltaDiff = (l == 1 && this.prevItemKey == itemKey ? this.makeDeltaDiff : false);
|
||||
const forceSaveRecent = l > 10 || (this.sameKeyCount > 5 && (l > 1)) || (l == 1 && this.sameKeyCount > 10 && !this.makeDeltaDiff);
|
||||
//newRecentMod
|
||||
let newRecentMod = {};
|
||||
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
|
||||
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||
newRecentMod.rev++;
|
||||
|
||||
this.sameKeyCount = (!forceSaveRecent ? this.sameKeyCount : 0);
|
||||
this.prevItemKey = itemKey;
|
||||
|
||||
//дифф от дельты для уменьшения размера передаваемых данных в частном случае
|
||||
/*if (this.makeDeltaDiff) {
|
||||
this.recentDelta.diff = utils.getObjDiff(this.prevSavedItem, bm.recent[itemKey]);
|
||||
this.recentDelta.diff.key = itemKey;
|
||||
delete this.recentDelta[itemKey];
|
||||
} else if (this.recentDelta.diff) {
|
||||
const key = this.recentDelta.diff.key;
|
||||
if (!this.prevSavedItem && bm.recent[key])
|
||||
this.prevSavedItem = _.cloneDeep(bm.recent[key]);
|
||||
if (this.prevSavedItem) {
|
||||
this.recentDelta[key] = utils.applyObjDiff(this.prevSavedItem, this.recentDelta.diff);
|
||||
newRecentMod.data.key = itemKey;
|
||||
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
|
||||
needSaveRecentMod = true;
|
||||
}
|
||||
delete this.recentDelta.diff;
|
||||
}*/
|
||||
delete this.recentDelta.diff;
|
||||
this.prevItemKey = itemKey;
|
||||
|
||||
//сохранение
|
||||
this.savingRecent = true;
|
||||
try {
|
||||
if (forceSaveRecent) {//сохраняем recent целиком
|
||||
let result = {state: ''};
|
||||
//newRecentPatch
|
||||
let newRecentPatch = {};
|
||||
if (itemKey && !needSaveRecentMod) {
|
||||
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||
newRecentPatch.rev++;
|
||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||
|
||||
try {
|
||||
result = await this.storageSet({recent: {rev: bm.recentRev + 1, data: bm.recent}, recentDelta: {rev: bm.recentDeltaRev + 1, data: {}}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
}
|
||||
let applyMod = this.cachedRecentMod.data;
|
||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true);
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadRecent(false);
|
||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||
needSaveRecentPatch = true;
|
||||
needSaveRecentMod = true;
|
||||
}
|
||||
|
||||
//newRecent
|
||||
let newRecent = {};
|
||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||
//ждем весь bm.recent
|
||||
while (!bookManager.loaded)
|
||||
await utils.sleep(100);
|
||||
|
||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||
needSaveRecent = true;
|
||||
needSaveRecentPatch = true;
|
||||
needSaveRecentMod = true;
|
||||
}
|
||||
|
||||
//query
|
||||
let query = {};
|
||||
if (needSaveRecent) {
|
||||
query = {recent: newRecent, recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||
} else if (needSaveRecentPatch) {
|
||||
query = {recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||
} else {
|
||||
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) {
|
||||
this.savingRecent = false;
|
||||
this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||
this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
this.makeDeltaDiff = true;
|
||||
this.prevSavedItem = _.cloneDeep(bm.recent[itemKey]);
|
||||
|
||||
this.recentDelta = {};
|
||||
await bm.setRecentRev(bm.recentRev + 1);
|
||||
await bm.setRecentDeltaRev(bm.recentDeltaRev + 1);
|
||||
}
|
||||
} else {//сохраняем только дифф
|
||||
let result = {state: ''};
|
||||
|
||||
try {
|
||||
result = await this.storageSet({recentDelta: {rev: bm.recentDeltaRev + 1, data: this.recentDelta}});
|
||||
} catch(e) {
|
||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||
}
|
||||
|
||||
if (result.state == 'reject') {
|
||||
await this.loadRecent(false);
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
if (!recurse) {
|
||||
this.savingRecent = false;
|
||||
this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||
this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
await bm.setRecentDeltaRev(bm.recentDeltaRev + 1);
|
||||
if (!recurse && itemKey) {
|
||||
this.savingRecent = false;
|
||||
this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
if (needSaveRecent && newRecent.rev)
|
||||
await this.setCachedRecent(newRecent);
|
||||
if (needSaveRecentPatch && newRecentPatch.rev)
|
||||
await this.setCachedRecentPatch(newRecentPatch);
|
||||
if (needSaveRecentMod && newRecentMod.rev)
|
||||
await this.setCachedRecentMod(newRecentMod);
|
||||
}
|
||||
} finally {
|
||||
this.savingRecent = false;
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
Установить позицию
|
||||
</template>
|
||||
|
||||
<div class="slider">
|
||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||
<div id="set-position-slider" class="slider q-px-md">
|
||||
<q-slider
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
v-model="sliderValue"
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
@@ -46,13 +53,6 @@ class SetPositionPage extends Vue {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
formatTooltip(val) {
|
||||
if (this.sliderMax)
|
||||
return (val/this.sliderMax*100).toFixed(2) + '%';
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('set-position-toggle');
|
||||
}
|
||||
@@ -73,9 +73,13 @@ class SetPositionPage extends Vue {
|
||||
background-color: #efefef;
|
||||
border-radius: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.el-slider {
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
<style>
|
||||
#set-position-slider .q-slider__thumb path {
|
||||
fill: white !important;
|
||||
stroke: blue !important;
|
||||
stroke-width: 2 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const defPalette = [
|
||||
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
|
||||
'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
|
||||
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
|
||||
'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
|
||||
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
|
||||
'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
|
||||
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
|
||||
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
|
||||
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
|
||||
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
|
||||
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
|
||||
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
|
||||
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
|
||||
];
|
||||
|
||||
export default defPalette;
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="part-header">Показывать кнопки панели</div>
|
||||
|
||||
<div class="item row" v-for="item in toolButtons" :key="item.name">
|
||||
<div class="label-3"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="item.text" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="part-header">Управление</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-4"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
|
||||
</div>
|
||||
</div>
|
||||
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
@@ -0,0 +1,107 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Подсказки, уведомления</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-6">Подсказка</div>
|
||||
<q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать или нет подсказку при каждой загрузке книги
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Подсказка</div>
|
||||
<q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Мерцать сообщением в строке статуса и на кнопке<br>
|
||||
обновления при загрузке книги из кэша
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления и ошибки от<br>
|
||||
синхронизатора данных с сервером
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showWhatsNewDialog">
|
||||
Показывать уведомление "Что нового"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления "Что нового"<br>
|
||||
при каждом выходе новой версии читалки
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||
Показывать "Оплатим хостинг вместе"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомление "Оплатим хостинг вместе"
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Включение этой опции позволяет делать предварительную<br>
|
||||
подготовку всего текста в ленивом режиме сразу после<br>
|
||||
загрузки книги. Это может повысить отзывчивость читалки,<br>
|
||||
но нагружает процессор каждый раз при открытии книги.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Парам. в URL</div>
|
||||
<q-checkbox size="xs" v-model="allowUrlParamBookPos">
|
||||
Добавлять параметр "__p"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавление параметра "__p" в строке браузера<br>
|
||||
позволяет передавать ссылку на книгу в читалке<br>
|
||||
без потери текущей позиции. Однако в этом случае<br>
|
||||
при листании забивается история браузера, т.к. на<br>
|
||||
каждое изменение позиции происходит смена URL.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Копирование</div>
|
||||
<q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Загружать весь текст в окно<br>
|
||||
копирования текста со страницы
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Анимация</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Тип</div>
|
||||
<q-select class="col-left" v-model="pageChangeAnimation" :options="pageChangeAnimationOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Скорость</div>
|
||||
<NumInput class="col-left" v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disable="pageChangeAnimation == ''"/>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Страница</div>
|
||||
<q-checkbox v-model="keepLastToFirst" size="xs" label="Переносить последнюю строку">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Переносить последнюю строку страницы<br>
|
||||
в начало следующей при листании
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
@@ -0,0 +1,101 @@
|
||||
<div class="part-header">Управление синхронизацией данных</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
|
||||
</div>
|
||||
|
||||
<div v-show="serverSyncEnabled">
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Профили устройств</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
|
||||
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
|
||||
</div>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1">Устройство</div>
|
||||
<div class="col">
|
||||
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
||||
style="width: 275px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</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'">
|
||||
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
|
||||
<br><div class="text-center" style="margin-top: 5px">
|
||||
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
|
||||
например, после переустановки ОС или чистки/смены браузера.<br>
|
||||
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
|
||||
и шифруются ключом доступа перед отправкой на сервер.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="item row">
|
||||
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
||||
</div>
|
||||
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
@@ -0,0 +1,34 @@
|
||||
<q-tabs
|
||||
v-model="selectedViewTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="color" label="Цвет" />
|
||||
<q-tab name="font" label="Шрифт" />
|
||||
<q-tab name="text" label="Текст" />
|
||||
<q-tab name="status" label="Строка статуса" />
|
||||
</q-tabs>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedViewTab == 'color'">
|
||||
@@include('./ViewTab/Color.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'font'">
|
||||
@@include('./ViewTab/Font.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'text'">
|
||||
@@include('./ViewTab/Text.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'status'">
|
||||
@@include('./ViewTab/Status.inc');
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Цвет</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Текст</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="textColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="textColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md"/>
|
||||
<div class="item row">
|
||||
<div class="label-2">Фон</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="bgColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="wallpaper != ''"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Шрифт</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Локальный/веб</div>
|
||||
<div class="col row">
|
||||
<q-select class="col-left" v-model="fontName" :options="fontsOptions" :disable="webFontName != ''"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="webFontName" :options="webFontsOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Веб шрифты дают большое разнообразие,<br>
|
||||
однако есть шанс, что шрифт будет загружаться<br>
|
||||
очень медленно или вовсе не загрузится
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Размер</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="fontSize" :min="5" :max="200"/>
|
||||
|
||||
<div class="col q-pt-xs text-right">
|
||||
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="vertShift" :min="-100" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сдвиг шрифта по вертикали в процентах от размера.<br>
|
||||
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||
вниз. Значение зависит от метрики шрифта.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Стиль</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="fontBold" size="xs" label="Жирный" />
|
||||
<q-checkbox class="q-ml-sm" v-model="fontItalic" size="xs" label="Курсив" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Строка статуса</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Статус</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Высота</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По клику на автора-название в строке статуса<br>
|
||||
открывать оригинал произведения в новой вкладке
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
@@ -0,0 +1,144 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Текст</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Интервал</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="lineInterval" :min="0" :max="200"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Параграф</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="p" :min="0" :max="2000"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Отступ</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Слева/справа
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сверху/снизу
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="textVertShift" :min="-100" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
|
||||
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||
вниз.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Скроллинг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="scrollingDelay" :min="1" :max="10000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Замедление скроллинга в миллисекундах.<br>
|
||||
Определяет время, за которое текст<br>
|
||||
прокручивается на одну строку.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="scrollingType" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Вид скроллинга: линейный,<br>
|
||||
ускорение-замедление и пр.
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Выравнивание</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="textAlignJustify" size="xs" label="По ширине" />
|
||||
<q-checkbox class="q-ml-sm" v-model="wordWrap" size="xs" label="Перенос по слогам" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Компактность
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="compactTextPerc" :min="0" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Степень компактности текста в процентах.<br>
|
||||
Чем больше компактность, тем хуже выравнивание<br>
|
||||
по правому краю.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Добавлять пустые
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="addEmptyParagraphs" :min="0" :max="2"/>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Изображения</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showImages" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="showInlineImagesInCenter" @input="needReload" :disable="!showImages" size="xs" label="Инлайн в центр">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Выносить все изображения в центр экрана
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Высота не более
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="imageHeightLines" :min="1" :max="100" :disable="!showImages">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Определяет высоту изображения количеством строк.<br>
|
||||
В случае превышения высоты, изображение будет<br>
|
||||
уменьшено с сохранением пропорций так, чтобы<br>
|
||||
помещаться в указанное количество строк.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
@@ -19,15 +19,17 @@
|
||||
</div>
|
||||
<div v-show="clickControl" ref="layoutEvents" class="layout events" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||
@wheel.prevent.stop="onMouseWheel"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchcancel.prevent.stop="onTouchCancel"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||
oncontextmenu="return false;">
|
||||
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
</div>
|
||||
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick">
|
||||
</div>
|
||||
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
|
||||
<canvas ref="offscreenCanvas" class="layout" style="width: 0px; height: 0px"></canvas>
|
||||
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,8 +39,8 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {loadCSS} from 'fg-loadcss';
|
||||
import _ from 'lodash';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
import {sleep} from '../../../share/utils';
|
||||
import bookManager from '../share/bookManager';
|
||||
import DrawHelper from './DrawHelper';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
@@ -130,7 +132,11 @@ class TextPage extends Vue {
|
||||
await this.doPageAnimation();
|
||||
}, 10);
|
||||
|
||||
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
||||
this.$root.$on('resize', async() => {
|
||||
this.$nextTick(this.onResize);
|
||||
await sleep(500);
|
||||
this.$nextTick(this.onResize);
|
||||
});
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@@ -143,6 +149,8 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
calcDrawProps() {
|
||||
const wideLetter = 'Щ';
|
||||
|
||||
//preloaded fonts
|
||||
this.fontList = [`12px ${this.fontName}`];
|
||||
|
||||
@@ -199,6 +207,22 @@ class TextPage extends Vue {
|
||||
this.drawHelper.lineHeight = this.lineHeight;
|
||||
this.drawHelper.context = this.context;
|
||||
|
||||
//альтернатива context.measureText
|
||||
if (!this.context.measureText(wideLetter).width) {
|
||||
const ctx = this.$refs.measureWidth;
|
||||
this.drawHelper.measureText = function(text, style) {
|
||||
ctx.innerText = text;
|
||||
ctx.style.font = this.fontByStyle(style);
|
||||
return ctx.clientWidth;
|
||||
};
|
||||
|
||||
this.drawHelper.measureTextFont = function(text, font) {
|
||||
ctx.innerText = text;
|
||||
ctx.style.font = font;
|
||||
return ctx.clientWidth;
|
||||
}
|
||||
}
|
||||
|
||||
//statusBar
|
||||
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
||||
|
||||
@@ -211,8 +235,10 @@ class TextPage extends Vue {
|
||||
this.parsed.wordWrap = this.wordWrap;
|
||||
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
|
||||
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
|
||||
let t = '';
|
||||
while (this.drawHelper.measureText(t, {}) < this.w) t += 'Щ';
|
||||
let t = wideLetter;
|
||||
if (!this.drawHelper.measureText(t, {}))
|
||||
throw new Error('Ошибка measureText');
|
||||
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
|
||||
this.parsed.maxWordLength = t.length - 1;
|
||||
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
|
||||
this.parsed.lineHeight = this.lineHeight;
|
||||
@@ -221,58 +247,47 @@ class TextPage extends Vue {
|
||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||
|
||||
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
|
||||
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
|
||||
}
|
||||
|
||||
//scrolling page
|
||||
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
|
||||
let y = pageSpace/2;
|
||||
let top = pageSpace/2;
|
||||
if (this.showStatusBar)
|
||||
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
let page1 = this.$refs.scrollBox1;
|
||||
let page2 = this.$refs.scrollBox2;
|
||||
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
let page1 = this.$refs.scrollBox1.style;
|
||||
let page2 = this.$refs.scrollBox2.style;
|
||||
|
||||
page1.style.perspective = '3072px';
|
||||
page2.style.perspective = '3072px';
|
||||
page1.perspective = page2.perspective = '3072px';
|
||||
|
||||
page1.style.width = this.w + this.indentLR + 'px';
|
||||
page2.style.width = this.w + this.indentLR + 'px';
|
||||
page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page2.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page1.style.top = y + 'px';
|
||||
page2.style.top = y + 'px';
|
||||
page1.style.left = this.indentLR + 'px';
|
||||
page2.style.left = this.indentLR + 'px';
|
||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
||||
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page1.top = page2.top = top + 'px';
|
||||
page1.left = page2.left = this.indentLR + 'px';
|
||||
|
||||
page1 = this.$refs.scrollingPage1;
|
||||
page2 = this.$refs.scrollingPage2;
|
||||
page1.style.width = this.w + this.indentLR + 'px';
|
||||
page2.style.width = this.w + this.indentLR + 'px';
|
||||
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
page1 = this.$refs.scrollingPage1.style;
|
||||
page2 = this.$refs.scrollingPage2.style;
|
||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
||||
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
}
|
||||
|
||||
async checkLoadedFonts() {
|
||||
let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
|
||||
if (loaded.some(r => !r)) {
|
||||
loaded = await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||
if (loaded.some(r => !r.length))
|
||||
throw new Error('some font not loaded');
|
||||
await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||
}
|
||||
}
|
||||
|
||||
async loadFonts() {
|
||||
this.fontsLoading = true;
|
||||
|
||||
let inst = null;
|
||||
let close = null;
|
||||
(async() => {
|
||||
await sleep(500);
|
||||
if (this.fontsLoading)
|
||||
inst = this.$notify({
|
||||
title: '',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: 'Загрузка шрифта <i class="el-icon-loading"></i>',
|
||||
duration: 0
|
||||
});
|
||||
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
||||
})();
|
||||
|
||||
if (!this.fontsLoaded)
|
||||
@@ -284,29 +299,15 @@ class TextPage extends Vue {
|
||||
this.fontsLoaded[this.fontCssUrl] = 1;
|
||||
}
|
||||
|
||||
const waitingTime = 10*1000;
|
||||
const delay = 100;
|
||||
let i = 0;
|
||||
//ждем шрифты
|
||||
while (i < waitingTime/delay) {
|
||||
i++;
|
||||
try {
|
||||
await this.checkLoadedFonts();
|
||||
i = waitingTime;
|
||||
} catch (e) {
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
if (i !== waitingTime) {
|
||||
this.$notify.error({
|
||||
title: 'Ошибка загрузки',
|
||||
message: 'Некоторые шрифты не удалось загрузить'
|
||||
});
|
||||
try {
|
||||
await this.checkLoadedFonts();
|
||||
} catch (e) {
|
||||
this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
|
||||
}
|
||||
|
||||
this.fontsLoading = false;
|
||||
if (inst)
|
||||
inst.close();
|
||||
if (close)
|
||||
close();
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
@@ -334,13 +335,19 @@ class TextPage extends Vue {
|
||||
|
||||
this.draw();
|
||||
|
||||
// шрифты хрен знает когда подгружаются в div, поэтому
|
||||
const parsed = this.parsed;
|
||||
await sleep(5000);
|
||||
if (this.parsed === parsed) {
|
||||
parsed.force = true;
|
||||
this.draw();
|
||||
parsed.force = false;
|
||||
// ширина шрифта некоторое время выдается неверно, поэтому
|
||||
if (!omitLoadFonts) {
|
||||
const parsed = this.parsed;
|
||||
|
||||
let i = 0;
|
||||
const t = this.parsed.testText;
|
||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
||||
await sleep(100);
|
||||
|
||||
if (this.parsed === parsed) {
|
||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,47 +380,51 @@ class TextPage extends Vue {
|
||||
|
||||
if (this.lastBook) {
|
||||
(async() => {
|
||||
//подождем ленивый парсинг
|
||||
this.stopLazyParse = true;
|
||||
while (this.doingLazyParse) await sleep(10);
|
||||
try {
|
||||
//подождем ленивый парсинг
|
||||
this.stopLazyParse = true;
|
||||
while (this.doingLazyParse) await sleep(10);
|
||||
|
||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||
if (!isParsed) {
|
||||
return;
|
||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||
if (!isParsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.book = await bookManager.getBook(this.lastBook);
|
||||
this.meta = bookManager.metaOnly(this.book);
|
||||
this.fb2 = this.meta.fb2;
|
||||
|
||||
let authorNames = [];
|
||||
if (this.fb2.author) {
|
||||
authorNames = this.fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
}
|
||||
|
||||
this.title = _.compact([
|
||||
authorNames.join(', '),
|
||||
this.fb2.bookTitle
|
||||
]).join(' - ');
|
||||
|
||||
this.$root.$emit('set-app-title', this.title);
|
||||
|
||||
this.parsed = this.book.parsed;
|
||||
|
||||
this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;
|
||||
await this.stopTextScrolling();
|
||||
|
||||
await this.calcPropsAndLoadFonts();
|
||||
|
||||
this.refreshTime();
|
||||
if (this.lazyParseEnabled)
|
||||
this.lazyParsePara();
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
|
||||
}
|
||||
|
||||
this.book = await bookManager.getBook(this.lastBook);
|
||||
this.meta = bookManager.metaOnly(this.book);
|
||||
this.fb2 = this.meta.fb2;
|
||||
|
||||
let authorNames = [];
|
||||
if (this.fb2.author) {
|
||||
authorNames = this.fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
}
|
||||
|
||||
this.title = _.compact([
|
||||
authorNames.join(', '),
|
||||
this.fb2.bookTitle
|
||||
]).join(' - ');
|
||||
|
||||
this.$root.$emit('set-app-title', this.title);
|
||||
|
||||
this.parsed = this.book.parsed;
|
||||
|
||||
this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;
|
||||
await this.stopTextScrolling();
|
||||
|
||||
this.calcPropsAndLoadFonts();
|
||||
|
||||
this.refreshTime();
|
||||
if (this.lazyParseEnabled)
|
||||
this.lazyParsePara();
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -437,13 +448,13 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
async onResize() {
|
||||
/*this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;*/
|
||||
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
try {
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
get settings() {
|
||||
@@ -477,7 +488,7 @@ class TextPage extends Vue {
|
||||
|
||||
generateWaitingFunc(waitingHandlerName, stopPropertyName) {
|
||||
const func = (timeout) => {
|
||||
return new Promise(async(resolve) => {
|
||||
return new Promise((resolve, reject) => { (async() => {
|
||||
this[waitingHandlerName] = resolve;
|
||||
let wait = (timeout + 201)/100;
|
||||
while (wait > 0 && !this[stopPropertyName]) {
|
||||
@@ -485,7 +496,7 @@ class TextPage extends Vue {
|
||||
await sleep(100);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
})().catch(reject); });
|
||||
};
|
||||
return func;
|
||||
}
|
||||
@@ -877,6 +888,14 @@ class TextPage extends Vue {
|
||||
this.$emit('tool-bar-toggle');
|
||||
}
|
||||
|
||||
doScrollingToggle() {
|
||||
this.$emit('scrolling-toggle');
|
||||
}
|
||||
|
||||
doFullScreenToggle() {
|
||||
this.$emit('full-screen-toogle');
|
||||
}
|
||||
|
||||
async doFontSizeInc() {
|
||||
if (!this.settingsChanging) {
|
||||
this.settingsChanging = true;
|
||||
@@ -968,7 +987,7 @@ class TextPage extends Vue {
|
||||
case 'Enter':
|
||||
case 'Backquote'://`
|
||||
case 'KeyF':
|
||||
this.$emit('full-screen-toogle');
|
||||
this.doFullScreenToggle();
|
||||
break;
|
||||
case 'Tab':
|
||||
case 'KeyQ':
|
||||
@@ -1009,22 +1028,64 @@ class TextPage extends Vue {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
if (event.touches.length == 1) {
|
||||
const touch = event.touches[0];
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const x = touch.pageX - rect.left;
|
||||
const y = touch.pageY - rect.top;
|
||||
if (this.handleClick(x, y)) {
|
||||
this.repDoing = true;
|
||||
this.debouncedStartClickRepeat(x, y);
|
||||
const hc = this.handleClick(x, y, new Set(['Menu']));
|
||||
if (hc) {
|
||||
if (hc != 'Menu') {
|
||||
this.repDoing = true;
|
||||
this.debouncedStartClickRepeat(x, y);
|
||||
} else {
|
||||
this.startTouch = {x, y};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
onTouchMove(event) {
|
||||
if (this.startTouch) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
if (event.changedTouches.length == 1) {
|
||||
const touch = event.changedTouches[0];
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const x = touch.pageX - rect.left;
|
||||
const y = touch.pageY - rect.top;
|
||||
if (this.startTouch) {
|
||||
const dy = this.startTouch.y - y;
|
||||
const dx = this.startTouch.x - x;
|
||||
const moveDelta = 30;
|
||||
const touchDelta = 15;
|
||||
if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
|
||||
//движение вверх
|
||||
this.doFullScreenToggle();
|
||||
} else if (dy < 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
|
||||
//движение вниз
|
||||
this.doScrollingToggle();
|
||||
} else if (dx > 0 && Math.abs(dx) >= moveDelta && Math.abs(dy) < Math.abs(dx)) {
|
||||
//движение влево
|
||||
this.doScrollingSpeedDown();
|
||||
} else if (dx < 0 && Math.abs(dx) >= moveDelta && Math.abs(dy) < Math.abs(dx)) {
|
||||
//движение вправо
|
||||
this.doScrollingSpeedUp();
|
||||
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
|
||||
this.doToolBarToggle();
|
||||
}
|
||||
|
||||
this.startTouch = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTouchCancel() {
|
||||
@@ -1038,12 +1099,13 @@ class TextPage extends Vue {
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
if (event.button == 0) {
|
||||
if (this.handleClick(event.offsetX, event.offsetY)) {
|
||||
const hc = this.handleClick(event.offsetX, event.offsetY);
|
||||
if (hc && hc != 'Menu') {
|
||||
this.repDoing = true;
|
||||
this.debouncedStartClickRepeat(event.offsetX, event.offsetY);
|
||||
}
|
||||
} else if (event.button == 1) {
|
||||
this.$emit('scrolling-toggle');
|
||||
this.doScrollingToggle();
|
||||
} else if (event.button == 2) {
|
||||
this.doToolBarToggle();
|
||||
}
|
||||
@@ -1070,11 +1132,11 @@ class TextPage extends Vue {
|
||||
if (url && url.indexOf('file://') != 0) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
this.$alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска', '', {type: 'warning'});
|
||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {type: 'info'});
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(pointX, pointY) {
|
||||
getClickAction(pointX, pointY) {
|
||||
const w = pointX/this.realWidth*100;
|
||||
const h = pointY/this.realHeight*100;
|
||||
|
||||
@@ -1090,27 +1152,35 @@ class TextPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'Down' ://Down
|
||||
this.doDown();
|
||||
break;
|
||||
case 'Up' ://Up
|
||||
this.doUp();
|
||||
break;
|
||||
case 'PgDown' ://PgDown
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'PgUp' ://PgUp
|
||||
this.doPageUp();
|
||||
break;
|
||||
case 'Menu' :
|
||||
this.doToolBarToggle();
|
||||
break;
|
||||
default :
|
||||
// Nothing
|
||||
return action;
|
||||
}
|
||||
|
||||
handleClick(pointX, pointY, exclude) {
|
||||
const action = this.getClickAction(pointX, pointY);
|
||||
|
||||
if (!exclude || !exclude.has(action)) {
|
||||
switch (action) {
|
||||
case 'Down' ://Down
|
||||
this.doDown();
|
||||
break;
|
||||
case 'Up' ://Up
|
||||
this.doUp();
|
||||
break;
|
||||
case 'PgDown' ://PgDown
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'PgUp' ://PgUp
|
||||
this.doPageUp();
|
||||
break;
|
||||
case 'Menu' :
|
||||
this.doToolBarToggle();
|
||||
break;
|
||||
default :
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
|
||||
return (action && action != 'Menu');
|
||||
return action;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import he from 'he';
|
||||
import sax from '../../../../server/core/BookConverter/sax';
|
||||
import sax from '../../../../server/core/sax';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
@@ -32,9 +32,6 @@ export default class BookParser {
|
||||
|
||||
//defaults
|
||||
let fb2 = {
|
||||
firstName: '',
|
||||
middleName: '',
|
||||
lastName: '',
|
||||
bookTitle: '',
|
||||
};
|
||||
|
||||
@@ -67,7 +64,7 @@ export default class BookParser {
|
||||
}
|
||||
*/
|
||||
const getImageDimensions = (binaryId, binaryType, data) => {
|
||||
return new Promise (async(resolve, reject) => {
|
||||
return new Promise ((resolve, reject) => { (async() => {
|
||||
const i = new Image();
|
||||
let resolved = false;
|
||||
i.onload = () => {
|
||||
@@ -81,19 +78,17 @@ export default class BookParser {
|
||||
resolve();
|
||||
};
|
||||
|
||||
i.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = `data:${binaryType};base64,${data}`;
|
||||
await sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
});
|
||||
})().catch(reject); });
|
||||
};
|
||||
|
||||
const getExternalImageDimensions = (src) => {
|
||||
return new Promise (async(resolve, reject) => {
|
||||
return new Promise ((resolve, reject) => { (async() => {
|
||||
const i = new Image();
|
||||
let resolved = false;
|
||||
i.onload = () => {
|
||||
@@ -105,15 +100,13 @@ export default class BookParser {
|
||||
resolve();
|
||||
};
|
||||
|
||||
i.onerror = (e) => {
|
||||
reject(e);
|
||||
};
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = src;
|
||||
await sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
});
|
||||
})().catch(reject); });
|
||||
};
|
||||
|
||||
const newParagraph = (text, len, addIndex) => {
|
||||
@@ -244,6 +237,7 @@ export default class BookParser {
|
||||
newParagraph(' ', 1);
|
||||
isFirstTitlePara = true;
|
||||
bold = true;
|
||||
center = true;
|
||||
}
|
||||
|
||||
if (tag == 'epigraph') {
|
||||
@@ -286,6 +280,7 @@ export default class BookParser {
|
||||
if (tag == 'subtitle') {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
}
|
||||
|
||||
if (tag == 'epigraph') {
|
||||
@@ -371,11 +366,10 @@ export default class BookParser {
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (center ? '</center>' : '');
|
||||
|
||||
if (path.indexOf('/fictionbook/body/title') == 0) {
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
}
|
||||
|
||||
if (path.indexOf('/fictionbook/body/section') == 0) {
|
||||
if (path.indexOf('/fictionbook/body/title') == 0 ||
|
||||
path.indexOf('/fictionbook/body/section') == 0 ||
|
||||
path.indexOf('/fictionbook/body/epigraph') == 0
|
||||
) {
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
}
|
||||
|
||||
@@ -611,6 +605,7 @@ export default class BookParser {
|
||||
|
||||
if (!this.force &&
|
||||
para.parsed &&
|
||||
para.parsed.testWidth === this.testWidth &&
|
||||
para.parsed.w === this.w &&
|
||||
para.parsed.p === this.p &&
|
||||
para.parsed.wordWrap === this.wordWrap &&
|
||||
@@ -626,6 +621,7 @@ export default class BookParser {
|
||||
return para.parsed;
|
||||
|
||||
const parsed = {
|
||||
testWidth: this.testWidth,
|
||||
w: this.w,
|
||||
p: this.p,
|
||||
wordWrap: this.wordWrap,
|
||||
|
||||
@@ -36,9 +36,6 @@ class BookManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.recentRev = await bmRecentStore.getItem('recent-rev') || 0;
|
||||
this.recentDeltaRev = await bmRecentStore.getItem('recent-delta-rev') || 0;
|
||||
|
||||
this.recentChanged = true;
|
||||
|
||||
this.loadStored();//no await
|
||||
@@ -262,6 +259,11 @@ class BookManager {
|
||||
this.books[meta.key] = result;
|
||||
}
|
||||
|
||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
||||
if (meta.path && result && meta.path != result.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && !result.parsed) {
|
||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||
callback(5);
|
||||
@@ -317,7 +319,6 @@ class BookManager {
|
||||
|
||||
metaOnly(book) {
|
||||
let result = Object.assign({}, book);
|
||||
delete result.data;//можно будет убрать эту строку со временем
|
||||
delete result.parsed;
|
||||
return result;
|
||||
}
|
||||
@@ -434,6 +435,12 @@ class BookManager {
|
||||
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];
|
||||
}
|
||||
|
||||
//"ленивое" обновление хранилища
|
||||
(async() => {
|
||||
@@ -455,19 +462,9 @@ class BookManager {
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
|
||||
async setRecentRev(value) {
|
||||
await bmRecentStore.setItem('recent-rev', value);
|
||||
this.recentRev = value;
|
||||
}
|
||||
|
||||
async setRecentDeltaRev(value) {
|
||||
await bmRecentStore.setItem('recent-delta-rev', value);
|
||||
this.recentDeltaRev = value;
|
||||
}
|
||||
|
||||
addEventListener(listener) {
|
||||
if (this.eventListeners.indexOf(listener) < 0)
|
||||
this.eventListeners.push(listener);
|
||||
this.eventListeners.push(listener);
|
||||
}
|
||||
|
||||
removeEventListener(listener) {
|
||||
|
||||
@@ -1,4 +1,164 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
showUntil: '2020-03-02',
|
||||
header: '0.9.1 (2020-03-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение работы серверной части</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-02-25',
|
||||
header: '0.9.0 (2020-02-26)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на UI-фреймфорк Quasar</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-02-05',
|
||||
header: '0.8.4 (2020-02-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен paypal-адрес для пожертвований</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-27',
|
||||
header: '0.8.3 (2020-01-28)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-19',
|
||||
header: '0.8.2 (2020-01-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-06',
|
||||
header: '0.8.1 (2020-01-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата FB3</li>
|
||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-01-05',
|
||||
header: '0.8.0 (2020-01-02)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>окончательный переход на https</li>
|
||||
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-11-26',
|
||||
header: '0.7.9 (2019-11-27)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-11-24',
|
||||
header: '0.7.8 (2019-11-25)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение html-фильтров для сайтов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-11-10',
|
||||
header: '0.7.7 (2019-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлены следующие жесты для тачскрина (только при включенной опции "управление кликом"):</li>
|
||||
<ul>
|
||||
<li style="list-style-type: square">от центра вверх: на весь экран</li>
|
||||
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
|
||||
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-29',
|
||||
header: '0.7.6 (2019-10-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-21',
|
||||
header: '0.7.5 (2019-10-22)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-10-17',
|
||||
header: '0.7.3 (2019-10-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние переделки механизма синхронизации с сервером</li>
|
||||
<li>добавлен html-фильтр для сайтов www.fanfiction.net, archiveofourown.org</li>
|
||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2019-09-19',
|
||||
header: '0.7.1 (2019-09-20)',
|
||||
@@ -6,6 +166,8 @@ export const versionHistory = [
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||
<li>актуализирована справка</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Settings в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Sources в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
64
client/components/share/Dialog.vue
Normal file
64
client/components/share/Dialog.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<q-dialog v-model="active">
|
||||
<div class="column bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col q-mx-md">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="row justify-end q-pa-md">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
const DialogProps = Vue.extend({
|
||||
props: {
|
||||
value: Boolean,
|
||||
}
|
||||
})
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Dialog extends DialogProps {
|
||||
get active() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set active(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
58
client/components/share/Notify.vue
Normal file
58
client/components/share/Notify.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Notify extends Vue {
|
||||
notify(opts) {
|
||||
let {
|
||||
caption = null,
|
||||
captionColor = 'black',
|
||||
color = 'positive',
|
||||
icon = '',
|
||||
iconColor = 'white',
|
||||
message = '',
|
||||
messageColor = 'black',
|
||||
} = opts;
|
||||
|
||||
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
|
||||
return this.$q.notify({
|
||||
position: 'top-right',
|
||||
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) {
|
||||
this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
|
||||
}
|
||||
|
||||
warning(message, caption) {
|
||||
this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
|
||||
}
|
||||
|
||||
error(message, caption) {
|
||||
this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
|
||||
}
|
||||
|
||||
info(message, caption) {
|
||||
this.notify({color: 'info', icon: 'la la-bell', message, caption});
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
185
client/components/share/NumInput.vue
Normal file
185
client/components/share/NumInput.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<q-input outlined dense
|
||||
v-model="filteredValue"
|
||||
input-style="text-align: center"
|
||||
class="no-mp"
|
||||
:class="(error ? 'error' : '')"
|
||||
:disable="disable"
|
||||
>
|
||||
<slot></slot>
|
||||
<template v-slot:prepend>
|
||||
<q-icon :class="(validate(value - step) ? '' : 'disable')"
|
||||
name="la la-minus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value - step)"
|
||||
@click="minus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@mouseout.prevent.stop="onMouseUp"
|
||||
@touchstart.stop="onTouchStart($event, 'minus')"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon :class="(validate(value + step) ? '' : 'disable')"
|
||||
name="la la-plus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value + step)"
|
||||
@click="plus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@mouseout.prevent.stop="onMouseUp"
|
||||
@touchstart.stop="onTouchStart($event, 'plus')"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const NumInputProps = Vue.extend({
|
||||
props: {
|
||||
value: Number,
|
||||
min: { type: Number, default: -Number.MAX_VALUE },
|
||||
max: { type: Number, default: Number.MAX_VALUE },
|
||||
step: { type: Number, default: 1 },
|
||||
digits: { type: Number, default: 0 },
|
||||
disable: Boolean
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
filteredValue: function(newValue) {
|
||||
if (this.validate(newValue)) {
|
||||
this.error = false;
|
||||
this.$emit('input', this.string2number(newValue));
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
value: function(newValue) {
|
||||
this.filteredValue = newValue;
|
||||
},
|
||||
}
|
||||
})
|
||||
class NumInput extends NumInputProps {
|
||||
filteredValue = 0;
|
||||
error = false;
|
||||
|
||||
created() {
|
||||
this.filteredValue = this.value;
|
||||
}
|
||||
|
||||
string2number(value) {
|
||||
return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
let n = this.string2number(value);
|
||||
if (isNaN(n))
|
||||
return false;
|
||||
if (n < this.min)
|
||||
return false;
|
||||
if (n > this.max)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
plus() {
|
||||
const newValue = this.value + this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
minus() {
|
||||
const newValue = this.value - this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
onMouseDown(event, way) {
|
||||
this.startClickRepeat = true;
|
||||
this.clickRepeat = false;
|
||||
|
||||
if (event.button == 0) {
|
||||
(async() => {
|
||||
await utils.sleep(300);
|
||||
if (this.startClickRepeat) {
|
||||
this.clickRepeat = true;
|
||||
while (this.clickRepeat) {
|
||||
if (way == 'plus') {
|
||||
this.plus();
|
||||
} else {
|
||||
this.minus();
|
||||
}
|
||||
await utils.sleep(50);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.inTouch)
|
||||
return;
|
||||
this.startClickRepeat = false;
|
||||
this.clickRepeat = false;
|
||||
}
|
||||
|
||||
onTouchStart(event, way) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
this.inTouch = true;
|
||||
this.onMouseDown({button: 0}, way);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.inTouch = false;
|
||||
this.onMouseUp();
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-mp {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 130%;
|
||||
border-radius: 20px;
|
||||
color: #bbb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: #616161;
|
||||
background-color: #efebe9;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #ffabab;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.disable, .disable:hover {
|
||||
cursor: not-allowed;
|
||||
color: #bbb;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
255
client/components/share/StdDialog.vue
Normal file
255
client/components/share/StdDialog.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
|
||||
<slot></slot>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'alert'" class="dialog column bg-white no-wrap" style="min-height: 150px">
|
||||
<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="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col 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="dialog column bg-white no-wrap" style="min-height: 150px">
|
||||
<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="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'prompt'" class="dialog column bg-white no-wrap" style="min-height: 250px">
|
||||
<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="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
|
||||
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
//import * as utils from '../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
inputValue: function(newValue) {
|
||||
this.validate(newValue);
|
||||
},
|
||||
}
|
||||
})
|
||||
class StdDialog extends Vue {
|
||||
caption = '';
|
||||
message = '';
|
||||
active = false;
|
||||
type = '';
|
||||
inputValue = '';
|
||||
error = '';
|
||||
iconColor = '';
|
||||
|
||||
created() {
|
||||
if (this.$root.addKeyHook) {
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
}
|
||||
}
|
||||
|
||||
init(message, caption, opts) {
|
||||
this.caption = caption;
|
||||
this.message = message;
|
||||
|
||||
this.ok = false;
|
||||
this.type = '';
|
||||
this.inputValidator = null;
|
||||
this.inputValue = '';
|
||||
this.error = '';
|
||||
|
||||
this.iconColor = 'text-warning';
|
||||
if (opts && opts.type) {
|
||||
this.iconColor = `text-${opts.type}`;
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
if (this.hideTrigger) {
|
||||
this.hideTrigger();
|
||||
this.hideTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
onShow() {
|
||||
if (this.type == 'prompt') {
|
||||
this.enableValidator = true;
|
||||
if (this.inputValue)
|
||||
this.validate(this.inputValue);
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
this.ok = true;
|
||||
this.$refs.dialog.hide();
|
||||
}
|
||||
|
||||
alert(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'alert';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
confirm(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'confirm';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
prompt(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.enableValidator = false;
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve({value: this.inputValue});
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'prompt';
|
||||
if (opts) {
|
||||
this.inputValidator = opts.inputValidator || null;
|
||||
this.inputValue = opts.inputValue || '';
|
||||
}
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.active && event.code == 'Enter') {
|
||||
this.okClick();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.error {
|
||||
height: 20px;
|
||||
font-size: 80%;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="windowBox" @click.stop>
|
||||
<div class="window">
|
||||
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
|
||||
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
|
||||
<div class="window flexfit column no-wrap">
|
||||
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
||||
<span class="header-text"><slot name="header"></slot></span>
|
||||
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
|
||||
<span class="header-text col"><slot name="header"></slot></span>
|
||||
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,23 +117,20 @@ class Window extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent !important;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.windowBox {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
.xyfit {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.window {
|
||||
.flexfit {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.window {
|
||||
margin: 10px;
|
||||
background-color: #ffffff;
|
||||
border: 3px double black;
|
||||
@@ -141,23 +139,21 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #59B04F;
|
||||
background: linear-gradient(to bottom right, green, #59B04F);
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: yellow;
|
||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
@@ -166,4 +162,5 @@ class Window extends Vue {
|
||||
.close-button:hover {
|
||||
background-color: #69C05F;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,128 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
/*
|
||||
import ElementUI from 'element-ui';
|
||||
import './theme/index.css';
|
||||
import locale from 'element-ui/lib/locale/lang/ru-RU';
|
||||
|
||||
Vue.use(ElementUI, { locale });
|
||||
*/
|
||||
|
||||
//------------------------------------------------------
|
||||
//import './theme/index.css';
|
||||
|
||||
import './theme/icon.css';
|
||||
import './theme/tooltip.css';
|
||||
|
||||
import ElMenu from 'element-ui/lib/menu';
|
||||
import './theme/menu.css';
|
||||
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
import './theme/menu-item.css';
|
||||
|
||||
import ElButton from 'element-ui/lib/button';
|
||||
import './theme/button.css';
|
||||
|
||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
||||
import './theme/button-group.css';
|
||||
|
||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
||||
import './theme/checkbox.css';
|
||||
|
||||
import ElTabs from 'element-ui/lib/tabs';
|
||||
import './theme/tabs.css';
|
||||
|
||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
||||
import './theme/tab-pane.css';
|
||||
|
||||
import ElTooltip from 'element-ui/lib/tooltip';
|
||||
import './theme/tooltip.css';
|
||||
|
||||
import ElCol from 'element-ui/lib/col';
|
||||
import './theme/col.css';
|
||||
|
||||
import ElContainer from 'element-ui/lib/container';
|
||||
import './theme/container.css';
|
||||
|
||||
import ElAside from 'element-ui/lib/aside';
|
||||
import './theme/aside.css';
|
||||
|
||||
import ElHeader from 'element-ui/lib/header';
|
||||
import './theme/header.css';
|
||||
|
||||
import ElMain from 'element-ui/lib/main';
|
||||
import './theme/main.css';
|
||||
|
||||
import ElInput from 'element-ui/lib/input';
|
||||
import './theme/input.css';
|
||||
|
||||
import ElInputNumber from 'element-ui/lib/input-number';
|
||||
import './theme/input-number.css';
|
||||
|
||||
import ElSelect from 'element-ui/lib/select';
|
||||
import './theme/select.css';
|
||||
|
||||
import ElOption from 'element-ui/lib/option';
|
||||
import './theme/option.css';
|
||||
|
||||
import ElTable from 'element-ui/lib/table';
|
||||
import './theme/table.css';
|
||||
|
||||
import ElTableColumn from 'element-ui/lib/table-column';
|
||||
import './theme/table-column.css';
|
||||
|
||||
import ElProgress from 'element-ui/lib/progress';
|
||||
import './theme/progress.css';
|
||||
|
||||
import ElSlider from 'element-ui/lib/slider';
|
||||
import './theme/slider.css';
|
||||
|
||||
import ElForm from 'element-ui/lib/form';
|
||||
import './theme/form.css';
|
||||
|
||||
import ElFormItem from 'element-ui/lib/form-item';
|
||||
import './theme/form-item.css';
|
||||
|
||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
||||
import './theme/color-picker.css';
|
||||
|
||||
import ElDialog from 'element-ui/lib/dialog';
|
||||
import './theme/dialog.css';
|
||||
|
||||
import Notification from 'element-ui/lib/notification';
|
||||
import './theme/notification.css';
|
||||
|
||||
import Loading from 'element-ui/lib/loading';
|
||||
import './theme/loading.css';
|
||||
|
||||
import MessageBox from 'element-ui/lib/message-box';
|
||||
import './theme/message-box.css';
|
||||
|
||||
//import Message from 'element-ui/lib/message';
|
||||
//import './theme/message.css';
|
||||
|
||||
const components = {
|
||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
||||
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
||||
ElColorPicker, ElDialog,
|
||||
};
|
||||
|
||||
for (let name in components) {
|
||||
Vue.component(name, components[name]);
|
||||
}
|
||||
|
||||
//Vue.use(Loading.directive);
|
||||
|
||||
Vue.prototype.$loading = Loading.service;
|
||||
Vue.prototype.$msgbox = MessageBox;
|
||||
Vue.prototype.$alert = MessageBox.alert;
|
||||
Vue.prototype.$confirm = MessageBox.confirm;
|
||||
Vue.prototype.$prompt = MessageBox.prompt;
|
||||
Vue.prototype.$notify = Notification;
|
||||
//Vue.prototype.$message = Message;
|
||||
|
||||
import lang from 'element-ui/lib/locale/lang/ru-RU';
|
||||
import locale from 'element-ui/lib/locale';
|
||||
locale.use(lang);
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="браузерная онлайн-читалка книг из интернета и библиотека">
|
||||
<meta name="keywords" content="библиотека,онлайн,читалка,книги,читать,браузер,интернет">
|
||||
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
||||
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Vue from 'vue';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './element';
|
||||
import './quasar';
|
||||
|
||||
import App from './components/App.vue';
|
||||
//Vue.config.productionTip = false;
|
||||
|
||||
87
client/quasar.js
Normal file
87
client/quasar.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import 'quasar/dist/quasar.css';
|
||||
import Quasar from 'quasar/src/vue-plugin.js'
|
||||
|
||||
//config
|
||||
const config = {};
|
||||
|
||||
//components
|
||||
//import {QLayout} from 'quasar/src/components/layout';
|
||||
//import {QPageContainer, QPage} from 'quasar/src/components/page';
|
||||
//import {QDrawer} from 'quasar/src/components/drawer';
|
||||
|
||||
import {QCircularProgress} from 'quasar/src/components/circular-progress';
|
||||
import {QInput} from 'quasar/src/components/input';
|
||||
import {QBtn} from 'quasar/src/components/btn';
|
||||
import {QBtnGroup} from 'quasar/src/components/btn-group';
|
||||
import {QBtnToggle} from 'quasar/src/components/btn-toggle';
|
||||
import {QIcon} from 'quasar/src/components/icon';
|
||||
import {QSlider} from 'quasar/src/components/slider';
|
||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||
import {QSeparator} from 'quasar/src/components/separator';
|
||||
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||
import {QSpinner} from 'quasar/src/components/spinner';
|
||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||
import {QCheckbox} from 'quasar/src/components/checkbox';
|
||||
import {QSelect} from 'quasar/src/components/select';
|
||||
import {QColor} from 'quasar/src/components/color';
|
||||
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||
import {QDialog} from 'quasar/src/components/dialog';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
//directives
|
||||
import Ripple from 'quasar/src/directives/Ripple';
|
||||
import ClosePopup from 'quasar/src/directives/ClosePopup';
|
||||
|
||||
const directives = {Ripple, ClosePopup};
|
||||
|
||||
//plugins
|
||||
import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
|
||||
import Notify from 'quasar/src/plugins/Notify';
|
||||
|
||||
const plugins = {
|
||||
AppFullscreen,
|
||||
Notify,
|
||||
};
|
||||
|
||||
//use
|
||||
Vue.use(Quasar, { config, components, directives, plugins });
|
||||
|
||||
//icons
|
||||
//import '@quasar/extras/material-icons/material-icons.css';
|
||||
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
|
||||
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
|
||||
|
||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||
|
||||
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||
Quasar.iconSet.set(lineAwesome);
|
||||
@@ -193,4 +193,13 @@ export function parseQuery(str) {
|
||||
query[first] = [query[first], second];
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export function escapeXml(str) {
|
||||
return str.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const toolButtons = [
|
||||
{name: 'search', show: true, text: 'Найти в тексте'},
|
||||
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
||||
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
||||
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
|
||||
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
||||
];
|
||||
|
||||
@@ -159,11 +160,12 @@ const settingDefaults = {
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAlpha: 0.4,
|
||||
statusBarClickOpen: true,
|
||||
|
||||
scrollingDelay: 3000,// замедление, ms
|
||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||
|
||||
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
|
||||
allowUrlParamBookPos: false,
|
||||
@@ -181,7 +183,8 @@ const settingDefaults = {
|
||||
imageFitWidth: true,
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
showMigrationDialog: true,
|
||||
showDonationDialog2020: true,
|
||||
enableSitesFilter: true,
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
@@ -203,7 +206,7 @@ const state = {
|
||||
profilesRev: 0,
|
||||
allowProfilesSave: false,//подстраховка для разработки
|
||||
whatsNewContentHash: '',
|
||||
migrationRemindDate: '',
|
||||
donationRemindDate: '',
|
||||
currentProfile: '',
|
||||
settings: Object.assign({}, settingDefaults),
|
||||
settingsRev: {},
|
||||
@@ -238,8 +241,8 @@ const mutations = {
|
||||
setWhatsNewContentHash(state, value) {
|
||||
state.whatsNewContentHash = value;
|
||||
},
|
||||
setMigrationRemindDate(state, value) {
|
||||
state.migrationRemindDate = value;
|
||||
setDonationRemindDate(state, value) {
|
||||
state.donationRemindDate = value;
|
||||
},
|
||||
setCurrentProfile(state, value) {
|
||||
state.currentProfile = value;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success{background-color:#f0f9eb;color:#67c23a}.el-alert--success .el-alert__description{color:#67c23a}.el-alert--info{background-color:#f4f4f5;color:#909399}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning .el-alert__description{color:#e6a23c}.el-alert--error{background-color:#fef0f0;color:#f56c6c}.el-alert--error .el-alert__description{color:#f56c6c}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;color:#c0c4cc;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-alert-fade-enter,.el-alert-fade-leave-active{opacity:0}
|
||||
@@ -1 +0,0 @@
|
||||
.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#f56c6c;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#00468F}.el-badge__content--success{background-color:#67c23a}.el-badge__content--warning{background-color:#e6a23c}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#f56c6c}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#00468F;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-card{border-radius:4px;border:1px solid #ebeef5;background-color:#fff;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__body{padding:20px}
|
||||
@@ -1 +0,0 @@
|
||||
.el-carousel__item,.el-carousel__mask{position:absolute;height:100%;top:0;left:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#fff;opacity:.24;-webkit-transition:.2s;transition:.2s}
|
||||
@@ -1 +0,0 @@
|
||||
.el-carousel{overflow-x:hidden;position:relative}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0;padding:0;z-index:2}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#c0c4cc;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{display:inline-block;background-color:transparent;padding:12px 4px;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.el-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:0}.el-container.is-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-dialog{position:relative;margin:0 auto 50px;background:#fff;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#00468F}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px}.el-dialog__footer{padding:10px 20px 20px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}
|
||||
@@ -1 +0,0 @@
|
||||
@media only screen and (max-width:767px){.hidden-xs-only{display:none!important}}@media only screen and (min-width:768px){.hidden-sm-and-up{display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.hidden-sm-only{display:none!important}}@media only screen and (max-width:991px){.hidden-sm-and-down{display:none!important}}@media only screen and (min-width:992px){.hidden-md-and-up{display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.hidden-md-only{display:none!important}}@media only screen and (max-width:1199px){.hidden-md-and-down{display:none!important}}@media only screen and (min-width:1200px){.hidden-lg-and-up{display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.hidden-lg-only{display:none!important}}@media only screen and (max-width:1919px){.hidden-lg-and-down{display:none!important}}@media only screen and (min-width:1920px){.hidden-xl-only{display:none!important}}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
.el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
||||
@@ -1 +0,0 @@
|
||||
.el-form--inline .el-form-item,.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form-item::after,.el-form-item__content::after{clear:both}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{margin-right:10px}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form-item__content .el-input-group,.el-form-item__label{vertical-align:middle}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label{text-align:right;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__error{color:#f56c6c;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:'*';color:#f56c6c;margin-right:4px}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus{border-color:#f56c6c}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#f56c6c}.el-form-item.is-success .el-input__inner,.el-form-item.is-success .el-input__inner:focus,.el-form-item.is-success .el-textarea__inner,.el-form-item.is-success .el-textarea__inner:focus{border-color:#67c23a}.el-form-item.is-success .el-input-group__append .el-input__inner,.el-form-item.is-success .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-success .el-input__validateIcon{color:#67c23a}.el-form-item--feedback .el-input__validateIcon{display:inline-block}
|
||||
@@ -1 +0,0 @@
|
||||
.el-header{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
||||
@@ -1 +0,0 @@
|
||||
@font-face{font-family:element-icons;src:url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");font-weight:400;font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-info:before{content:"\e61a"}.el-icon-error:before{content:"\e62c"}.el-icon-success:before{content:"\e62d"}.el-icon-warning:before{content:"\e62e"}.el-icon-question:before{content:"\e634"}.el-icon-back:before{content:"\e606"}.el-icon-arrow-left:before{content:"\e600"}.el-icon-arrow-down:before{content:"\e603"}.el-icon-arrow-right:before{content:"\e604"}.el-icon-arrow-up:before{content:"\e605"}.el-icon-caret-left:before{content:"\e60a"}.el-icon-caret-bottom:before{content:"\e60b"}.el-icon-caret-top:before{content:"\e60c"}.el-icon-caret-right:before{content:"\e60e"}.el-icon-d-arrow-left:before{content:"\e610"}.el-icon-d-arrow-right:before{content:"\e613"}.el-icon-minus:before{content:"\e621"}.el-icon-plus:before{content:"\e62b"}.el-icon-remove:before{content:"\e635"}.el-icon-circle-plus:before{content:"\e601"}.el-icon-remove-outline:before{content:"\e63c"}.el-icon-circle-plus-outline:before{content:"\e602"}.el-icon-close:before{content:"\e60f"}.el-icon-check:before{content:"\e611"}.el-icon-circle-close:before{content:"\e607"}.el-icon-circle-check:before{content:"\e639"}.el-icon-circle-close-outline:before{content:"\e609"}.el-icon-circle-check-outline:before{content:"\e63e"}.el-icon-zoom-out:before{content:"\e645"}.el-icon-zoom-in:before{content:"\e641"}.el-icon-d-caret:before{content:"\e615"}.el-icon-sort:before{content:"\e640"}.el-icon-sort-down:before{content:"\e630"}.el-icon-sort-up:before{content:"\e631"}.el-icon-tickets:before{content:"\e63f"}.el-icon-document:before{content:"\e614"}.el-icon-goods:before{content:"\e618"}.el-icon-sold-out:before{content:"\e63b"}.el-icon-news:before{content:"\e625"}.el-icon-message:before{content:"\e61b"}.el-icon-date:before{content:"\e608"}.el-icon-printer:before{content:"\e62f"}.el-icon-time:before{content:"\e642"}.el-icon-bell:before{content:"\e622"}.el-icon-mobile-phone:before{content:"\e624"}.el-icon-service:before{content:"\e63a"}.el-icon-view:before{content:"\e643"}.el-icon-menu:before{content:"\e620"}.el-icon-more:before{content:"\e646"}.el-icon-more-outline:before{content:"\e626"}.el-icon-star-on:before{content:"\e637"}.el-icon-star-off:before{content:"\e63d"}.el-icon-location:before{content:"\e61d"}.el-icon-location-outline:before{content:"\e61f"}.el-icon-phone:before{content:"\e627"}.el-icon-phone-outline:before{content:"\e628"}.el-icon-picture:before{content:"\e629"}.el-icon-picture-outline:before{content:"\e62a"}.el-icon-delete:before{content:"\e612"}.el-icon-search:before{content:"\e619"}.el-icon-edit:before{content:"\e61c"}.el-icon-edit-outline:before{content:"\e616"}.el-icon-rank:before{content:"\e632"}.el-icon-refresh:before{content:"\e633"}.el-icon-share:before{content:"\e636"}.el-icon-setting:before{content:"\e638"}.el-icon-upload:before{content:"\e60d"}.el-icon-upload2:before{content:"\e644"}.el-icon-download:before{content:"\e617"}.el-icon-loading:before{content:"\e61e"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}@keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user