Compare commits
404 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cf5a0f4c8 | ||
|
|
8f0d526af2 | ||
|
|
6ca3881841 | ||
|
|
d8e765a04f | ||
|
|
40ff572f94 | ||
|
|
cc4275dc03 | ||
|
|
b387f4a0db | ||
|
|
1a096031c4 | ||
|
|
83a60b4091 | ||
|
|
b292407ec2 | ||
|
|
952c337b76 | ||
|
|
e947b887fe | ||
|
|
bd1e5485d7 | ||
|
|
e095c3318b | ||
|
|
d75a08b519 | ||
|
|
d55a616fe0 | ||
|
|
2146cb3576 | ||
|
|
ae260e74f6 | ||
|
|
355410c03c | ||
|
|
718ad51fac | ||
|
|
4242a8679f | ||
|
|
4ff9ff699b | ||
|
|
7a76673274 | ||
|
|
bd03ca5136 | ||
|
|
b3e1e4b909 | ||
|
|
4bb22183df | ||
|
|
e72f8f4245 | ||
|
|
5b52f48bce | ||
|
|
f07a157a2a | ||
|
|
ca40854106 | ||
|
|
d6a8209b31 | ||
|
|
731e1f1f15 | ||
|
|
b4a2a8fb98 | ||
|
|
5a3e4ee5ca | ||
|
|
ab2cf0aeec | ||
|
|
9de6a02b30 | ||
|
|
9fb7892bfe | ||
|
|
546f4556f6 | ||
|
|
471de104bc | ||
|
|
d30be1536d | ||
|
|
6c0678ed61 | ||
|
|
4883b8a190 | ||
|
|
14742ed4ad | ||
|
|
1d8bd56862 | ||
|
|
94b8f9fe1c | ||
|
|
27412211a5 | ||
|
|
f8c4960079 | ||
|
|
b2e0bcf995 | ||
|
|
fcf6639d38 | ||
|
|
d540cb91a9 | ||
|
|
f69cc6f1b1 | ||
|
|
607f2ff407 | ||
|
|
ba6bf8c091 | ||
|
|
7e4c938dfd | ||
|
|
7f36d55320 | ||
|
|
d9634a134c | ||
|
|
4f8868d4b1 | ||
|
|
956546585c | ||
|
|
3ca0a92442 | ||
|
|
213f7e48c9 | ||
|
|
8b66fd522d | ||
|
|
fdf5009999 | ||
|
|
bbdba0ef16 | ||
|
|
a63602df7a | ||
|
|
587120f984 | ||
|
|
e72ca0de7e | ||
|
|
c44c27d3d2 | ||
|
|
df4e201ccd | ||
|
|
c8c0e9ec1a | ||
|
|
9a4a84a367 | ||
|
|
1dc3424411 | ||
|
|
c13745e913 | ||
|
|
25c12309f2 | ||
|
|
4b632da5af | ||
|
|
87c364b8ee | ||
|
|
efa48fbc8a | ||
|
|
21df6c1d21 | ||
|
|
39d2ceb94b | ||
|
|
1dad013d60 | ||
|
|
add7a03f88 | ||
|
|
0cefaa6d48 | ||
|
|
f08e73f359 | ||
|
|
cf9ce26438 | ||
|
|
fd74a5a82e | ||
|
|
3109104928 | ||
|
|
b1ec4df2e4 | ||
|
|
1609e149a8 | ||
|
|
298c483d0e | ||
|
|
dc917b75b1 | ||
|
|
ad32bdab44 | ||
|
|
5e8b2e1c87 | ||
|
|
08b4afd287 | ||
|
|
0de31f643b | ||
|
|
5e815eb3c4 | ||
|
|
48c93a2120 | ||
|
|
ad5de42172 | ||
|
|
32bafedaad | ||
|
|
c67fd11be9 | ||
|
|
8b59c72848 | ||
|
|
c35b2f3bfc | ||
|
|
ac63ad4612 | ||
|
|
e1dea1c752 | ||
|
|
25648e2327 | ||
|
|
18ac04bb0f | ||
|
|
5263ee58b2 | ||
|
|
af542b89f7 | ||
|
|
684b675fca | ||
|
|
c29044eca1 | ||
|
|
a36510fcc8 | ||
|
|
bc21ace416 | ||
|
|
57e521e2ff | ||
|
|
ac6ebb9e8d | ||
|
|
54bef54635 | ||
|
|
593e201f79 | ||
|
|
d7b24253fe | ||
|
|
33961abd86 | ||
|
|
37e0e1d42f | ||
|
|
1121f9c918 | ||
|
|
582203f5da | ||
|
|
8c0f193738 | ||
|
|
ebe42956ad | ||
|
|
b8f8df8927 | ||
|
|
2c66ca4fdd | ||
|
|
49f813e880 | ||
|
|
da6fed80d1 | ||
|
|
b901d9b8c9 | ||
|
|
b41d46ac57 | ||
|
|
4f0189f3e0 | ||
|
|
c956e7a802 | ||
|
|
dcbc8409e0 | ||
|
|
fd58568cf0 | ||
|
|
0f81fa53d2 | ||
|
|
44655dc81c | ||
|
|
749667aefd | ||
|
|
dd94418c26 | ||
|
|
55a5375e46 | ||
|
|
df76de7352 | ||
|
|
1fb1a1b2b1 | ||
|
|
f998edb2aa | ||
|
|
7c2cb9a0c7 | ||
|
|
0690a365da | ||
|
|
a20d05aba8 | ||
|
|
4362ae95ba | ||
|
|
d658814399 | ||
|
|
39e14d70ee | ||
|
|
2e58cfdb75 | ||
|
|
fcaa724c00 | ||
|
|
8806b4141e | ||
|
|
7bd159766b | ||
|
|
4df15d603f | ||
|
|
b453c3efe5 | ||
|
|
56590ef8a4 | ||
|
|
7c133136b9 | ||
|
|
41881639aa | ||
|
|
416003f078 | ||
|
|
bbfcd0efa3 | ||
|
|
150e4332c3 | ||
|
|
49649765c7 | ||
|
|
726b7bfa93 | ||
|
|
265f838868 | ||
|
|
6e2e5b5520 | ||
|
|
100ea2f64a | ||
|
|
4e7ed1ee33 | ||
|
|
8ab6aed1aa | ||
|
|
4ff096014c | ||
|
|
03b60b6ca9 | ||
|
|
e30b832e05 | ||
|
|
e646de85a7 | ||
|
|
70a7a0e344 | ||
|
|
b444abeb3e | ||
|
|
c72f56917d | ||
|
|
192283d6b2 | ||
|
|
6be6fa1966 | ||
|
|
510553b055 | ||
|
|
6c4616892e | ||
|
|
1e79a099b8 | ||
|
|
31a22327f1 | ||
|
|
c1712bebc6 | ||
|
|
cd91541245 | ||
|
|
4c1fc83256 | ||
|
|
34c7a33576 | ||
|
|
23ecfeeb4f | ||
|
|
9703f83eb3 | ||
|
|
0f3cc03d00 | ||
|
|
6f7ba1f9fc | ||
|
|
e1b85e4a1b | ||
|
|
b308dd58cc | ||
|
|
9f4c0479ce | ||
|
|
2c57817dde | ||
|
|
ba85c54d7c | ||
|
|
a80e5c3a65 | ||
|
|
22e2c34da8 | ||
|
|
00a8e4c2c5 | ||
|
|
10d0a4079c | ||
|
|
589f7f3c22 | ||
|
|
d1126a7eb0 | ||
|
|
9f4e72a0e1 | ||
|
|
a024295379 | ||
|
|
dc2b2ec488 | ||
|
|
0c5f5975aa | ||
|
|
dc3f682d2d | ||
|
|
2db8876c66 | ||
|
|
8f6201b0f7 | ||
|
|
4b146c70ad | ||
|
|
0118034b4b | ||
|
|
39217053ca | ||
|
|
fba190c826 | ||
|
|
5e9d528e16 | ||
|
|
c5921d88fc | ||
|
|
eb980b0ea1 | ||
|
|
de5b4216f7 | ||
|
|
495ff57b19 | ||
|
|
57948cf6e3 | ||
|
|
1aebbbcabd | ||
|
|
25b4cb072d | ||
|
|
1cdacc3a08 | ||
|
|
34d9466d09 | ||
|
|
c182c4ce66 | ||
|
|
dbb9bd1282 | ||
|
|
8019d2d6cc | ||
|
|
459cdb2e0b | ||
|
|
a230cd9513 | ||
|
|
0c44a25e85 | ||
|
|
34f3d04370 | ||
|
|
1f3e6b7e16 | ||
|
|
47d49a200a | ||
|
|
e1767d6e52 | ||
|
|
0f8e343cd2 | ||
|
|
23ab487baf | ||
|
|
22e5d38ef5 | ||
|
|
5819ccb528 | ||
|
|
42a2fd77cf | ||
|
|
ab93a8b0b3 | ||
|
|
84437eafa6 | ||
|
|
0107d848e0 | ||
|
|
5eeac96a0d | ||
|
|
9351c115be | ||
|
|
f95a11096c | ||
|
|
4203d179e6 | ||
|
|
78dfc9cb1c | ||
|
|
0bef307d77 | ||
|
|
b0da806f7a | ||
|
|
badecd1d81 | ||
|
|
6418e8ee30 | ||
|
|
09115c9658 | ||
|
|
74e3866bd7 | ||
|
|
408de78c13 | ||
|
|
c0451c18b3 | ||
|
|
f303d26c1e | ||
|
|
1b58a34859 | ||
|
|
82ea416e67 | ||
|
|
efd4fbad70 | ||
|
|
01bd15121b | ||
|
|
a9c2495349 | ||
|
|
e7c50b50ed | ||
|
|
6e25b289d2 | ||
|
|
157267eaf7 | ||
|
|
a317f9137a | ||
|
|
5dad3d22ea | ||
|
|
be85df456b | ||
|
|
2e172a08c7 | ||
|
|
bb1069ca60 | ||
|
|
d8141a1628 | ||
|
|
de9f7c4baf | ||
|
|
fa9b3116f1 | ||
|
|
dcf9d52961 | ||
|
|
1da93e2cc7 | ||
|
|
1d1bab988e | ||
|
|
dcc6ad3af3 | ||
|
|
d57f266789 | ||
|
|
c3395e1eff | ||
|
|
ca59ec2dbe | ||
|
|
79788125f3 | ||
|
|
2154f20fa4 | ||
|
|
afe40b6a89 | ||
|
|
ba4b3bd6b8 | ||
|
|
e423b5d745 | ||
|
|
6de8eca7ea | ||
|
|
9d68cfcaf0 | ||
|
|
225de11e6a | ||
|
|
916581bbd0 | ||
|
|
1cbb35840f | ||
|
|
7a1d769e39 | ||
|
|
8254bf934c | ||
|
|
5e2f20542f | ||
|
|
551a707ee4 | ||
|
|
024b15b4f9 | ||
|
|
1935df4143 | ||
|
|
3f99f90076 | ||
|
|
53cb445dde | ||
|
|
6e46947220 | ||
|
|
9b65e1671b | ||
|
|
d5c741db35 | ||
|
|
11e0780b6e | ||
|
|
f153541570 | ||
|
|
f066af88e7 | ||
|
|
97e1eef799 | ||
|
|
1bcd902817 | ||
|
|
2484568b21 | ||
|
|
085cc47ea5 | ||
|
|
aac36a88f3 | ||
|
|
1f2ebc82b7 | ||
|
|
9781949064 | ||
|
|
b06ef3781a | ||
|
|
b32213cb7b | ||
|
|
ac4c7d2421 | ||
|
|
824a49b80f | ||
|
|
13efd50d80 | ||
|
|
6fb091d20f | ||
|
|
518ab85cae | ||
|
|
f5124ad8b5 | ||
|
|
6f80900aa8 | ||
|
|
06b80e9281 | ||
|
|
51b39d9365 | ||
|
|
f7d2d8fc95 | ||
|
|
f34fb94c1a | ||
|
|
3107224e50 | ||
|
|
e1c481c534 | ||
|
|
945a2dd3eb | ||
|
|
e318945eb1 | ||
|
|
926709568d | ||
|
|
da040e799c | ||
|
|
694976cb6e | ||
|
|
3f7bd1846a | ||
|
|
714898b4c3 | ||
|
|
4efc9b6990 | ||
|
|
73c3beaff1 | ||
|
|
a6bdccd4ef | ||
|
|
8007991e7d | ||
|
|
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 | ||
|
|
107ae70651 | ||
|
|
04de19033e | ||
|
|
089ac70cd3 | ||
|
|
ae40a9ead9 | ||
|
|
152806b7f6 | ||
|
|
06beb8e704 | ||
|
|
64f2b94685 | ||
|
|
5a42eb98ab | ||
|
|
7d4baa7046 | ||
|
|
a24eaaed50 | ||
|
|
26813c582f | ||
|
|
6067ac73e2 | ||
|
|
b1d94b67f4 | ||
|
|
452f4e69fd |
@@ -8,7 +8,7 @@
|
|||||||

|

|
||||||
|
|
||||||
## VPS
|
## VPS
|
||||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
|
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||||
|
|
||||||
## Сборка проекта
|
## Сборка проекта
|
||||||
Необходима версия node.js не ниже 10.
|
Необходима версия node.js не ниже 10.
|
||||||
|
|||||||
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');
|
||||||
|
}
|
||||||
@@ -16,6 +16,11 @@ module.exports = {
|
|||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
loader: "vue-loader"
|
loader: "vue-loader"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.includer$/,
|
||||||
|
resourceQuery: /^\?vue/,
|
||||||
|
use: path.resolve('build/includer.js')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
const AppCachePlugin = require('appcache-webpack-plugin');
|
const {GenerateSW} = require('workbox-webpack-plugin');
|
||||||
|
|
||||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
@@ -55,6 +55,12 @@ module.exports = merge(baseWpConfig, {
|
|||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||||
new AppCachePlugin({exclude: ['../index.html']})
|
new GenerateSW({
|
||||||
|
cacheId: 'liberama',
|
||||||
|
swDest: `${publicDir}/service-worker.js`,
|
||||||
|
navigateFallback: '/index.html',
|
||||||
|
navigateFallbackDenylist: [new RegExp('^/api'), new RegExp('^/ws'), new RegExp('^/tmp'),],
|
||||||
|
skipWaiting: true,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ class Misc {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await wsc.open();
|
await wsc.open();
|
||||||
return await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,17 +18,22 @@ class Reader {
|
|||||||
if (!callback) callback = () => {};
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
let response = {};
|
let response = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wsc.open();
|
await wsc.open();
|
||||||
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
||||||
|
|
||||||
|
let prevResponse = false;
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
response = await wsc.message(requestId);
|
response = await wsc.message(requestId);
|
||||||
callback(response);
|
|
||||||
|
|
||||||
|
if (!response.state && prevResponse !== false) {//экономия траффика
|
||||||
|
callback(prevResponse);
|
||||||
|
} else {//были изменения worker state
|
||||||
if (!response.state)
|
if (!response.state)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
|
callback(response);
|
||||||
|
prevResponse = response;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.state == 'finish' || response.state == 'error') {
|
if (response.state == 'finish' || response.state == 'error') {
|
||||||
break;
|
break;
|
||||||
@@ -127,6 +132,9 @@ class Reader {
|
|||||||
response = await api.post('/restore-cached-file', {path: url});
|
response = await api.post('/restore-cached-file', {path: url});
|
||||||
response = response.data;
|
response = response.data;
|
||||||
}
|
}
|
||||||
|
if (response.state == 'error') {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
const workerId = response.workerId;
|
const workerId = response.workerId;
|
||||||
if (!workerId)
|
if (!workerId)
|
||||||
@@ -215,6 +223,9 @@ class Reader {
|
|||||||
const state = response.state;
|
const state = response.state;
|
||||||
if (!state)
|
if (!state)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
|
if (response.state == 'error') {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
const cleanPeriod = 60*1000;//1 минута
|
const cleanPeriod = 60*1000;//1 минута
|
||||||
|
|
||||||
class WebSocketConnection {
|
class WebSocketConnection {
|
||||||
@@ -9,6 +11,8 @@ class WebSocketConnection {
|
|||||||
this.messageQueue = [];
|
this.messageQueue = [];
|
||||||
this.messageLifeTime = messageLifeTime;
|
this.messageLifeTime = messageLifeTime;
|
||||||
this.requestId = 0;
|
this.requestId = 0;
|
||||||
|
|
||||||
|
this.connecting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener) {
|
addListener(listener) {
|
||||||
@@ -53,14 +57,22 @@ class WebSocketConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(url) {
|
open(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => { (async() => {
|
||||||
|
//Ожидаем окончания процесса подключения, если open уже был вызван
|
||||||
|
let i = 0;
|
||||||
|
while (this.connecting && i < 200) {//10 сек
|
||||||
|
await utils.sleep(50);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i >= 200)
|
||||||
|
this.connecting = false;
|
||||||
|
|
||||||
|
//проверим подключение, и если нет, то подключимся заново
|
||||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||||
resolve(this.ws);
|
resolve(this.ws);
|
||||||
} else {
|
} else {
|
||||||
let protocol = 'ws:';
|
this.connecting = true;
|
||||||
if (window.location.protocol == 'https:') {
|
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
|
||||||
protocol = 'wss:'
|
|
||||||
}
|
|
||||||
|
|
||||||
url = url || `${protocol}//${window.location.host}/ws`;
|
url = url || `${protocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
@@ -71,9 +83,8 @@ class WebSocketConnection {
|
|||||||
}
|
}
|
||||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
|
||||||
let resolved = false;
|
|
||||||
this.ws.onopen = (e) => {
|
this.ws.onopen = (e) => {
|
||||||
resolved = true;
|
this.connecting = false;
|
||||||
resolve(e);
|
resolve(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,11 +108,13 @@ class WebSocketConnection {
|
|||||||
|
|
||||||
this.ws.onerror = (e) => {
|
this.ws.onerror = (e) => {
|
||||||
this.emit(e.message, true);
|
this.emit(e.message, true);
|
||||||
if (!resolved)
|
if (this.connecting) {
|
||||||
|
this.connecting = false;
|
||||||
reject(e);
|
reject(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
})() });
|
||||||
}
|
}
|
||||||
|
|
||||||
//timeout в минутах (cleanPeriod)
|
//timeout в минутах (cleanPeriod)
|
||||||
@@ -111,11 +124,7 @@ class WebSocketConnection {
|
|||||||
requestId,
|
requestId,
|
||||||
timeout,
|
timeout,
|
||||||
onMessage: (mes) => {
|
onMessage: (mes) => {
|
||||||
if (mes.error) {
|
|
||||||
reject(mes.error);
|
|
||||||
} else {
|
|
||||||
resolve(mes);
|
resolve(mes);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
|||||||
5
client/assets/sw-register.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(function() {
|
||||||
|
if('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,55 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div class="fit row">
|
||||||
<el-aside v-if="showAsideBar" :width="asideWidth">
|
<Notify ref="notify"/>
|
||||||
<div class="app-name"><span v-html="appName"></span></div>
|
<StdDialog ref="stdDialog"/>
|
||||||
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
|
|
||||||
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
|
||||||
<el-menu-item index="/cardindex">
|
|
||||||
<i class="el-icon-search"></i>
|
|
||||||
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/reader">
|
|
||||||
<i class="el-icon-tickets"></i>
|
|
||||||
<span :class="itemTitleClass('/reader')" slot="title">{{ this.itemRuText['/reader'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/forum" disabled>
|
|
||||||
<i class="el-icon-message"></i>
|
|
||||||
<span :class="itemTitleClass('/forum')" slot="title">{{ this.itemRuText['/forum'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/income">
|
|
||||||
<i class="el-icon-upload"></i>
|
|
||||||
<span :class="itemTitleClass('/income')" slot="title">{{ this.itemRuText['/income'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/sources">
|
|
||||||
<i class="el-icon-menu"></i>
|
|
||||||
<span :class="itemTitleClass('/sources')" slot="title">{{ this.itemRuText['/sources'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/settings">
|
|
||||||
<i class="el-icon-setting"></i>
|
|
||||||
<span :class="itemTitleClass('/settings')" slot="title">{{ this.itemRuText['/settings'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="/help">
|
|
||||||
<i class="el-icon-question"></i>
|
|
||||||
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<router-view></router-view>
|
<router-view class="col"></router-view>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</el-main>
|
</div>
|
||||||
</el-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import Notify from './share/Notify.vue';
|
||||||
|
import StdDialog from './share/StdDialog.vue';
|
||||||
import * as utils from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Notify,
|
||||||
|
StdDialog,
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
mode: function() {
|
mode: function() {
|
||||||
this.setAppTitle();
|
this.setAppTitle();
|
||||||
@@ -67,7 +38,8 @@ class App extends Vue {
|
|||||||
'/sources': 'Источники',
|
'/sources': 'Источники',
|
||||||
'/settings': 'Параметры',
|
'/settings': 'Параметры',
|
||||||
'/help': 'Справка',
|
'/help': 'Справка',
|
||||||
}
|
};
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
this.dispatch = this.$store.dispatch;
|
||||||
@@ -75,6 +47,28 @@ class App extends Vue {
|
|||||||
this.uistate = this.$store.state.uistate;
|
this.uistate = this.$store.state.uistate;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
|
|
||||||
|
//root route
|
||||||
|
let cachedRoute = '';
|
||||||
|
let cachedPath = '';
|
||||||
|
this.$root.rootRoute = () => {
|
||||||
|
if (this.$route.path != cachedPath) {
|
||||||
|
cachedPath = this.$route.path;
|
||||||
|
const m = cachedPath.match(/^(\/[^/]*).*$/i);
|
||||||
|
cachedRoute = (m ? m[1] : this.$route.path);
|
||||||
|
|
||||||
|
}
|
||||||
|
return cachedRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.beforeEach((to, from, next) => {
|
||||||
|
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
|
||||||
|
if (window.location.host.indexOf('b.') == 0 && to.path != '/external-libs' && to.path != '/404') {
|
||||||
|
next('/404');
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// set-app-title
|
// set-app-title
|
||||||
this.$root.$on('set-app-title', this.setAppTitle);
|
this.$root.$on('set-app-title', this.setAppTitle);
|
||||||
|
|
||||||
@@ -99,6 +93,9 @@ class App extends Vue {
|
|||||||
document.addEventListener('keyup', (event) => {
|
document.addEventListener('keyup', (event) => {
|
||||||
this.keyHook(event);
|
this.keyHook(event);
|
||||||
});
|
});
|
||||||
|
document.addEventListener('keypress', (event) => {
|
||||||
|
this.keyHook(event);
|
||||||
|
});
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
this.keyHook(event);
|
this.keyHook(event);
|
||||||
});
|
});
|
||||||
@@ -107,23 +104,33 @@ class App extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routerReady() {
|
||||||
|
return new Promise ((resolve) => {
|
||||||
|
this.$router.onReady(() => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$root.notify = this.$refs.notify;
|
||||||
|
this.$root.stdDialog = this.$refs.stdDialog;
|
||||||
|
|
||||||
this.dispatch('config/loadConfig');
|
this.dispatch('config/loadConfig');
|
||||||
this.$watch('apiError', function(newError) {
|
this.$watch('apiError', function(newError) {
|
||||||
if (newError) {
|
if (newError) {
|
||||||
let mes = newError.message;
|
let mes = newError.message;
|
||||||
if (newError.response && newError.response.config)
|
if (newError.response && newError.response.config)
|
||||||
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
||||||
this.$notify.error({
|
this.$root.notify.error(mes, 'Ошибка API');
|
||||||
title: 'Ошибка API',
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
message: mes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setAppTitle();
|
this.setAppTitle();
|
||||||
|
(async() => {
|
||||||
|
await this.routerReady();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
@@ -137,9 +144,9 @@ class App extends Vue {
|
|||||||
|
|
||||||
get asideWidth() {
|
get asideWidth() {
|
||||||
if (this.uistate.asideBarCollapse) {
|
if (this.uistate.asideBarCollapse) {
|
||||||
return '64px';
|
return 64;
|
||||||
} else {
|
} else {
|
||||||
return '170px';
|
return 170;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,15 +170,14 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get rootRoute() {
|
get rootRoute() {
|
||||||
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
|
return this.$root.rootRoute();
|
||||||
this.$root.rootRoute = (m ? m[1] : this.$route.path);
|
|
||||||
|
|
||||||
return this.$root.rootRoute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setAppTitle(title) {
|
setAppTitle(title) {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
if (this.mode == 'omnireader') {
|
if (this.mode == 'liberama.top') {
|
||||||
|
document.title = `Liberama Reader - всегда с вами`;
|
||||||
|
} else if (this.mode == 'omnireader') {
|
||||||
document.title = `Omni Reader - всегда с вами`;
|
document.title = `Omni Reader - всегда с вами`;
|
||||||
} else if (this.config && this.mode !== null) {
|
} else if (this.config && this.mode !== null) {
|
||||||
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
|
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
|
||||||
@@ -190,21 +196,22 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get showAsideBar() {
|
get showAsideBar() {
|
||||||
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
|
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
|
||||||
|
}
|
||||||
|
|
||||||
|
set showAsideBar(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isReaderActive() {
|
get isReaderActive() {
|
||||||
return this.rootRoute == '/reader';
|
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
|
||||||
}
|
|
||||||
|
|
||||||
get showMain() {
|
|
||||||
return (this.showAsideBar || this.isReaderActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectIfNeeded() {
|
redirectIfNeeded() {
|
||||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
|
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
|
||||||
//старый url
|
|
||||||
const search = window.location.search.substr(1);
|
const search = window.location.search.substr(1);
|
||||||
|
|
||||||
|
//распознавание параметра url вида "?url=<link>" и редирект при необходимости
|
||||||
|
if (!this.isReaderActive) {
|
||||||
const s = search.split('url=');
|
const s = search.split('url=');
|
||||||
const url = s[1] || '';
|
const url = s[1] || '';
|
||||||
const q = utils.parseQuery(s[0] || '');
|
const q = utils.parseQuery(s[0] || '');
|
||||||
@@ -217,6 +224,7 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -228,68 +236,28 @@ class App extends Vue {
|
|||||||
line-height: 140%;
|
line-height: 140%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold-font {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-aside {
|
|
||||||
line-height: 1;
|
|
||||||
background-color: #ccc;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-main {
|
|
||||||
padding: 0;
|
|
||||||
background-color: #E6EDF4;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu-vertical:not(.el-menu--collapse) {
|
|
||||||
background-color: inherit;
|
|
||||||
color: inherit;
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu--collapse {
|
|
||||||
background-color: inherit;
|
|
||||||
color: inherit;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
|
|
||||||
background-color: inherit;
|
|
||||||
color: inherit;
|
|
||||||
margin-top: 5px;
|
|
||||||
width: 100%;
|
|
||||||
height: 64px;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.el-menu-item {
|
|
||||||
font-size: 85%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body, html, #app {
|
body, html, #app {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font: normal 12pt ReaderDefault;
|
font: normal 12pt ReaderDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-tabs__content {
|
.dborder {
|
||||||
flex: 1;
|
border: 2px solid magenta !important;
|
||||||
padding: 0 !important;
|
}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
.icon-rotate {
|
||||||
overflow: hidden;
|
vertical-align: middle;
|
||||||
|
animation: rotating 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notify-button-icon {
|
||||||
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Book в разработке
|
Раздел Book в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Card в разработке
|
Раздел Card в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container direction="vertical">
|
<div>
|
||||||
<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>
|
<keep-alive>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</el-tabs>
|
</div>
|
||||||
</el-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -18,7 +12,7 @@ import Vue from 'vue';
|
|||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
const rootRoute = '/cardindex';
|
const selfRoute = '/cardindex';
|
||||||
const tab2Route = [
|
const tab2Route = [
|
||||||
'/cardindex/search',
|
'/cardindex/search',
|
||||||
'/cardindex/card',
|
'/cardindex/card',
|
||||||
@@ -51,7 +45,7 @@ class CardIndex extends Vue {
|
|||||||
if (t !== this.selectedTab)
|
if (t !== this.selectedTab)
|
||||||
this.selectedTab = t.toString();
|
this.selectedTab = t.toString();
|
||||||
} else {
|
} else {
|
||||||
if (route == rootRoute && lastActiveTab !== null)
|
if (route == selfRoute && lastActiveTab !== null)
|
||||||
this.setRouteByTab(lastActiveTab);
|
this.setRouteByTab(lastActiveTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел History в разработке
|
Раздел History в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Search в разработке
|
Раздел Search в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
<template>
|
||||||
|
<Window ref="window" width="600px" height="95%" @close="close">
|
||||||
|
<template slot="header">
|
||||||
|
Настроить закладки
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="col column fit">
|
||||||
|
<div class="row items-center top-panel bg-grey-3">
|
||||||
|
<q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search">
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col row">
|
||||||
|
<div class="left-panel column items-center no-wrap bg-grey-3">
|
||||||
|
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<div class="space"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col fit tree">
|
||||||
|
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||||
|
<q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" />
|
||||||
|
</div>
|
||||||
|
<q-tree
|
||||||
|
class="q-my-xs"
|
||||||
|
:nodes="nodes"
|
||||||
|
node-key="key"
|
||||||
|
tick-strategy="leaf"
|
||||||
|
:selected.sync="selected"
|
||||||
|
:ticked.sync="ticked"
|
||||||
|
:expanded.sync="expanded"
|
||||||
|
selected-color="black"
|
||||||
|
:filter="search"
|
||||||
|
no-nodes-label="Закладок пока нет"
|
||||||
|
no-results-label="Ничего не найдено"
|
||||||
|
>
|
||||||
|
<template v-slot:default-header="p">
|
||||||
|
<div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div>
|
||||||
|
</template>
|
||||||
|
</q-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import Window from '../../share/Window.vue';
|
||||||
|
import * as lu from '../linkUtils';
|
||||||
|
import rstore from '../../../store/modules/reader';
|
||||||
|
|
||||||
|
const BookmarkSettingsProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
libs: Object,
|
||||||
|
addBookmarkVisible: Boolean,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
ticked: function() {
|
||||||
|
this.checkAllTicked();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class BookmarkSettings extends BookmarkSettingsProps {
|
||||||
|
search = '';
|
||||||
|
selected = '';
|
||||||
|
ticked = [];
|
||||||
|
expanded = [];
|
||||||
|
tickAll = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.afterInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.$refs.window.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodes() {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
const expanded = [];
|
||||||
|
this.links = {};
|
||||||
|
this.libs.groups.forEach(group => {
|
||||||
|
const rkey = `r-${group.r}`;
|
||||||
|
const g = {label: group.r, key: rkey, children: []};
|
||||||
|
this.links[rkey] = {l: group.r, c: ''};
|
||||||
|
|
||||||
|
group.list.forEach(link => {
|
||||||
|
const key = link.l;
|
||||||
|
g.children.push({
|
||||||
|
label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l),
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
this.links[key] = link;
|
||||||
|
if (link.l == this.libs.startLink && expanded.indexOf(rkey) < 0) {
|
||||||
|
expanded.push(rkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push(g);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.afterInit) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.expanded = expanded;
|
||||||
|
});
|
||||||
|
this.afterInit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeTickAll() {
|
||||||
|
if (this.tickAll) {
|
||||||
|
const newTicked = [];
|
||||||
|
for (const key of Object.keys(this.links)) {
|
||||||
|
if (key.indexOf('r-') != 0)
|
||||||
|
newTicked.push(key);
|
||||||
|
}
|
||||||
|
this.ticked = newTicked;
|
||||||
|
} else {
|
||||||
|
this.ticked = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAllTicked() {
|
||||||
|
const ticked = new Set(this.ticked);
|
||||||
|
|
||||||
|
let newTickAll = !!(this.nodes.length);
|
||||||
|
for (const key of Object.keys(this.links)) {
|
||||||
|
if (key.indexOf('r-') != 0 && !ticked.has(key))
|
||||||
|
newTickAll = false;
|
||||||
|
}
|
||||||
|
this.tickAll = newTickAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSearch() {
|
||||||
|
this.search = '';
|
||||||
|
this.$refs.search.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
openSelected() {
|
||||||
|
if (!this.selected)
|
||||||
|
return;
|
||||||
|
if (this.selected.indexOf('r-') === 0) {//rootLink
|
||||||
|
this.$emit('do-action', {action: 'setRootLink', data: this.links[this.selected].l});
|
||||||
|
} else {//selectedLink
|
||||||
|
this.$emit('do-action', {action: 'setSelectedLink', data: this.links[this.selected].l});
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
editBookmark() {
|
||||||
|
this.$emit('do-action', {action: 'editBookmark', data: {link: this.links[this.selected].l, desc: this.links[this.selected].c}});
|
||||||
|
}
|
||||||
|
|
||||||
|
addBookmark() {
|
||||||
|
this.$emit('do-action', {action: 'addBookmark'});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delBookmark() {
|
||||||
|
const newLibs = _.cloneDeep(this.libs);
|
||||||
|
|
||||||
|
if (await this.$root.stdDialog.confirm(`Подтвердите удаление ${this.ticked.length} закладок:`, ' ')) {
|
||||||
|
const ticked = new Set(this.ticked);
|
||||||
|
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
|
||||||
|
const g = newLibs.groups[i];
|
||||||
|
for (let j = g.list.length - 1; j >= 0; j--) {
|
||||||
|
if (ticked.has(g.list[j].l)) {
|
||||||
|
delete g.list[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.list = g.list.filter(v => v);
|
||||||
|
if (!g.list.length)
|
||||||
|
delete newLibs.groups[i];
|
||||||
|
else {
|
||||||
|
const item = lu.getListItemByLink(g.list, g.s);
|
||||||
|
if (!item)
|
||||||
|
g.s = g.list[0].l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newLibs.groups = newLibs.groups.filter(v => v);
|
||||||
|
this.ticked = [];
|
||||||
|
this.selected = '';
|
||||||
|
this.$emit('do-action', {action: 'setLibs', data: newLibs});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBookmark(down = false) {
|
||||||
|
const newLibs = _.cloneDeep(this.libs);
|
||||||
|
|
||||||
|
const ticked = new Set(this.ticked);
|
||||||
|
let moved = false;
|
||||||
|
let prevFull = false;
|
||||||
|
if (!down) {
|
||||||
|
for (let i = 0; i < newLibs.groups.length; i++) {
|
||||||
|
const g = newLibs.groups[i];
|
||||||
|
let count = 0;
|
||||||
|
for (let j = 0; j < g.list.length; j++) {
|
||||||
|
if (ticked.has(g.list[j].l)) {
|
||||||
|
if (j > 0 && !ticked.has(g.list[j - 1].l)) {
|
||||||
|
[g.list[j], g.list[j - 1]] = [g.list[j - 1], g.list[j]];
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == g.list.length && !prevFull && i > 0) {
|
||||||
|
const gs = newLibs.groups;
|
||||||
|
[gs[i], gs[i - 1]] = [gs[i - 1], gs[i]];
|
||||||
|
moved = true;
|
||||||
|
} else
|
||||||
|
prevFull = (count == g.list.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
|
||||||
|
const g = newLibs.groups[i];
|
||||||
|
let count = 0;
|
||||||
|
for (let j = g.list.length - 1; j >= 0; j--) {
|
||||||
|
if (ticked.has(g.list[j].l)) {
|
||||||
|
if (j < g.list.length - 1 && !ticked.has(g.list[j + 1].l)) {
|
||||||
|
[g.list[j], g.list[j + 1]] = [g.list[j + 1], g.list[j]];
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == g.list.length && !prevFull && i < newLibs.groups.length - 1) {
|
||||||
|
const gs = newLibs.groups;
|
||||||
|
[gs[i], gs[i + 1]] = [gs[i + 1], gs[i]];
|
||||||
|
moved = true;
|
||||||
|
} else
|
||||||
|
prevFull = (count == g.list.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved)
|
||||||
|
this.$emit('do-action', {action: 'setLibs', data: newLibs});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultBookmarks() {
|
||||||
|
const result = await this.$root.stdDialog.prompt(`Введите 'да' для сброса всех закладок в предустановленные значения:`, ' ', {
|
||||||
|
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.value && result.value.toLowerCase() == 'да') {
|
||||||
|
this.$emit('do-action', {action: 'setLibs', data: _.cloneDeep(
|
||||||
|
Object.assign({helpShowed: true}, rstore.libsDefaults)
|
||||||
|
)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.afterInit = false;
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (this.addBookmarkVisible)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
|
this.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.top-panel {
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
padding: 0 10px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 60px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
padding: 0px 10px 10px 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
text-shadow: 0 0 20px yellow, 0 0 15px yellow, 0 0 10px yellow, 0 0 10px yellow, 0 0 5px yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-tick-all {
|
||||||
|
border-bottom: 1px solid #bbbbbb;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
padding: 5px 5px 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
min-height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
858
client/components/ExternalLibs/ExternalLibs.vue
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
<template>
|
||||||
|
<Window ref="window" @close="close" margin="2px">
|
||||||
|
<template slot="header">
|
||||||
|
{{ header }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template slot="buttons">
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
||||||
|
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||||
|
</span>
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||||
|
<q-icon name="la la-plus" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||||
|
</span>
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||||
|
<q-icon name="la la-minus" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||||
|
</span>
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||||
|
<q-icon name="la la-question-circle" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-show="ready" class="col column" style="min-width: 600px">
|
||||||
|
<div class="row items-center q-px-sm" style="height: 50px">
|
||||||
|
<q-select class="q-mr-sm" ref="rootLink" v-model="rootLink" :options="rootLinkOptions" @input="rootLinkInput"
|
||||||
|
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||||
|
style="width: 230px"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
<template v-slot:selected>
|
||||||
|
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-select class="q-mr-sm" ref="selectedLink" v-model="selectedLink" :options="selectedLinkOptions" @input="selectedLinkInput" style="width: 50px"
|
||||||
|
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Закладки</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги"
|
||||||
|
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
|
||||||
|
<div class="col fit" ref="frameBox" style="position: relative;">
|
||||||
|
<div ref="frameWrap" class="overflow-hidden">
|
||||||
|
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
|
||||||
|
<template slot="header">
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
|
||||||
|
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
|
||||||
|
<div v-else>Добавить закладку</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="q-mx-md row">
|
||||||
|
<q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" @keydown="bookmarkLinkKeyDown"
|
||||||
|
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus">
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Предустановленные ссылки</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md q-mt-md">
|
||||||
|
<q-input class="col q-mr-sm" ref="bookmarkDesc" outlined dense bg-color="white" v-model="bookmarkDesc" @keydown="bookmarkDescKeyDown"
|
||||||
|
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus">
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template slot="footer">
|
||||||
|
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog ref="options" v-model="optionsVisible">
|
||||||
|
<template slot="header">
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
|
||||||
|
Опции
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="q-mx-md column">
|
||||||
|
<q-checkbox v-model="closeAfterSubmit" size="36px" label="Закрыть окно при отправке ссылки в читалку" />
|
||||||
|
<q-checkbox v-model="openInFrameOnEnter" size="36px" label="Открывать ссылку во фрейме при нажатии 'Enter'" />
|
||||||
|
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template slot="footer">
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
|
||||||
|
@do-action="doAction" @close="closeBookmarkSettings">
|
||||||
|
</BookmarkSettings>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import Window from '../share/Window.vue';
|
||||||
|
import Dialog from '../share/Dialog.vue';
|
||||||
|
import BookmarkSettings from './BookmarkSettings/BookmarkSettings.vue';
|
||||||
|
|
||||||
|
import rstore from '../../store/modules/reader';
|
||||||
|
import * as utils from '../../share/utils';
|
||||||
|
import * as lu from './linkUtils';
|
||||||
|
|
||||||
|
const proxySubst = {
|
||||||
|
'http://flibusta.is': 'http://b.liberama.top:23480',
|
||||||
|
'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
Dialog,
|
||||||
|
BookmarkSettings
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
libs: function() {
|
||||||
|
this.loadLibs();
|
||||||
|
},
|
||||||
|
defaultRootLink: function() {
|
||||||
|
this.updateBookmarkLink();
|
||||||
|
},
|
||||||
|
bookUrl: function(newValue) {
|
||||||
|
const value = lu.addProtocol(newValue);
|
||||||
|
const subst = this.makeProxySubst(value, true);
|
||||||
|
if (value != subst) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bookUrl = subst;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bookmarkLink: function(newValue) {
|
||||||
|
const value = lu.addProtocol(newValue);
|
||||||
|
const subst = this.makeProxySubst(value, true);
|
||||||
|
if (value != subst) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bookmarkLink = subst;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeAfterSubmit: function(newValue) {
|
||||||
|
this.commitProp('closeAfterSubmit', newValue);
|
||||||
|
},
|
||||||
|
openInFrameOnEnter: function(newValue) {
|
||||||
|
this.commitProp('openInFrameOnEnter', newValue);
|
||||||
|
},
|
||||||
|
openInFrameOnAdd: function(newValue) {
|
||||||
|
this.commitProp('openInFrameOnAdd', newValue);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class ExternalLibs extends Vue {
|
||||||
|
ready = false;
|
||||||
|
frameVisible = false;
|
||||||
|
rootLink = '';
|
||||||
|
selectedLink = '';
|
||||||
|
frameSrc = '';
|
||||||
|
bookUrl = '';
|
||||||
|
libs = {};
|
||||||
|
fullScreenActive = false;
|
||||||
|
transparentLayoutVisible = false;
|
||||||
|
|
||||||
|
addBookmarkVisible = false;
|
||||||
|
optionsVisible = false;
|
||||||
|
|
||||||
|
addBookmarkMode = '';
|
||||||
|
bookmarkLink = '';
|
||||||
|
bookmarkDesc = '';
|
||||||
|
defaultRootLink = '';
|
||||||
|
|
||||||
|
bookmarkSettingsActive = false;
|
||||||
|
|
||||||
|
closeAfterSubmit = false;
|
||||||
|
openInFrameOnEnter = false;
|
||||||
|
openInFrameOnAdd = false;
|
||||||
|
frameScale = 1;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.oldStartLink = '';
|
||||||
|
this.justOpened = true;
|
||||||
|
this.$root.addKeyHook(this.keyHook);
|
||||||
|
|
||||||
|
this.$root.$on('resize', async() => {
|
||||||
|
await utils.sleep(200);
|
||||||
|
this.frameResize();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.debouncedGoToLink = _.debounce((link) => {
|
||||||
|
this.goToLink(link);
|
||||||
|
}, 100, {'maxWait':200});
|
||||||
|
//this.commit = this.$store.commit;
|
||||||
|
//this.commit('reader/setLibs', rstore.libsDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
//Поправка метода toggleOption компонента select фреймворка quasar, необходимо другое поведение
|
||||||
|
//$emit('input'.. вызывается всегда
|
||||||
|
this.toggleOption = function(opt, keepOpen) {
|
||||||
|
if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optValue = this.getOptionValue(opt);
|
||||||
|
|
||||||
|
if (this.multiple !== true) {
|
||||||
|
if (keepOpen !== true) {
|
||||||
|
this.updateInputValue(this.fillInput === true ? this.getOptionLabel(opt) : '', true, true);
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.target !== void 0 && this.$refs.target.focus();
|
||||||
|
this.$emit('input', this.emitValue === true ? optValue : opt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$refs.rootLink.toggleOption = this.toggleOption;
|
||||||
|
this.$refs.selectedLink.toggleOption = this.toggleOption;
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
//подождем this.mode
|
||||||
|
let i = 0;
|
||||||
|
while(!this.mode && i < 100) {
|
||||||
|
await utils.sleep(100);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode != 'liberama.top') {
|
||||||
|
this.$router.replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
|
this.opener = null;
|
||||||
|
const host = window.location.host;
|
||||||
|
const openerHost = (host.indexOf('b.') == 0 ? host.substring(2) : host);
|
||||||
|
const openerOrigin1 = `http://${openerHost}`;
|
||||||
|
const openerOrigin2 = `https://${openerHost}`;
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
|
||||||
|
return;
|
||||||
|
if (!_.isObject(event.data) || event.data.from != 'LibsPage')
|
||||||
|
return;
|
||||||
|
if (event.origin == openerOrigin1)
|
||||||
|
this.opener = window.opener;
|
||||||
|
else
|
||||||
|
this.opener = event.source;
|
||||||
|
this.openerOrigin = event.origin;
|
||||||
|
|
||||||
|
//console.log(event);
|
||||||
|
|
||||||
|
this.recvMessage(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Ожидаем родителя
|
||||||
|
i = 0;
|
||||||
|
while(!this.opener) {
|
||||||
|
await utils.sleep(1000);
|
||||||
|
i++;
|
||||||
|
if (i >= 5) {
|
||||||
|
await this.$root.stdDialog.alert('Нет связи с читалкой. Окно будет закрыто', 'Ошибка');
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Проверка закрытия родительского окна
|
||||||
|
while(this.opener) {
|
||||||
|
await this.checkOpener();
|
||||||
|
await utils.sleep(1000);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
recvMessage(d) {
|
||||||
|
if (d.type == 'mes') {
|
||||||
|
switch(d.data) {
|
||||||
|
case 'hello': this.sendMessage({type: 'mes', data: 'ready'}); break;
|
||||||
|
}
|
||||||
|
} else if (d.type == 'libs') {
|
||||||
|
this.ready = true;
|
||||||
|
this.libs = _.cloneDeep(d.data);
|
||||||
|
} else if (d.type == 'notify') {
|
||||||
|
this.$root.notify.success(d.data, '', {position: 'bottom-right'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(d) {
|
||||||
|
(async() => {
|
||||||
|
await this.checkOpener();
|
||||||
|
if (this.opener && this.openerOrigin)
|
||||||
|
this.opener.postMessage(Object.assign({}, {from: 'ExternalLibs'}, d), this.openerOrigin);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkOpener() {
|
||||||
|
if (this.opener.closed) {
|
||||||
|
await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commitLibs(libs) {
|
||||||
|
this.sendMessage({type: 'libs', data: libs});
|
||||||
|
}
|
||||||
|
|
||||||
|
commitProp(prop, value) {
|
||||||
|
let libs = _.cloneDeep(this.libs);
|
||||||
|
libs[prop] = value;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLibs() {
|
||||||
|
const libs = this.libs;
|
||||||
|
|
||||||
|
if (!libs.helpShowed) {
|
||||||
|
this.showHelp();
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(1000);
|
||||||
|
this.commitProp('helpShowed', true);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedLink = libs.startLink;
|
||||||
|
this.closeAfterSubmit = libs.closeAfterSubmit || false;
|
||||||
|
this.openInFrameOnEnter = libs.openInFrameOnEnter || false;
|
||||||
|
this.openInFrameOnAdd = libs.openInFrameOnAdd || false;
|
||||||
|
|
||||||
|
this.frameScale = 1;
|
||||||
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
|
||||||
|
if (index >= 0)
|
||||||
|
this.frameScale = this.libs.groups[index].frameScale || 1;
|
||||||
|
|
||||||
|
this.updateStartLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
doAction(event) {
|
||||||
|
switch (event.action) {
|
||||||
|
case 'setLibs': this.commitLibs(event.data); break;
|
||||||
|
case 'setRootLink': this.rootLink = event.data; this.rootLinkInput(); break;
|
||||||
|
case 'setSelectedLink': this.selectedLink = event.data; this.selectedLinkInput(); break;
|
||||||
|
case 'editBookmark': this.addBookmark('edit', event.data.link, event.data.desc); break;
|
||||||
|
case 'addBookmark': this.addBookmark('add'); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get header() {
|
||||||
|
let result = (this.ready ? 'Библиотека' : 'Загрузка...');
|
||||||
|
if (this.ready && this.selectedLink) {
|
||||||
|
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||||
|
}
|
||||||
|
this.$root.$emit('set-app-title', result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rootLinkWithoutProtocol() {
|
||||||
|
return lu.removeProtocol(this.rootLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedLinkByRoot() {
|
||||||
|
if (!this.ready)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||||
|
if (index >= 0)
|
||||||
|
this.selectedLink = this.libs.groups[index].s;
|
||||||
|
else
|
||||||
|
this.selectedLink = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStartLink(force) {
|
||||||
|
if (!this.ready)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
try {
|
||||||
|
this.rootLink = lu.getOrigin(this.selectedLink);
|
||||||
|
index = lu.getRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||||
|
} catch(e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
let libs = _.cloneDeep(this.libs);
|
||||||
|
const com = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
|
||||||
|
if (libs.groups[index].s != this.selectedLink ||
|
||||||
|
libs.startLink != this.selectedLink ||
|
||||||
|
libs.comment != com) {
|
||||||
|
libs.groups[index].s = this.selectedLink;
|
||||||
|
libs.startLink = this.selectedLink;
|
||||||
|
libs.comment = com;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force || this.oldStartLink != libs.startLink) {
|
||||||
|
this.oldStartLink = libs.startLink;
|
||||||
|
this.debouncedGoToLink(this.selectedLink);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.rootLink = '';
|
||||||
|
this.selectedLink = '';
|
||||||
|
this.debouncedGoToLink(this.selectedLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get rootLinkOptions() {
|
||||||
|
let result = [];
|
||||||
|
if (!this.ready)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
this.libs.groups.forEach(group => {
|
||||||
|
result.push({label: lu.removeProtocol(group.r), value: group.r});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultRootLinkOptions() {
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
rstore.libsDefaults.groups.forEach(group => {
|
||||||
|
result.push({label: lu.removeProtocol(group.r), value: group.r});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedLinkOptions() {
|
||||||
|
let result = [];
|
||||||
|
if (!this.ready)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.libs.groups[index].list.forEach(link => {
|
||||||
|
result.push({label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l), value: link.l});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
openBookUrlInFrame() {
|
||||||
|
if (this.bookUrl) {
|
||||||
|
this.goToLink(lu.addProtocol(this.bookUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goToLink(link) {
|
||||||
|
if (!this.ready || !link)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
this.frameVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frameSrc = this.makeProxySubst(link);
|
||||||
|
|
||||||
|
this.frameVisible = false;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.frameVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.frame) {
|
||||||
|
this.$refs.frame.contentWindow.focus();
|
||||||
|
this.frameResize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
frameResize() {
|
||||||
|
this.$refs.frameWrap.style = 'width: 1px; height: 1px;';
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.frame) {
|
||||||
|
const w = this.$refs.frameBox.offsetWidth;
|
||||||
|
const h = this.$refs.frameBox.offsetHeight;
|
||||||
|
const normalSize = `width: ${w}px; height: ${h}px;`;
|
||||||
|
this.$refs.frameWrap.style = normalSize;
|
||||||
|
if (this.frameScale != 1) {
|
||||||
|
const s = this.frameScale;
|
||||||
|
this.$refs.frame.style = `width: ${w/s}px; height: ${h/s}px; transform: scale(${s}); transform-origin: 0 0;`;
|
||||||
|
} else {
|
||||||
|
this.$refs.frame.style = normalSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeScale(delta) {
|
||||||
|
if ((this.frameScale > 0.1 && delta <= 0) || (this.frameScale < 5 && delta >= 0)) {
|
||||||
|
this.frameScale = _.round(this.frameScale + delta, 1);
|
||||||
|
|
||||||
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
|
||||||
|
if (index >= 0) {
|
||||||
|
let libs = _.cloneDeep(this.libs);
|
||||||
|
libs.groups[index].frameScale = this.frameScale;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frameResize();
|
||||||
|
this.$root.notify.success(`Масштаб изменен: ${(this.frameScale*100).toFixed(0)}%`, '', {position: 'bottom-right'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommentByLink(list, link) {
|
||||||
|
const item = lu.getListItemByLink(list, link);
|
||||||
|
return (item ? item.c : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
makeProxySubst(url, reverse = false) {
|
||||||
|
for (const [key, value] of Object.entries(proxySubst)) {
|
||||||
|
if (reverse && value == url.substring(0, value.length)) {
|
||||||
|
return key + url.substring(value.length);
|
||||||
|
} else if (!reverse && key == url.substring(0, key.length)) {
|
||||||
|
return value + url.substring(key.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAllOnFocus(event) {
|
||||||
|
if (event.target.select)
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
rootLinkInput() {
|
||||||
|
this.updateSelectedLinkByRoot();
|
||||||
|
this.updateStartLink(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedLinkInput() {
|
||||||
|
this.updateStartLink(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitUrl() {
|
||||||
|
if (this.bookUrl) {
|
||||||
|
this.sendMessage({type: 'submitUrl', data: {
|
||||||
|
url: this.bookUrl,
|
||||||
|
force: true
|
||||||
|
}});
|
||||||
|
this.bookUrl = '';
|
||||||
|
if (this.closeAfterSubmit)
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addBookmark(mode = 'add', link = '', desc = '') {
|
||||||
|
|
||||||
|
if (mode == 'edit') {
|
||||||
|
this.editBookmarkLink = this.bookmarkLink = link;
|
||||||
|
this.editBookmarkDesc = this.bookmarkDesc = desc;
|
||||||
|
} else {
|
||||||
|
this.bookmarkLink = this.bookUrl;
|
||||||
|
this.bookmarkDesc = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addBookmarkMode = mode;
|
||||||
|
this.addBookmarkVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.bookmarkLink.focus();
|
||||||
|
this.$refs.defaultRootLink.toggleOption = this.toggleOption;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBookmarkLink() {
|
||||||
|
const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.bookmarkLink = rstore.libsDefaults.groups[index].s;
|
||||||
|
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
|
||||||
|
} else {
|
||||||
|
this.bookmarkLink = '';
|
||||||
|
this.bookmarkDesc = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRootLinkInput() {
|
||||||
|
this.updateBookmarkLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkLinkKeyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.$refs.bookmarkDesc.focus();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkDescKeyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
this.okAddBookmark();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async okAddBookmark() {
|
||||||
|
if (!this.bookmarkLink)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const link = (this.addBookmarkMode == 'edit' ? lu.addProtocol(this.editBookmarkLink) : lu.addProtocol(this.bookmarkLink));
|
||||||
|
let index = -1;
|
||||||
|
try {
|
||||||
|
index = lu.getRootIndexByUrl(this.libs.groups, link);
|
||||||
|
} catch (e) {
|
||||||
|
await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let libs = _.cloneDeep(this.libs);
|
||||||
|
|
||||||
|
//добавление
|
||||||
|
//есть группа в закладках
|
||||||
|
if (index >= 0) {
|
||||||
|
const item = lu.getListItemByLink(libs.groups[index].list, link);
|
||||||
|
|
||||||
|
//редактирование
|
||||||
|
if (item && this.addBookmarkMode == 'edit') {
|
||||||
|
if (item) {
|
||||||
|
//редактируем
|
||||||
|
item.l = link;
|
||||||
|
item.c = this.bookmarkDesc;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
} else {
|
||||||
|
await this.$root.stdDialog.alert('Не удалось отредактировать закладку', 'Ошибка');
|
||||||
|
}
|
||||||
|
} else if (!item) {
|
||||||
|
//добавляем
|
||||||
|
if (libs.groups[index].list.length >= 100) {
|
||||||
|
await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
||||||
|
|
||||||
|
if (this.openInFrameOnAdd) {
|
||||||
|
libs.startLink = link;
|
||||||
|
libs.comment = this.bookmarkDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commitLibs(libs);
|
||||||
|
} else if (item.c != this.bookmarkDesc) {
|
||||||
|
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
|
||||||
|
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
|
||||||
|
item.c = this.bookmarkDesc;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
} else
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await this.$root.stdDialog.alert('Такая закладка уже существует', ' ');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {//нет группы в закладках
|
||||||
|
if (libs.groups.length >= 100) {
|
||||||
|
await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//добавляем сначала группу
|
||||||
|
libs.groups.push({r: lu.getOrigin(link), s: link, list: []});
|
||||||
|
|
||||||
|
index = lu.getSafeRootIndexByUrl(libs.groups, link);
|
||||||
|
if (index >= 0)
|
||||||
|
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
||||||
|
|
||||||
|
if (this.openInFrameOnAdd) {
|
||||||
|
libs.startLink = link;
|
||||||
|
libs.comment = this.bookmarkDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addBookmarkVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullScreenToggle() {
|
||||||
|
this.fullScreenActive = !this.fullScreenActive;
|
||||||
|
if (this.fullScreenActive) {
|
||||||
|
this.$q.fullscreen.request();
|
||||||
|
} else {
|
||||||
|
this.$q.fullscreen.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transparentLayoutClick() {
|
||||||
|
this.transparentLayoutVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectPopupShow() {
|
||||||
|
this.transparentLayoutVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectPopupHide() {
|
||||||
|
this.transparentLayoutVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.sendMessage({type: 'close'});
|
||||||
|
}
|
||||||
|
|
||||||
|
bookUrlKeyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
if (!this.openInFrameOnEnter) {
|
||||||
|
this.submitUrl();
|
||||||
|
} else {
|
||||||
|
if (this.bookUrl)
|
||||||
|
this.goToLink(this.bookUrl);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkSettings() {
|
||||||
|
this.bookmarkSettingsActive = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.bookmarkSettings.init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBookmarkSettings() {
|
||||||
|
this.bookmarkSettingsActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showHelp() {
|
||||||
|
this.$root.stdDialog.alert(`
|
||||||
|
<p>Окно 'Библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
|
||||||
|
что особенно актуально для мобильных устройств.</p>
|
||||||
|
|
||||||
|
<p>'Библиотека' разрешает свободный доступ к сайту flibusta.is. Имеется возможность управлять закладками
|
||||||
|
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
|
||||||
|
к сожалению, в нем открываются не все страницы.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
|
||||||
|
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Приятного пользования ;-)
|
||||||
|
</p>
|
||||||
|
`, 'Справка', {iconName: 'la la-info-circle'});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (this.$root.rootRoute() == '/external-libs') {
|
||||||
|
if (this.$root.stdDialog.active)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.bookmarkSettingsActive && this.$refs.bookmarkSettings.keyHook(event))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (this.addBookmarkVisible || this.optionsVisible)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (event.type == 'keydown' && event.key == 'F4') {
|
||||||
|
this.addBookmark();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type == 'keydown' && event.key == 'Escape' &&
|
||||||
|
(document.activeElement != this.$refs.rootLink.$refs.target || !this.$refs.rootLink.menu) &&
|
||||||
|
(document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu)
|
||||||
|
) {
|
||||||
|
this.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.separator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #A0A0A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-screen-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-screen-button:hover {
|
||||||
|
background-color: #69C05F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transparent-layout {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
48
client/components/ExternalLibs/linkUtils.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export function addProtocol(url) {
|
||||||
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0))
|
||||||
|
return 'http://' + url;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeProtocol(url) {
|
||||||
|
return url.replace(/(^\w+:|^)\/\//, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrigin(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeOrigin(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const result = url.substring(parsed.origin.length);
|
||||||
|
return (result ? result : '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRootIndexByUrl(groups, url) {
|
||||||
|
const origin = getOrigin(url);
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
if (groups[i].r == origin)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSafeRootIndexByUrl(groups, url) {
|
||||||
|
let index = -1;
|
||||||
|
try {
|
||||||
|
index = getRootIndexByUrl(groups, url);
|
||||||
|
} catch(e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getListItemByLink(list, link) {
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.l == link)
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Help в разработке
|
Раздел Help в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Income в разработке
|
Раздел Income в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Страница не найдена
|
Страница не найдена
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
207
client/components/Reader/ContentsPage/ContentsPage.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<Window width="600px" ref="window" @close="close">
|
||||||
|
<template slot="header">
|
||||||
|
Оглавление/закладки
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="bg-grey-3 row">
|
||||||
|
<q-tabs
|
||||||
|
v-model="selectedTab"
|
||||||
|
active-color="black"
|
||||||
|
active-bg-color="white"
|
||||||
|
indicator-color="white"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
inline-label
|
||||||
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
|
>
|
||||||
|
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||||
|
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mb-sm"/>
|
||||||
|
|
||||||
|
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
||||||
|
<div>
|
||||||
|
<div class="row" v-for="item in contents" :key="item.key">
|
||||||
|
<q-expansion-item v-if="item.list.length"
|
||||||
|
class="item separator-bottom"
|
||||||
|
expand-icon-toggle
|
||||||
|
switch-toggle-side
|
||||||
|
expand-icon="la la-arrow-circle-down"
|
||||||
|
>
|
||||||
|
<template slot="header">
|
||||||
|
<div class="row no-wrap clickable" style="width: 465px" @click="setBookPos(item.offset)">
|
||||||
|
<div :style="item.style"></div>
|
||||||
|
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
|
||||||
|
<div class="column justify-center">{{ item.perc }}%</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-item class="subitem separator-top column justify-center" v-for="subitem in item.list" :key="subitem.key">
|
||||||
|
<div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(subitem.offset)">
|
||||||
|
<div :style="subitem.style"></div>
|
||||||
|
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="subitem.label"></div>
|
||||||
|
<div class="column justify-center">{{ subitem.perc }}%</div>
|
||||||
|
</div>
|
||||||
|
</q-item>
|
||||||
|
</q-expansion-item>
|
||||||
|
<q-item v-else class="item separator-bottom">
|
||||||
|
<div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(item.offset)">
|
||||||
|
<div :style="item.style"></div>
|
||||||
|
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
|
||||||
|
<div class="column justify-center">{{ item.perc }}%</div>
|
||||||
|
</div>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
|
||||||
|
<div class="column justify-center items-center" style="height: 100px">
|
||||||
|
Раздел находится в разработке
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
//import _ from 'lodash';
|
||||||
|
|
||||||
|
import Window from '../../share/Window.vue';
|
||||||
|
//import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class ContentsPage extends Vue {
|
||||||
|
selectedTab = 'contents';
|
||||||
|
contents = [];
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(currentBook, parsed) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
|
if (this.parsed != parsed) {
|
||||||
|
this.contents = [];
|
||||||
|
await this.$nextTick();
|
||||||
|
this.parsed = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareLabel = (title, bolder = false) => {
|
||||||
|
let titleParts = title.split('<p>');
|
||||||
|
const textParts = titleParts.filter(v => v).map(v => `<div>${v.replace(/(<([^>]+)>)/ig, '')}</div>`);
|
||||||
|
if (bolder && textParts.length > 1)
|
||||||
|
textParts[0] = `<b>${textParts[0]}</b>`;
|
||||||
|
return textParts.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const insetStyle = inset => `width: ${inset*20}px`;
|
||||||
|
const pc = parsed.contents;
|
||||||
|
const newpc = [];
|
||||||
|
|
||||||
|
//преобразуем не первые разделы body в title-subtitle
|
||||||
|
let curSubtitles = [];
|
||||||
|
let prevBodyIndex = -1;
|
||||||
|
for (let i = 0; i < pc.length; i++) {
|
||||||
|
const cont = pc[i];
|
||||||
|
if (prevBodyIndex != cont.bodyIndex)
|
||||||
|
curSubtitles = [];
|
||||||
|
|
||||||
|
prevBodyIndex = cont.bodyIndex;
|
||||||
|
|
||||||
|
if (cont.bodyIndex > 1) {
|
||||||
|
if (cont.inset < 1) {
|
||||||
|
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||||
|
} else {
|
||||||
|
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newpc.push(cont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//формируем newContents
|
||||||
|
let i = 0;
|
||||||
|
const newContents = [];
|
||||||
|
newpc.forEach((cont) => {
|
||||||
|
const label = prepareLabel(cont.title, true);
|
||||||
|
const style = insetStyle(cont.inset);
|
||||||
|
|
||||||
|
let j = 0;
|
||||||
|
const list = [];
|
||||||
|
cont.subtitles.forEach((sub) => {
|
||||||
|
const l = prepareLabel(sub.title);
|
||||||
|
const s = insetStyle(sub.inset + 1);
|
||||||
|
const p = parsed.para[sub.paraIndex];
|
||||||
|
list.push({perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, style: s});
|
||||||
|
j++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const p = parsed.para[cont.paraIndex];
|
||||||
|
newContents.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, style, list});
|
||||||
|
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.contents = newContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBookPos(newValue) {
|
||||||
|
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||||
|
await this.$nextTick();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('do-action', {action: 'contents'});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-panel {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 90%;
|
||||||
|
padding: 0 10px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subitem:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator-top {
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.separator-bottom {
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -91,11 +91,11 @@ class CopyTextPage extends Vue {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.stopInit = true;
|
this.stopInit = true;
|
||||||
this.$emit('copy-text-toggle');
|
this.$emit('do-action', {action: 'copyText'});
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h4>Возможности читалки:</h4>
|
<span class="text-h6 text-bold">Возможности читалки:</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>загрузка любой страницы интернета</li>
|
<li>загрузка любой страницы интернета</li>
|
||||||
|
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||||
<li>работа в автономном режиме (без связи)</li>
|
<li>работа в автономном режиме (без связи)</li>
|
||||||
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
||||||
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
||||||
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
|
||||||
<li>кэширование файлов книг на клиенте и на сервере</li>
|
<li>кэширование файлов книг на клиенте и на сервере</li>
|
||||||
<li>открытие книг с локального диска</li>
|
<li>открытие книг с локального диска</li>
|
||||||
<li>плавный скроллинг текста</li>
|
<li>плавный скроллинг текста</li>
|
||||||
@@ -22,15 +22,15 @@
|
|||||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
||||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||||
|
|
||||||
<div v-show="mode == 'omnireader'">
|
<div v-show="mode == 'omnireader' || mode == 'liberama.top'">
|
||||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||||
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
<br><strong>{{ bookmarkText }}</strong>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||||
<span class="clickable" @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>
|
||||||
</span>
|
|
||||||
<br>или перетащив на панель закладок следующую ссылку:
|
<br>или перетащив на панель закладок следующую ссылку:
|
||||||
<br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
|
<br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
|
||||||
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||||
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
||||||
</p>
|
</p>
|
||||||
@@ -56,13 +56,17 @@ class CommonHelpPage extends Vue {
|
|||||||
return this.$store.state.config.mode;
|
return this.$store.state.config.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get bookmarkText() {
|
||||||
|
return `javascript:location.href='https://${window.location.host}/?url='+location.href;`
|
||||||
|
}
|
||||||
|
|
||||||
async copyText(text, mes) {
|
async copyText(text, mes) {
|
||||||
const result = await copyTextToClipboard(text);
|
const result = await copyTextToClipboard(text);
|
||||||
const msg = (result ? mes : 'Копирование не удалось');
|
const msg = (result ? mes : 'Копирование не удалось');
|
||||||
if (result)
|
if (result)
|
||||||
this.$notify.success({message: msg});
|
this.$root.notify.success(msg);
|
||||||
else
|
else
|
||||||
this.$notify.error({message: msg});
|
this.$root.notify.error(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
@@ -70,20 +74,16 @@ class CommonHelpPage extends Vue {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.copy-icon {
|
||||||
margin: 0;
|
margin-left: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.clickable {
|
|
||||||
color: blue;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,50 +4,47 @@
|
|||||||
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/yandex.png">
|
<img class="logo" src="./assets/yandex.png">
|
||||||
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
|
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
|
||||||
<div class="para">{{ yandexAddress }}
|
<div class="para">{{ yandexAddress }}
|
||||||
<el-tooltip :open-delay="500" effect="light">
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
||||||
<template slot="content">
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
Скопировать
|
</q-icon>
|
||||||
</template>
|
</div>
|
||||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(yandexAddress, 'Яндекс кошелек')"></i>
|
</div>
|
||||||
</el-tooltip>
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/bitcoin.png">
|
<img class="logo" src="./assets/bitcoin.png">
|
||||||
<div class="para">{{ bitcoinAddress }}
|
<div class="para">{{ bitcoinAddress }}
|
||||||
<el-tooltip :open-delay="500" effect="light">
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
||||||
<template slot="content">
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
Скопировать
|
</q-icon>
|
||||||
</template>
|
|
||||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')"></i>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/litecoin.png">
|
<img class="logo" src="./assets/litecoin.png">
|
||||||
<div class="para">{{ litecoinAddress }}
|
<div class="para">{{ litecoinAddress }}
|
||||||
<el-tooltip :open-delay="500" effect="light">
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
||||||
<template slot="content">
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
Скопировать
|
</q-icon>
|
||||||
</template>
|
|
||||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')"></i>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/monero.png">
|
<img class="logo" src="./assets/monero.png">
|
||||||
<div class="para">{{ moneroAddress }}
|
<div class="para">{{ moneroAddress }}
|
||||||
<el-tooltip :open-delay="500" effect="light">
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
||||||
<template slot="content">
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
Скопировать
|
</q-icon>
|
||||||
</template>
|
|
||||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(moneroAddress, 'Monero-адрес')"></i>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,6 +61,7 @@ export default @Component({
|
|||||||
})
|
})
|
||||||
class DonateHelpPage extends Vue {
|
class DonateHelpPage extends Vue {
|
||||||
yandexAddress = '410018702323056';
|
yandexAddress = '410018702323056';
|
||||||
|
paypalAddress = 'bookpauk@gmail.com';
|
||||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
||||||
@@ -78,9 +76,9 @@ class DonateHelpPage extends Vue {
|
|||||||
async copyAddress(address, prefix) {
|
async copyAddress(address, prefix) {
|
||||||
const result = await copyTextToClipboard(address);
|
const result = await copyTextToClipboard(address);
|
||||||
if (result)
|
if (result)
|
||||||
this.$notify.success({message: `${prefix} ${address} успешно скопирован в буфер обмена`});
|
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
||||||
else
|
else
|
||||||
this.$notify.error({message: 'Копирование не удалось'});
|
this.$root.notify.error('Копирование не удалось');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
@@ -88,12 +86,10 @@ class DonateHelpPage extends Vue {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.p {
|
.p {
|
||||||
@@ -103,15 +99,10 @@ class DonateHelpPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
flex: 1;
|
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
.address {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@@ -121,10 +112,6 @@ h5 {
|
|||||||
margin: 10px 10px 10px 40px;
|
margin: 10px 10px 10px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -135,5 +122,6 @@ h5 {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 17 KiB |
@@ -4,23 +4,20 @@
|
|||||||
Справка
|
Справка
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-tabs type="border-card" v-model="selectedTab">
|
<div class="col column" style="min-width: 600px">
|
||||||
<el-tab-pane class="tab" label="Общее">
|
<q-btn-toggle
|
||||||
<CommonHelpPage></CommonHelpPage>
|
v-model="selectedTab"
|
||||||
</el-tab-pane>
|
toggle-color="primary"
|
||||||
<el-tab-pane label="Клавиатура">
|
no-caps unelevated
|
||||||
<HotkeysHelpPage></HotkeysHelpPage>
|
:options="buttons"
|
||||||
</el-tab-pane>
|
/>
|
||||||
<el-tab-pane label="Мышь/тачскрин">
|
<div class="separator"></div>
|
||||||
<MouseHelpPage></MouseHelpPage>
|
|
||||||
</el-tab-pane>
|
<keep-alive>
|
||||||
<el-tab-pane label="История версий" name="releases">
|
<component ref="page" class="col" :is="activePage"
|
||||||
<VersionHistoryPage></VersionHistoryPage>
|
></component>
|
||||||
</el-tab-pane>
|
</keep-alive>
|
||||||
<el-tab-pane label="Помочь проекту" name="donate">
|
</div>
|
||||||
<DonateHelpPage></DonateHelpPage>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -33,36 +30,58 @@ import Window from '../../share/Window.vue';
|
|||||||
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
|
||||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||||
|
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||||
|
|
||||||
|
const pages = {
|
||||||
|
'CommonHelpPage': CommonHelpPage,
|
||||||
|
'HotkeysHelpPage': HotkeysHelpPage,
|
||||||
|
'MouseHelpPage': MouseHelpPage,
|
||||||
|
'VersionHistoryPage': VersionHistoryPage,
|
||||||
|
'DonateHelpPage': DonateHelpPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
['CommonHelpPage', 'Общее'],
|
||||||
|
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||||
|
['HotkeysHelpPage', 'Клавиатура'],
|
||||||
|
['VersionHistoryPage', 'История версий'],
|
||||||
|
['DonateHelpPage', 'Помочь проекту'],
|
||||||
|
];
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
components: {
|
components: Object.assign({ Window }, pages),
|
||||||
Window,
|
|
||||||
CommonHelpPage,
|
|
||||||
HotkeysHelpPage,
|
|
||||||
MouseHelpPage,
|
|
||||||
DonateHelpPage,
|
|
||||||
VersionHistoryPage,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
class HelpPage extends Vue {
|
class HelpPage extends Vue {
|
||||||
selectedTab = null;
|
selectedTab = 'CommonHelpPage';
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$emit('help-toggle');
|
this.$emit('do-action', {action: 'help'});
|
||||||
|
}
|
||||||
|
|
||||||
|
get activePage() {
|
||||||
|
if (pages[this.selectedTab])
|
||||||
|
return pages[this.selectedTab];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get buttons() {
|
||||||
|
let result = [];
|
||||||
|
for (const tab of tabs)
|
||||||
|
result.push({label: tab[1], value: tab[0]});
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
activateDonateHelpPage() {
|
activateDonateHelpPage() {
|
||||||
this.selectedTab = 'donate';
|
this.selectedTab = 'DonateHelpPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
activateVersionHistoryHelpPage() {
|
activateVersionHistoryHelpPage() {
|
||||||
this.selectedTab = 'releases';
|
this.selectedTab = 'VersionHistoryPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -72,16 +91,8 @@ class HelpPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-tabs {
|
.separator {
|
||||||
flex: 1;
|
height: 1px;
|
||||||
display: flex;
|
background-color: #E0E0E0;
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tab-pane {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h4>Управление с помощью горячих клавиш:</h4>
|
<div style="font-size: 120%">
|
||||||
<ul>
|
<div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
|
||||||
<li><b>F1, H</b> - открыть справку</li>
|
<br>
|
||||||
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
|
</div>
|
||||||
<li><b>Tab, Q</b> - показать/скрыть панель управления</li>
|
<div class="q-mb-md" style="width: 550px">
|
||||||
<li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
|
<div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div>
|
||||||
<li><b>PageDown, Right, Space</b> - страницу вперед</li>
|
<UserHotKeys v-model="userHotKeys" readonly/>
|
||||||
<li><b>Home</b> - в начало книги</li>
|
</div>
|
||||||
<li><b>End</b> - в конец книги</li>
|
|
||||||
<li><b>Up</b> - строчку назад</li>
|
|
||||||
<li><b>Down</b> - строчку вперёд</li>
|
|
||||||
<li><b>A, Shift+A</b> - изменить размер шрифта</li>
|
|
||||||
<li><b>Enter, F, F11, ` (апостроф)</b> - вкл./выкл. полный экран</li>
|
|
||||||
<li><b>Z</b> - вкл./выкл. плавный скроллинг текста</li>
|
|
||||||
<li><b>Shift+Down/Shift+Up</b> - увеличить/уменьшить скорость скроллинга
|
|
||||||
<li><b>P</b> - установить страницу</li>
|
|
||||||
<li><b>Ctrl+F</b> - найти в тексте</li>
|
|
||||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
|
||||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
|
||||||
<li><b>X</b> - открыть недавние</li>
|
|
||||||
<li><b>O</b> - автономный режим</li>
|
|
||||||
<li><b>S</b> - открыть окно настроек</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -31,25 +16,32 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
UserHotKeys,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
class HotkeysHelpPage extends Vue {
|
class HotkeysHelpPage extends Vue {
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userHotKeys() {
|
||||||
|
return this.$store.state.reader.settings.userHotKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
set userHotKeys(value) {
|
||||||
|
//no setter
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
|
||||||
line-height: 130%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h4>Управление с помощью мыши/тачскрина:</h4>
|
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
||||||
<div class="click-map-page">
|
<div class="click-map-page">
|
||||||
@@ -49,17 +49,12 @@ class MouseHelpPage extends Vue {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-map-page {
|
.click-map-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="versionHistoryPage" class="page">
|
<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)">
|
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
|
||||||
<p>
|
<p>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<br>
|
|
||||||
<h4>История версий:</h4>
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
||||||
@@ -58,15 +59,11 @@ class VersionHistoryPage extends Vue {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
}
|
position: relative;
|
||||||
|
|
||||||
h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|||||||
131
client/components/Reader/LibsPage/LibsPage.vue
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hidden"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import Window from '../../share/Window.vue';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
//import rstore from '../../../store/modules/reader';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
libs: function() {
|
||||||
|
this.sendLibs();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class LibsPage extends Vue {
|
||||||
|
created() {
|
||||||
|
this.popupWindow = null;
|
||||||
|
this.commit = this.$store.commit;
|
||||||
|
this.messageListener = null;
|
||||||
|
//this.commit('reader/setLibs', rstore.libsDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.mode != 'liberama.top')
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.childReady = false;
|
||||||
|
const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
|
||||||
|
this.origin = `http://${subdomain}${window.location.host}`;
|
||||||
|
|
||||||
|
this.messageListener = (event) => {
|
||||||
|
if (event.origin !== this.origin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//console.log(event.data);
|
||||||
|
|
||||||
|
this.recvMessage(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.popupWindow = window.open(`${this.origin}/#/external-libs`);
|
||||||
|
|
||||||
|
if (this.popupWindow) {
|
||||||
|
|
||||||
|
window.addEventListener('message', this.messageListener);
|
||||||
|
|
||||||
|
//Проверка закрытия окна
|
||||||
|
(async() => {
|
||||||
|
while(this.popupWindow) {
|
||||||
|
if (this.popupWindow && this.popupWindow.closed)
|
||||||
|
this.close();
|
||||||
|
await utils.sleep(1000);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
//Установление связи с окном
|
||||||
|
(async() => {
|
||||||
|
let i = 0;
|
||||||
|
while(!this.childReady && this.popupWindow && i < 100) {
|
||||||
|
this.sendMessage({type: 'mes', data: 'hello'});
|
||||||
|
await utils.sleep(100);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
this.sendLibs();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recvMessage(d) {
|
||||||
|
if (d.type == 'mes') {
|
||||||
|
switch(d.data) {
|
||||||
|
case 'ready':
|
||||||
|
this.childReady = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (d.type == 'libs') {
|
||||||
|
this.commit('reader/setLibs', d.data);
|
||||||
|
} else if (d.type == 'close') {
|
||||||
|
this.close();
|
||||||
|
} else if (d.type == 'submitUrl') {
|
||||||
|
this.$emit('load-book', d.data);
|
||||||
|
this.sendMessage({type: 'notify', data: 'Ссылка передана в читалку'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(d) {
|
||||||
|
if (this.popupWindow)
|
||||||
|
this.popupWindow.postMessage(Object.assign({}, {from: 'LibsPage'}, d), this.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
done() {
|
||||||
|
window.removeEventListener('message', this.messageListener);
|
||||||
|
if (this.popupWindow) {
|
||||||
|
this.popupWindow.close();
|
||||||
|
this.popupWindow = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get libs() {
|
||||||
|
return this.$store.state.reader.libs;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendLibs() {
|
||||||
|
this.sendMessage({type: 'libs', data: this.libs});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('libs-close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.separator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #A0A0A0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -60,8 +60,13 @@
|
|||||||
},
|
},
|
||||||
flipped: false,
|
flipped: false,
|
||||||
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
|
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',
|
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 ' +
|
||||||
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'
|
'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: {
|
methods: {
|
||||||
@@ -99,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
#github-corner .octo-arm {
|
#github-corner .octo-arm {
|
||||||
transform-origin: 130px 106px
|
transform-origin: 130px 106px
|
||||||
}
|
}
|
||||||
@@ -122,6 +127,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||||
transition: fill 1s ease;
|
transition: fill 1s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main">
|
<div ref="main" class="column no-wrap" style="min-height: 500px">
|
||||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F"></GithubCorner>
|
<div v-if="mode != 'liberama.top'" class="relative-position">
|
||||||
<div class="part top">
|
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
|
||||||
<span class="greeting bold-font">{{ title }}</span>
|
</div>
|
||||||
<div class="space"></div>
|
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
|
||||||
|
<span class="greeting"><b>{{ title }}</b></span>
|
||||||
|
<div class="q-my-sm"></div>
|
||||||
<span class="greeting">Добро пожаловать!</span>
|
<span class="greeting">Добро пожаловать!</span>
|
||||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
||||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="part center">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
|
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
|
||||||
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
|
<template v-slot:append>
|
||||||
</el-input>
|
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
|
||||||
<div class="space"></div>
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
<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>
|
</q-btn>
|
||||||
<div class="space"></div>
|
|
||||||
<el-button size="mini" @click="loadBufferClick">
|
|
||||||
Из буфера обмена
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<div class="space"></div>
|
<div class="q-my-sm"></div>
|
||||||
<div class="space"></div>
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||||
<div v-if="mode == 'omnireader'">
|
Из буфера обмена
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<div class="q-my-md"></div>
|
||||||
|
<!--div v-if="mode == 'omnireader'">
|
||||||
<div ref="yaShare2" class="ya-share2"
|
<div ref="yaShare2" class="ya-share2"
|
||||||
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
||||||
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
||||||
@@ -34,12 +39,12 @@
|
|||||||
data-url="https://omnireader.ru">
|
data-url="https://omnireader.ru">
|
||||||
</div>
|
</div>
|
||||||
</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="openComments">Отзывы о читалке</span>
|
||||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
|
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
|
||||||
</div>
|
</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="openHelp">Справка</span>
|
||||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||||
|
|
||||||
@@ -77,8 +82,8 @@ class LoaderPage extends Vue {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.progress = this.$refs.progress;
|
this.progress = this.$refs.progress;
|
||||||
if (this.mode == 'omnireader')
|
/*if (this.mode == 'omnireader')
|
||||||
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
|
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
|
||||||
}
|
}
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
@@ -88,6 +93,8 @@ class LoaderPage extends Vue {
|
|||||||
get title() {
|
get title() {
|
||||||
if (this.mode == 'omnireader')
|
if (this.mode == 'omnireader')
|
||||||
return 'Omni Reader - браузерная онлайн-читалка.';
|
return 'Omni Reader - браузерная онлайн-читалка.';
|
||||||
|
if (this.mode == 'liberama.top')
|
||||||
|
return 'Liberama Reader - браузерная онлайн-читалка.';
|
||||||
return 'Универсальная читалка книг и ресурсов интернета.';
|
return 'Универсальная читалка книг и ресурсов интернета.';
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -143,12 +150,12 @@ class LoaderPage extends Vue {
|
|||||||
this.pasteTextActive = !this.pasteTextActive;
|
this.pasteTextActive = !this.pasteTextActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
openHelp() {
|
openHelp(event) {
|
||||||
this.$emit('help-toggle');
|
this.$emit('do-action', {action: 'help', event});
|
||||||
}
|
}
|
||||||
|
|
||||||
openDonate() {
|
openDonate() {
|
||||||
this.$emit('donate-toggle');
|
this.$emit('do-action', {action: 'donate'});
|
||||||
}
|
}
|
||||||
|
|
||||||
openComments() {
|
openComments() {
|
||||||
@@ -166,82 +173,39 @@ class LoaderPage extends Vue {
|
|||||||
|
|
||||||
//недостатки сторонних ui
|
//недостатки сторонних ui
|
||||||
const input = this.$refs.input.$refs.input;
|
const input = this.$refs.input.$refs.input;
|
||||||
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
|
||||||
this.submitUrl();
|
this.submitUrl();
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
|
|
||||||
this.$emit('help-toggle');
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
|
if (event.type == 'keydown' && document.activeElement !== input) {
|
||||||
this.$emit('tool-bar-toggle');
|
const action = this.$root.readerActionByKeyEvent(event);
|
||||||
event.preventDefault();
|
switch (action) {
|
||||||
event.stopPropagation();
|
case 'help':
|
||||||
|
this.openHelp(event);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.part {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.greeting {
|
.greeting {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
line-height: 160%;
|
line-height: 160%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold-font {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
color: blue;
|
color: blue;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top {
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 0 10px 0 10px;
|
|
||||||
min-height: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-span {
|
.bottom-span {
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-input {
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -3,14 +3,12 @@
|
|||||||
<template slot="header">
|
<template slot="header">
|
||||||
<span style="position: relative; top: -3px">
|
<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
|
или F2
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div>
|
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
|
||||||
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||||
</Window>
|
</Window>
|
||||||
@@ -70,7 +68,7 @@ class PasteTextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadBuffer() {
|
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();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +78,7 @@ class PasteTextPage extends Vue {
|
|||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown') {
|
if (event.type == 'keydown') {
|
||||||
switch (event.code) {
|
switch (event.key) {
|
||||||
case 'F2':
|
case 'F2':
|
||||||
this.loadBuffer();
|
this.loadBuffer();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="visible" class="main">
|
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
||||||
<div class="center">
|
<div class="column justify-start items-center" style="height: 250px">
|
||||||
<el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
|
<q-circular-progress
|
||||||
<p class="text">{{ text }}</p>
|
show-value
|
||||||
|
instant-feedback
|
||||||
|
font-size="13px"
|
||||||
|
:value="percentage"
|
||||||
|
size="100px"
|
||||||
|
:thickness="0.11"
|
||||||
|
color="green-7"
|
||||||
|
track-color="grey-4"
|
||||||
|
class="q-ma-md"
|
||||||
|
>
|
||||||
|
<span class="text-yellow">{{ percentage }}%</span>
|
||||||
|
</q-circular-progress>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-yellow">{{ text }}</span>
|
||||||
|
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -11,6 +27,7 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const ruMessage = {
|
const ruMessage = {
|
||||||
'start': ' ',
|
'start': ' ',
|
||||||
@@ -33,14 +50,15 @@ class ProgressPage extends Vue {
|
|||||||
step = 1;
|
step = 1;
|
||||||
progress = 0;
|
progress = 0;
|
||||||
visible = false;
|
visible = false;
|
||||||
|
iconStyle = '';
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
|
|
||||||
this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
|
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.totalSteps = 1;
|
this.totalSteps = 1;
|
||||||
this.step = 1;
|
this.step = 1;
|
||||||
this.progress = 0;
|
this.progress = 0;
|
||||||
|
this.iconAngle = 0;
|
||||||
|
this.ani = false;
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
@@ -48,6 +66,7 @@ class ProgressPage extends Vue {
|
|||||||
hide() {
|
hide() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.text = '';
|
this.text = '';
|
||||||
|
this.iconAngle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(state) {
|
setState(state) {
|
||||||
@@ -61,46 +80,21 @@ class ProgressPage extends Vue {
|
|||||||
this.step = (state.step ? state.step : this.step);
|
this.step = (state.step ? state.step : this.step);
|
||||||
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
||||||
this.progress = state.progress || 0;
|
this.progress = state.progress || 0;
|
||||||
|
|
||||||
|
if (!this.ani) {
|
||||||
|
(async() => {
|
||||||
|
this.ani = true;
|
||||||
|
this.iconAngle += 30;
|
||||||
|
this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
|
||||||
|
await utils.sleep(150);
|
||||||
|
this.ani = false;
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get percentage() {
|
get percentage() {
|
||||||
let circle = document.querySelector('path[class="el-progress-circle__path"]');
|
|
||||||
if (circle)
|
|
||||||
circle.style.transition = '';
|
|
||||||
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
z-index: 100;
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.el-progress__text {
|
|
||||||
color: lightgreen !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
250
client/components/Reader/ReaderDialogs/ReaderDialogs.vue
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||||
|
<template slot="header">
|
||||||
|
Что нового:
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||||
|
|
||||||
|
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||||
|
<span slot="footer">
|
||||||
|
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||||
|
</span>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog ref="dialog2" v-model="donationVisible">
|
||||||
|
<template slot="header">
|
||||||
|
Здравствуйте, уважаемые читатели!
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
||||||
|
|
||||||
|
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
||||||
|
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
||||||
|
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>непрерывно улучшаемой</li>
|
||||||
|
<li>без рекламы</li>
|
||||||
|
<li>без регистрации</li>
|
||||||
|
<li>Open Source</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Автор также обращается с просьбой о помощи в распространении
|
||||||
|
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
||||||
|
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
|
||||||
|
<br><br>
|
||||||
|
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer">
|
||||||
|
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||||
|
<br>
|
||||||
|
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||||
|
</span>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog ref="dialog3" v-model="liberamaTopVisible">
|
||||||
|
<template slot="header">
|
||||||
|
Здравствуйте, уважаемые читатели!
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Создан новый ресурс:<br><br>
|
||||||
|
|
||||||
|
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
|
||||||
|
<br><br>
|
||||||
|
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
|
||||||
|
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
|
||||||
|
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
|
||||||
|
Спасибо, что вы с нами!
|
||||||
|
<br><br>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer">
|
||||||
|
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
|
||||||
|
</span>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import Dialog from '../../share/Dialog.vue';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
import {versionHistory} from '../versionHistory';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Dialog
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
settings: function() {
|
||||||
|
this.loadSettings();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class ReaderDialogs extends Vue {
|
||||||
|
whatsNewVisible = false;
|
||||||
|
whatsNewContent = '';
|
||||||
|
donationVisible = false;
|
||||||
|
liberamaTopVisible = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.commit = this.$store.commit;
|
||||||
|
this.loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.showWhatsNew();
|
||||||
|
await this.showDonation();
|
||||||
|
await this.showLiberamaTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings() {
|
||||||
|
const settings = this.settings;
|
||||||
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
|
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||||
|
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
|
||||||
|
}
|
||||||
|
|
||||||
|
async showWhatsNew() {
|
||||||
|
const whatsNew = versionHistory[0];
|
||||||
|
if (this.showWhatsNewDialog &&
|
||||||
|
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||||
|
whatsNew.header != this.whatsNewContentHash) {
|
||||||
|
await utils.sleep(2000);
|
||||||
|
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
||||||
|
this.whatsNewVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showDonation() {
|
||||||
|
const today = utils.formatDate(new Date(), 'coDate');
|
||||||
|
|
||||||
|
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
this.donationVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donationDialogDisable() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
if (this.showDonationDialog2020) {
|
||||||
|
this.commit('reader/setSettings', { showDonationDialog2020: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donationDialogRemind() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
openDonate() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
this.liberamaTopVisible = false;
|
||||||
|
this.$emit('donate-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyLink(link) {
|
||||||
|
const result = await utils.copyTextToClipboard(link);
|
||||||
|
if (result)
|
||||||
|
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
||||||
|
else
|
||||||
|
this.$root.notify.error('Копирование не удалось');
|
||||||
|
}
|
||||||
|
|
||||||
|
openVersionHistory() {
|
||||||
|
this.whatsNewVisible = false;
|
||||||
|
this.$emit('version-history-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
whatsNewDisable() {
|
||||||
|
this.whatsNewVisible = false;
|
||||||
|
const whatsNew = versionHistory[0];
|
||||||
|
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return this.$store.state.reader.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
get whatsNewContentHash() {
|
||||||
|
return this.$store.state.reader.whatsNewContentHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
get donationRemindDate() {
|
||||||
|
return this.$store.state.reader.donationRemindDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async showLiberamaTop() {
|
||||||
|
const today = utils.formatDate(new Date(), 'coDate');
|
||||||
|
|
||||||
|
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
this.liberamaTopVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liberamaTopDialogDisable() {
|
||||||
|
this.liberamaTopVisible = false;
|
||||||
|
if (this.showLiberamaTopDialog2020) {
|
||||||
|
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook() {
|
||||||
|
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,97 +1,84 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window width="600px" ref="window" @close="close">
|
<Window width="600px" ref="window" @close="close">
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
|
<span v-show="!loading">{{ header }}</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-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<a ref="download" style='display: none;'></a>
|
<a ref="download" style='display: none;' target="_blank"></a>
|
||||||
<el-table
|
|
||||||
|
<q-table
|
||||||
|
class="recent-books-table col"
|
||||||
:data="tableData"
|
:data="tableData"
|
||||||
style="width: 570px"
|
:columns="columns"
|
||||||
size="mini"
|
row-key="key"
|
||||||
height="1px"
|
:pagination.sync="pagination"
|
||||||
stripe
|
separator="cell"
|
||||||
border
|
hide-bottom
|
||||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
virtual-scroll
|
||||||
:header-cell-style = "headerCellStyle"
|
dense
|
||||||
:row-key = "rowKey"
|
|
||||||
>
|
>
|
||||||
|
<template v-slot:header="props">
|
||||||
<el-table-column
|
<q-tr :props="props">
|
||||||
type="index"
|
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
|
||||||
width="35px"
|
<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">
|
||||||
</el-table-column>
|
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
||||||
<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>
|
|
||||||
|
|
||||||
<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="Найти"
|
placeholder="Найти"
|
||||||
style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
v-model="search"
|
||||||
:value="search" @input="search = $event.target.value"
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span v-html="props.cols[2].label"></span>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td key="num" :props="props" class="td-mp" auto-width>
|
||||||
|
<div class="break-word" style="width: 25px">
|
||||||
|
{{ props.row.num }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</q-td>
|
||||||
|
|
||||||
<el-table-column
|
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||||
min-width="280px"
|
<div class="break-word" style="width: 68px">
|
||||||
>
|
{{ props.row.touchDate }}<br>
|
||||||
<template slot-scope="scope">
|
{{ props.row.touchTime }}
|
||||||
<div class="desc" @click="loadBook(scope.row.url)">
|
|
||||||
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
|
||||||
<span>{{ scope.row.desc.title }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||||
|
<div class="break-word" style="width: 332px; font-size: 90%">
|
||||||
|
<div style="color: green">{{ props.row.desc.author }}</div>
|
||||||
|
<div>{{ props.row.desc.title }}</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||||
|
<div class="break-word" style="width: 75px; font-size: 90%">
|
||||||
|
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
||||||
|
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="close" :props="props" class="td-mp" auto-width>
|
||||||
|
<div style="width: 38px">
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||||
|
@click="handleDel(props.row.key)">
|
||||||
|
<q-icon class="la la-times" size="14px" style="top: -6px"/>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="last" :props="props" class="no-mp">
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</q-table>
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
min-width="90px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
|
||||||
<a :href="scope.row.path" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
width="60px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button
|
|
||||||
size="mini"
|
|
||||||
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
|
||||||
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
</el-table>
|
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -121,52 +108,90 @@ class RecentBooksPage extends Vue {
|
|||||||
loading = false;
|
loading = false;
|
||||||
search = null;
|
search = null;
|
||||||
tableData = [];
|
tableData = [];
|
||||||
|
columns = [];
|
||||||
|
pagination = {};
|
||||||
|
|
||||||
created() {
|
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() {
|
init() {
|
||||||
this.$refs.window.init();
|
this.$refs.window.init();
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
//this.$refs.input.focus();
|
//this.$refs.input.focus();//плохо на планшетах
|
||||||
});
|
});
|
||||||
(async() => {//отбражение подгрузки списка, иначе тормозит
|
(async() => {//подгрузка списка
|
||||||
if (this.initing)
|
if (this.initing)
|
||||||
return;
|
return;
|
||||||
this.initing = true;
|
this.initing = true;
|
||||||
|
|
||||||
await this.updateTableData(3);
|
|
||||||
await utils.sleep(200);
|
|
||||||
|
|
||||||
if (bookManager.loaded) {
|
if (!bookManager.loaded) {
|
||||||
const t = Date.now();
|
|
||||||
await this.updateTableData(10);
|
await this.updateTableData(10);
|
||||||
if (bookManager.getSortedRecent().length > 10)
|
//для отзывчивости
|
||||||
await utils.sleep(10*(Date.now() - t));
|
await utils.sleep(100);
|
||||||
} else {
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
let j = 5;
|
let j = 5;
|
||||||
while (i < 500 && !bookManager.loaded) {
|
while (i < 500 && !bookManager.loaded) {
|
||||||
if (i % j == 0) {
|
if (i % j == 0) {
|
||||||
bookManager.sortedRecentCached = null;
|
bookManager.sortedRecentCached = null;
|
||||||
await this.updateTableData(100);
|
await this.updateTableData(20);
|
||||||
j *= 2;
|
j *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
await utils.sleep(100);
|
await utils.sleep(100);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
//для отзывчивости
|
||||||
|
await utils.sleep(100);
|
||||||
}
|
}
|
||||||
await this.updateTableData();
|
await this.updateTableData();
|
||||||
this.initing = false;
|
this.initing = false;
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
rowKey(row) {
|
|
||||||
return row.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTableData(limit) {
|
async updateTableData(limit) {
|
||||||
while (this.updating) await utils.sleep(100);
|
while (this.updating) await utils.sleep(100);
|
||||||
this.updating = true;
|
this.updating = true;
|
||||||
@@ -175,11 +200,13 @@ class RecentBooksPage extends Vue {
|
|||||||
this.loading = !!limit;
|
this.loading = !!limit;
|
||||||
const sorted = bookManager.getSortedRecent();
|
const sorted = bookManager.getSortedRecent();
|
||||||
|
|
||||||
|
let num = 0;
|
||||||
for (let i = 0; i < sorted.length; i++) {
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
const book = sorted[i];
|
const book = sorted[i];
|
||||||
if (book.deleted)
|
if (book.deleted)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
num++;
|
||||||
if (limit && result.length >= limit)
|
if (limit && result.length >= limit)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -221,19 +248,19 @@ class RecentBooksPage extends Vue {
|
|||||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
|
num,
|
||||||
touchDateTime: book.touchTime,
|
touchDateTime: book.touchTime,
|
||||||
touchDate: t[0],
|
touchDate: t[0],
|
||||||
touchTime: t[1],
|
touchTime: t[1],
|
||||||
desc: {
|
desc: {
|
||||||
title: `${title}${perc}${textLen}`,
|
|
||||||
author,
|
author,
|
||||||
|
title: `${title}${perc}${textLen}`,
|
||||||
},
|
},
|
||||||
|
descString: `${author}${title}${perc}${textLen}`,
|
||||||
url: book.url,
|
url: book.url,
|
||||||
path: book.path,
|
path: book.path,
|
||||||
key: book.key,
|
key: book.key,
|
||||||
});
|
});
|
||||||
if (result.length >= 100)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = this.search;
|
const search = this.search;
|
||||||
@@ -245,29 +272,23 @@ class RecentBooksPage extends Vue {
|
|||||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||||
});
|
});
|
||||||
|
|
||||||
/*for (let i = 0; i < result.length; i++) {
|
|
||||||
if (!_.isEqual(this.tableData[i], result[i])) {
|
|
||||||
this.$set(this.tableData, i, result[i]);
|
|
||||||
await utils.sleep(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.tableData.length > result.length)
|
|
||||||
this.tableData.splice(result.length);*/
|
|
||||||
|
|
||||||
this.tableData = result;
|
this.tableData = result;
|
||||||
this.updating = false;
|
this.updating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
headerCellStyle(cell) {
|
wordEnding(num) {
|
||||||
let result = {margin: 0, padding: 0};
|
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
||||||
if (cell.columnIndex > 0) {
|
const deci = num % 100;
|
||||||
result['border-bottom'] = 0;
|
if (deci > 10 && deci < 20) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return endings[num % 10];
|
||||||
}
|
}
|
||||||
if (cell.rowIndex > 0) {
|
|
||||||
result.height = '0px';
|
|
||||||
result['border-right'] = 0;
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
get header() {
|
||||||
|
const len = (this.tableData ? this.tableData.length : 0);
|
||||||
|
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBook(fb2path) {
|
async downloadBook(fb2path) {
|
||||||
@@ -277,12 +298,13 @@ class RecentBooksPage extends Vue {
|
|||||||
const d = this.$refs.download;
|
const d = this.$refs.download;
|
||||||
d.href = fb2path;
|
d.href = fb2path;
|
||||||
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||||
|
|
||||||
d.click();
|
d.click();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errMes = e.message;
|
let errMes = e.message;
|
||||||
if (errMes.indexOf('404') >= 0)
|
if (errMes.indexOf('404') >= 0)
|
||||||
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
|
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
|
||||||
this.$alert(errMes, 'Ошибка', {type: 'error'});
|
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +318,7 @@ class RecentBooksPage extends Vue {
|
|||||||
|
|
||||||
async handleDel(key) {
|
async handleDel(key) {
|
||||||
await bookManager.delRecentBook({key});
|
await bookManager.delRecentBook({key});
|
||||||
this.updateTableData();
|
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||||
|
|
||||||
if (!bookManager.mostRecentBook())
|
if (!bookManager.mostRecentBook())
|
||||||
this.close();
|
this.close();
|
||||||
@@ -315,11 +337,11 @@ class RecentBooksPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$emit('recent-books-toggle');
|
this.$emit('recent-books-close');
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -329,7 +351,51 @@ class RecentBooksPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.desc {
|
.recent-books-table {
|
||||||
|
width: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.td-mp {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 4px 4px 4px 4px !important;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-mp {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0;
|
||||||
|
border-left: 1px solid #ddd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-word {
|
||||||
|
line-height: 180%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.recent-books-table .q-table__middle {
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-books-table thead tr:first-child th {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
background-color: #c1f4cd;
|
||||||
|
}
|
||||||
|
.recent-books-table tr:nth-child(even) {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -8,15 +8,19 @@
|
|||||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||||
|
|
||||||
<div v-show="!initStep" class="input">
|
<div v-show="!initStep" class="input">
|
||||||
<input ref="input" class="el-input__inner"
|
<!--input ref="input"
|
||||||
placeholder="что ищем"
|
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 style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button-group v-show="!initStep" class="button-group">
|
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
|
||||||
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
|
||||||
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
<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>
|
||||||
</el-button-group>
|
</q-btn-group>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,7 +43,10 @@ export default @Component({
|
|||||||
|
|
||||||
},
|
},
|
||||||
foundText: function(newValue) {
|
foundText: function(newValue) {
|
||||||
this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
|
//недостатки сторонних ui
|
||||||
|
const el = this.$refs.input.$el.querySelector('label div div div input');
|
||||||
|
if (el)
|
||||||
|
el.style.paddingRight = newValue.length*12 + 'px';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -157,16 +164,17 @@ class SearchPage extends Vue {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.stopInit = true;
|
this.stopInit = true;
|
||||||
this.$emit('search-toggle');
|
this.$emit('do-action', {action: 'search'});
|
||||||
|
}
|
||||||
|
|
||||||
|
inputKeyDown(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.showNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
//недостатки сторонних ui
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
|
|
||||||
this.showNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -194,17 +202,14 @@ class SearchPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
width: 150px;
|
width: 100px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: 37px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button {
|
.button {
|
||||||
padding: 9px 17px 9px 17px;
|
padding: 9px 17px 9px 17px;
|
||||||
width: 55px;
|
width: 50px;
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div class="hidden"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -35,6 +35,9 @@ export default @Component({
|
|||||||
currentProfile: function() {
|
currentProfile: function() {
|
||||||
this.currentProfileChanged(true);
|
this.currentProfileChanged(true);
|
||||||
},
|
},
|
||||||
|
libs: function() {
|
||||||
|
this.debouncedSaveLibs();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
class ServerStorage extends Vue {
|
class ServerStorage extends Vue {
|
||||||
@@ -49,27 +52,32 @@ class ServerStorage extends Vue {
|
|||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
this.debouncedSaveLibs = _.debounce(() => {
|
||||||
|
this.saveLibs();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
this.debouncedNotifySuccess = _.debounce(() => {
|
this.debouncedNotifySuccess = _.debounce(() => {
|
||||||
this.success('Данные синхронизированы с сервером');
|
this.success('Данные синхронизированы с сервером');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
this.oldProfiles = {};
|
this.oldProfiles = {};
|
||||||
this.oldSettings = {};
|
this.oldSettings = {};
|
||||||
|
this.oldLibs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
this.cachedRecent = await ssCacheStore.getItem('recent');
|
this.cachedRecent = await ssCacheStore.getItem('recent');
|
||||||
if (!this.cachedRecent)
|
if (!this.cachedRecent)
|
||||||
await this.setCachedRecent({rev: 0, data: {}});
|
await this.cleanCachedRecent('cachedRecent');
|
||||||
|
|
||||||
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
||||||
if (!this.cachedRecentPatch)
|
if (!this.cachedRecentPatch)
|
||||||
await this.setCachedRecentPatch({rev: 0, data: {}});
|
await this.cleanCachedRecent('cachedRecentPatch');
|
||||||
|
|
||||||
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
||||||
if (!this.cachedRecentMod)
|
if (!this.cachedRecentMod)
|
||||||
await this.setCachedRecentMod({rev: 0, data: {}});
|
await this.cleanCachedRecent('cachedRecentMod');
|
||||||
|
|
||||||
if (!this.serverStorageKey) {
|
if (!this.serverStorageKey) {
|
||||||
//генерируем новый ключ
|
//генерируем новый ключ
|
||||||
@@ -97,6 +105,15 @@ class ServerStorage extends Vue {
|
|||||||
this.cachedRecentMod = value;
|
this.cachedRecentMod = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cleanCachedRecent(whatToClean) {
|
||||||
|
if (whatToClean == 'cachedRecent' || whatToClean == 'all')
|
||||||
|
await this.setCachedRecent({rev: 0, data: {}});
|
||||||
|
if (whatToClean == 'cachedRecentPatch' || whatToClean == 'all')
|
||||||
|
await this.setCachedRecentPatch({rev: 0, data: {}});
|
||||||
|
if (whatToClean == 'cachedRecentMod' || whatToClean == 'all')
|
||||||
|
await this.setCachedRecentMod({rev: 0, data: {}});
|
||||||
|
}
|
||||||
|
|
||||||
async generateNewServerStorageKey() {
|
async generateNewServerStorageKey() {
|
||||||
const key = utils.toBase58(utils.randomArray(32));
|
const key = utils.toBase58(utils.randomArray(32));
|
||||||
this.commit('reader/setServerStorageKey', key);
|
this.commit('reader/setServerStorageKey', key);
|
||||||
@@ -124,11 +141,16 @@ class ServerStorage extends Vue {
|
|||||||
await this.loadProfiles(force);
|
await this.loadProfiles(force);
|
||||||
this.checkCurrentProfile();
|
this.checkCurrentProfile();
|
||||||
await this.currentProfileChanged(force);
|
await this.currentProfileChanged(force);
|
||||||
|
await this.loadLibs(force);
|
||||||
|
|
||||||
|
if (force)
|
||||||
|
await this.cleanCachedRecent('all');
|
||||||
const loadSuccess = await this.loadRecent();
|
const loadSuccess = await this.loadRecent();
|
||||||
if (loadSuccess && force)
|
if (loadSuccess && force) {
|
||||||
await this.saveRecent();
|
await this.saveRecent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async currentProfileChanged(force) {
|
async currentProfileChanged(force) {
|
||||||
if (!this.currentProfile)
|
if (!this.currentProfile)
|
||||||
@@ -169,6 +191,14 @@ class ServerStorage extends Vue {
|
|||||||
return this.settings.showServerStorageMessages;
|
return this.settings.showServerStorageMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get libs() {
|
||||||
|
return this.$store.state.reader.libs;
|
||||||
|
}
|
||||||
|
|
||||||
|
get libsRev() {
|
||||||
|
return this.$store.state.reader.libsRev;
|
||||||
|
}
|
||||||
|
|
||||||
checkCurrentProfile() {
|
checkCurrentProfile() {
|
||||||
if (!this.profiles[this.currentProfile]) {
|
if (!this.profiles[this.currentProfile]) {
|
||||||
this.commit('reader/setCurrentProfile', '');
|
this.commit('reader/setCurrentProfile', '');
|
||||||
@@ -177,17 +207,17 @@ class ServerStorage extends Vue {
|
|||||||
|
|
||||||
success(message) {
|
success(message) {
|
||||||
if (this.showServerStorageMessages)
|
if (this.showServerStorageMessages)
|
||||||
this.$notify.success({message});
|
this.$root.notify.success(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
warning(message) {
|
warning(message) {
|
||||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||||
this.$notify.warning({message});
|
this.$root.notify.warning(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message) {
|
error(message) {
|
||||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||||
this.$notify.error({message});
|
this.$root.notify.error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(force = false, doNotifySuccess = true) {
|
async loadSettings(force = false, doNotifySuccess = true) {
|
||||||
@@ -345,6 +375,78 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadLibs(force = false, doNotifySuccess = true) {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const oldRev = this.libsRev;
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
if (!force) {
|
||||||
|
try {
|
||||||
|
const revs = await this.storageCheck({libs: {}});
|
||||||
|
if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let libs = null;
|
||||||
|
try {
|
||||||
|
libs = await this.storageGet({libs: {}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libs.state == 'success') {
|
||||||
|
libs = libs.items.libs;
|
||||||
|
|
||||||
|
if (libs.rev == 0)
|
||||||
|
libs.data = {};
|
||||||
|
|
||||||
|
this.oldLibs = _.cloneDeep(libs.data);
|
||||||
|
this.commit('reader/setLibs', libs.data);
|
||||||
|
this.commit('reader/setLibsRev', libs.rev);
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${libs.state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveLibs() {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || this.savingLibs)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const diff = utils.getObjDiff(this.oldLibs, this.libs);
|
||||||
|
if (utils.isEmptyObjDiff(diff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.savingLibs = true;
|
||||||
|
try {
|
||||||
|
let result = {state: ''};
|
||||||
|
try {
|
||||||
|
result = await this.storageSet({libs: {rev: this.libsRev + 1, data: this.libs}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
await this.loadLibs(true, false);
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
this.oldLibs = _.cloneDeep(this.libs);
|
||||||
|
this.commit('reader/setLibsRev', this.libsRev + 1);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.savingLibs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
|
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
|
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
|
||||||
return;
|
return;
|
||||||
@@ -403,7 +505,7 @@ class ServerStorage extends Vue {
|
|||||||
|
|
||||||
const md = newRecentMod.data;
|
const md = newRecentMod.data;
|
||||||
if (md.key && result[md.key])
|
if (md.key && result[md.key])
|
||||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, true);
|
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||||
|
|
||||||
if (!bookManager.loaded) {
|
if (!bookManager.loaded) {
|
||||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||||
@@ -467,7 +569,7 @@ class ServerStorage extends Vue {
|
|||||||
|
|
||||||
let applyMod = this.cachedRecentMod.data;
|
let applyMod = this.cachedRecentMod.data;
|
||||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true);
|
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||||
|
|
||||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
needSaveRecentPatch = true;
|
needSaveRecentPatch = true;
|
||||||
|
|||||||
@@ -4,8 +4,15 @@
|
|||||||
Установить позицию
|
Установить позицию
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="slider">
|
<div id="set-position-slider" class="slider q-px-md">
|
||||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
<q-slider
|
||||||
|
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||||
|
v-model="sliderValue"
|
||||||
|
:max="sliderMax"
|
||||||
|
label
|
||||||
|
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,21 +53,17 @@ class SetPositionPage extends Vue {
|
|||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTooltip(val) {
|
|
||||||
if (this.sliderMax)
|
|
||||||
return (val/this.sliderMax*100).toFixed(2) + '%';
|
|
||||||
else
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$emit('set-position-toggle');
|
this.$emit('set-position-toggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
|
if (event.type == 'keydown') {
|
||||||
|
const action = this.$root.readerActionByKeyEvent(event);
|
||||||
|
if (event.key == 'Escape' || action == 'setPosition') {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,9 +76,13 @@ class SetPositionPage extends Vue {
|
|||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.el-slider {
|
|
||||||
margin-right: 20px;
|
<style>
|
||||||
margin-left: 20px;
|
#set-position-slider .q-slider__thumb path {
|
||||||
}
|
fill: white !important;
|
||||||
|
stroke: blue !important;
|
||||||
|
stroke-width: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table col column no-wrap">
|
||||||
|
<!-- header -->
|
||||||
|
<div class="table-row row">
|
||||||
|
<div class="desc q-pa-sm bg-blue-2">Команда</div>
|
||||||
|
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
|
||||||
|
<div style="width: 80px">Сочетание клавиш</div>
|
||||||
|
<q-input ref="input" class="q-ml-sm col"
|
||||||
|
outlined dense rounded
|
||||||
|
bg-color="grey-4"
|
||||||
|
placeholder="Найти"
|
||||||
|
v-model="search"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div v-show="!readonly" class="q-ml-sm column justify-center">
|
||||||
|
<q-btn class="bg-grey-4 text-grey-6" style="height: 35px; width: 35px" rounded flat icon="la la-broom" @click="defaultHotKeyAll">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Установить все сочетания по умолчанию
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- body -->
|
||||||
|
<div class="table-row row" v-for="(action, index) in tableData" :key="index">
|
||||||
|
<div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div>
|
||||||
|
<div class="hotKeys col q-pa-sm">
|
||||||
|
<q-chip
|
||||||
|
:color="collisions[code] ? 'red' : 'grey-7'"
|
||||||
|
:removable="!readonly" :clickable="collisions[code] ? true : false"
|
||||||
|
text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)"
|
||||||
|
@click="collisionWarning(code)"
|
||||||
|
>
|
||||||
|
{{ code }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
<div v-show="!readonly" class="column q-pa-xs">
|
||||||
|
<q-icon
|
||||||
|
name="la la-plus-circle"
|
||||||
|
class="button bg-green-8 text-white"
|
||||||
|
@click="addHotKey(action)"
|
||||||
|
v-ripple
|
||||||
|
:disabled="value[action].length >= maxCodesLength"
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Добавить сочетание клавиш
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
<q-icon
|
||||||
|
name="la la-broom"
|
||||||
|
class="button text-grey-5"
|
||||||
|
@click="defaultHotKey(action)"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
По умолчанию
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import rstore from '../../../../store/modules/reader';
|
||||||
|
//import * as utils from '../../share/utils';
|
||||||
|
|
||||||
|
const UserHotKeysProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: Object,
|
||||||
|
readonly: Boolean,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
watch: {
|
||||||
|
search: function() {
|
||||||
|
this.updateTableData();
|
||||||
|
},
|
||||||
|
value: function() {
|
||||||
|
this.checkCollisions();
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class UserHotKeys extends UserHotKeysProps {
|
||||||
|
search = '';
|
||||||
|
rstore = {};
|
||||||
|
tableData = [];
|
||||||
|
collisions = {};
|
||||||
|
maxCodesLength = 10;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.rstore = rstore;
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.checkCollisions();
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTableData() {
|
||||||
|
let result = rstore.hotKeys.map(hk => hk.name).filter(name => (this.mode == 'liberama.top' || name != 'libs'));
|
||||||
|
|
||||||
|
const search = this.search.toLowerCase();
|
||||||
|
const codesIncludeSearch = (action) => {
|
||||||
|
for (const code of this.value[action]) {
|
||||||
|
if (code.toLowerCase().includes(search))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result = result.filter(item => {
|
||||||
|
return !search ||
|
||||||
|
rstore.readerActions[item].toLowerCase().includes(search) ||
|
||||||
|
codesIncludeSearch(item)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tableData = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCollisions() {
|
||||||
|
const cols = {};
|
||||||
|
for (const [action, codes] of Object.entries(this.value)) {
|
||||||
|
codes.forEach(code => {
|
||||||
|
if (!cols[code])
|
||||||
|
cols[code] = [];
|
||||||
|
if (cols[code].indexOf(action) < 0)
|
||||||
|
cols[code].push(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const [code, actions] of Object.entries(cols)) {
|
||||||
|
if (actions.length > 1)
|
||||||
|
result[code] = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collisions = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
collisionWarning(code) {
|
||||||
|
if (this.collisions[code]) {
|
||||||
|
const descs = this.collisions[code].map(action => `<b>${rstore.readerActions[action]}</b>`);
|
||||||
|
this.$root.stdDialog.alert(`Сочетание '${code}' одновременно назначено<br>следующим командам:<br>${descs.join('<br>')}<br><br>
|
||||||
|
Возможно неожиданное поведение.`, 'Предупреждение');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCode(action, code) {
|
||||||
|
let codes = Array.from(this.value[action]);
|
||||||
|
const index = codes.indexOf(code);
|
||||||
|
if (index >= 0) {
|
||||||
|
codes.splice(index, 1);
|
||||||
|
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addHotKey(action) {
|
||||||
|
if (this.value[action].length >= this.maxCodesLength)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
|
||||||
|
if (result) {
|
||||||
|
let codes = Array.from(this.value[action]);
|
||||||
|
if (codes.indexOf(result) < 0) {
|
||||||
|
codes.push(result);
|
||||||
|
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.collisionWarning(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async defaultHotKey(action) {
|
||||||
|
try {
|
||||||
|
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
|
||||||
|
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
|
||||||
|
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async defaultHotKeyAll() {
|
||||||
|
try {
|
||||||
|
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
|
||||||
|
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table {
|
||||||
|
border-left: 1px solid grey;
|
||||||
|
border-top: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
border-right: 1px solid grey;
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:nth-child(even) {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
width: 130px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotKeys {
|
||||||
|
border-left: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: 25px;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const defPalette = [
|
||||||
|
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
|
||||||
|
'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
|
||||||
|
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
|
||||||
|
'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
|
||||||
|
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
|
||||||
|
'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
|
||||||
|
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
|
||||||
|
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
|
||||||
|
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
|
||||||
|
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
|
||||||
|
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
|
||||||
|
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
|
||||||
|
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defPalette;
|
||||||
10
client/components/Reader/SettingsPage/include/ButtonsTab.inc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="part-header">Показывать кнопки панели</div>
|
||||||
|
|
||||||
|
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||||
|
<div class="label-3"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox size="xs" @input="changeShowToolButton(item.name)"
|
||||||
|
:value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<div class="bg-grey-3 row">
|
||||||
|
<q-tabs
|
||||||
|
v-model="selectedKeysTab"
|
||||||
|
active-color="black"
|
||||||
|
active-bg-color="white"
|
||||||
|
indicator-color="white"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
|
>
|
||||||
|
<q-tab name="mouse" label="Мышь/тачскрин" />
|
||||||
|
<q-tab name="keyboard" label="Клавиатура" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mb-sm"/>
|
||||||
|
|
||||||
|
<div class="col tab-panel">
|
||||||
|
<div v-if="selectedKeysTab == 'mouse'">
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-4"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedKeysTab == 'keyboard'">
|
||||||
|
<div class="item row">
|
||||||
|
<UserHotKeys v-model="userHotKeys" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
@@ -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
@@ -0,0 +1,101 @@
|
|||||||
|
<div class="part-header">Управление синхронизацией данных</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="serverSyncEnabled">
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Профили устройств</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div class="text col">
|
||||||
|
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
|
||||||
|
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1">Устройство</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
||||||
|
style="width: 275px"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" dense no-caps @click="addProfile">Добавить</q-btn>
|
||||||
|
<q-btn class="button" dense no-caps @click="delProfile">Удалить</q-btn>
|
||||||
|
<q-btn class="button" dense no-caps @click="delAllProfiles">Удалить все</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Ключ доступа</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div class="text col">
|
||||||
|
Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
|
||||||
|
Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" style="width: 250px" dense no-caps @click="showServerStorageKey">
|
||||||
|
<span v-show="serverStorageKeyVisible">Скрыть</span>
|
||||||
|
<span v-show="!serverStorageKeyVisible">Показать</span>
|
||||||
|
ключ доступа
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div v-if="!serverStorageKeyVisible" class="col">
|
||||||
|
<hr/>
|
||||||
|
<b>{{ partialStorageKey }}</b> (часть вашего ключа)
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="col" style="line-height: 100%">
|
||||||
|
<hr/>
|
||||||
|
<div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
|
||||||
|
<b>{{ serverStorageKey }}</b>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
<div v-if="mode == 'omnireader' || mode == 'liberama.top'">
|
||||||
|
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
|
||||||
|
<br><div class="text-center" style="margin-top: 5px">
|
||||||
|
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div class="text col">
|
||||||
|
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
|
||||||
|
например, после переустановки ОС или чистки/смены браузера.<br>
|
||||||
|
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
|
||||||
|
и шифруются ключом доступа перед отправкой на сервер.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="item row">
|
||||||
|
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
||||||
|
</div>
|
||||||
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
@@ -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
@@ -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>
|
||||||
@@ -21,11 +21,12 @@
|
|||||||
@wheel.prevent.stop="onMouseWheel"
|
@wheel.prevent.stop="onMouseWheel"
|
||||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||||
oncontextmenu="return false;">
|
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>
|
@click.prevent.stop="onStatusBarClick"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"></div>
|
@click.prevent.stop="onStatusBarClick">
|
||||||
|
</div>
|
||||||
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
|
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
|
||||||
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||||
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||||
@@ -38,8 +39,8 @@ import Vue from 'vue';
|
|||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import {loadCSS} from 'fg-loadcss';
|
import {loadCSS} from 'fg-loadcss';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {sleep} from '../../../share/utils';
|
|
||||||
|
|
||||||
|
import {sleep} from '../../../share/utils';
|
||||||
import bookManager from '../share/bookManager';
|
import bookManager from '../share/bookManager';
|
||||||
import DrawHelper from './DrawHelper';
|
import DrawHelper from './DrawHelper';
|
||||||
import rstore from '../../../store/modules/reader';
|
import rstore from '../../../store/modules/reader';
|
||||||
@@ -131,7 +132,11 @@ class TextPage extends Vue {
|
|||||||
await this.doPageAnimation();
|
await this.doPageAnimation();
|
||||||
}, 10);
|
}, 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() {
|
mounted() {
|
||||||
@@ -242,6 +247,9 @@ class TextPage extends Vue {
|
|||||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||||
this.parsed.compactTextPerc = this.compactTextPerc;
|
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||||
|
|
||||||
|
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
|
||||||
|
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
//scrolling page
|
//scrolling page
|
||||||
@@ -268,25 +276,18 @@ class TextPage extends Vue {
|
|||||||
async checkLoadedFonts() {
|
async checkLoadedFonts() {
|
||||||
let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
|
let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
|
||||||
if (loaded.some(r => !r)) {
|
if (loaded.some(r => !r)) {
|
||||||
loaded = await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||||
if (loaded.some(r => !r.length))
|
|
||||||
throw new Error('some font not loaded');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadFonts() {
|
async loadFonts() {
|
||||||
this.fontsLoading = true;
|
this.fontsLoading = true;
|
||||||
|
|
||||||
let inst = null;
|
let close = null;
|
||||||
(async() => {
|
(async() => {
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
if (this.fontsLoading)
|
if (this.fontsLoading)
|
||||||
inst = this.$notify({
|
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
||||||
title: '',
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
message: 'Загрузка шрифта <i class="el-icon-loading"></i>',
|
|
||||||
duration: 0
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (!this.fontsLoaded)
|
if (!this.fontsLoaded)
|
||||||
@@ -298,29 +299,15 @@ class TextPage extends Vue {
|
|||||||
this.fontsLoaded[this.fontCssUrl] = 1;
|
this.fontsLoaded[this.fontCssUrl] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitingTime = 10*1000;
|
|
||||||
const delay = 100;
|
|
||||||
let i = 0;
|
|
||||||
//ждем шрифты
|
|
||||||
while (i < waitingTime/delay) {
|
|
||||||
i++;
|
|
||||||
try {
|
try {
|
||||||
await this.checkLoadedFonts();
|
await this.checkLoadedFonts();
|
||||||
i = waitingTime;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await sleep(delay);
|
this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i !== waitingTime) {
|
|
||||||
this.$notify.error({
|
|
||||||
title: 'Ошибка загрузки',
|
|
||||||
message: 'Некоторые шрифты не удалось загрузить'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fontsLoading = false;
|
this.fontsLoading = false;
|
||||||
if (inst)
|
if (close)
|
||||||
inst.close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings() {
|
getSettings() {
|
||||||
@@ -351,11 +338,15 @@ class TextPage extends Vue {
|
|||||||
// ширина шрифта некоторое время выдается неверно, поэтому
|
// ширина шрифта некоторое время выдается неверно, поэтому
|
||||||
if (!omitLoadFonts) {
|
if (!omitLoadFonts) {
|
||||||
const parsed = this.parsed;
|
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);
|
await sleep(100);
|
||||||
|
|
||||||
if (this.parsed === parsed) {
|
if (this.parsed === parsed) {
|
||||||
parsed.force = true;
|
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||||
this.draw();
|
this.draw();
|
||||||
parsed.force = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,7 +423,7 @@ class TextPage extends Vue {
|
|||||||
if (this.lazyParseEnabled)
|
if (this.lazyParseEnabled)
|
||||||
this.lazyParsePara();
|
this.lazyParsePara();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@@ -457,13 +448,13 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onResize() {
|
async onResize() {
|
||||||
/*this.page1 = null;
|
try {
|
||||||
this.page2 = null;
|
|
||||||
this.statusBar = null;*/
|
|
||||||
|
|
||||||
this.calcDrawProps();
|
this.calcDrawProps();
|
||||||
this.setBackground();
|
this.setBackground();
|
||||||
this.draw();
|
this.draw();
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
@@ -513,7 +504,7 @@ class TextPage extends Vue {
|
|||||||
async startTextScrolling() {
|
async startTextScrolling() {
|
||||||
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
||||||
this.linesDown.length <= this.pageLineCount) {
|
this.linesDown.length <= this.pageLineCount) {
|
||||||
this.$emit('stop-scrolling');
|
this.doStopScrolling();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +545,7 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
this.resolveTransition1Finish = null;
|
this.resolveTransition1Finish = null;
|
||||||
this.doingScrolling = false;
|
this.doingScrolling = false;
|
||||||
this.$emit('stop-scrolling');
|
this.doStopScrolling();
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,7 +839,7 @@ class TextPage extends Vue {
|
|||||||
let i = this.pageLineCount;
|
let i = this.pageLineCount;
|
||||||
if (this.keepLastToFirst)
|
if (this.keepLastToFirst)
|
||||||
i--;
|
i--;
|
||||||
if (i >= 0 && this.linesDown.length >= 2*i) {
|
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = true;
|
this.pageChangeDirectionDown = true;
|
||||||
this.bookPos = this.linesDown[i].begin;
|
this.bookPos = this.linesDown[i].begin;
|
||||||
@@ -893,24 +884,27 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doToolBarToggle() {
|
doToolBarToggle(event) {
|
||||||
this.$emit('tool-bar-toggle');
|
this.$emit('do-action', {action: 'switchToolbar', event});
|
||||||
}
|
}
|
||||||
|
|
||||||
doScrollingToggle() {
|
doScrollingToggle() {
|
||||||
this.$emit('scrolling-toggle');
|
this.$emit('do-action', {action: 'scrolling', event});
|
||||||
}
|
}
|
||||||
|
|
||||||
doFullScreenToggle() {
|
doFullScreenToggle() {
|
||||||
this.$emit('full-screen-toogle');
|
this.$emit('do-action', {action: 'fullScreen', event});
|
||||||
|
}
|
||||||
|
|
||||||
|
doStopScrolling() {
|
||||||
|
this.$emit('do-action', {action: 'stopScrolling', event});
|
||||||
}
|
}
|
||||||
|
|
||||||
async doFontSizeInc() {
|
async doFontSizeInc() {
|
||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newSize = (this.settings.fontSize + 1 < 100 ? this.settings.fontSize + 1 : 100);
|
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
||||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
this.commit('reader/setSettings', {fontSize: newSize});
|
||||||
this.commit('reader/setSettings', newSettings);
|
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
@@ -920,8 +914,7 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
||||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
this.commit('reader/setSettings', {fontSize: newSize});
|
||||||
this.commit('reader/setSettings', newSettings);
|
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
@@ -931,8 +924,7 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
||||||
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
|
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||||
this.commit('reader/setSettings', newSettings);
|
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
@@ -942,76 +934,12 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
||||||
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
|
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||||
this.commit('reader/setSettings', newSettings);
|
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
|
||||||
let result = false;
|
|
||||||
if (event.type == 'keydown' && !event.ctrlKey && !event.altKey) {
|
|
||||||
result = true;
|
|
||||||
switch (event.code) {
|
|
||||||
case 'ArrowDown':
|
|
||||||
if (event.shiftKey)
|
|
||||||
this.doScrollingSpeedUp();
|
|
||||||
else
|
|
||||||
this.doDown();
|
|
||||||
break;
|
|
||||||
case 'ArrowUp':
|
|
||||||
if (event.shiftKey)
|
|
||||||
this.doScrollingSpeedDown();
|
|
||||||
else
|
|
||||||
this.doUp();
|
|
||||||
break;
|
|
||||||
case 'PageDown':
|
|
||||||
case 'ArrowRight':
|
|
||||||
this.doPageDown();
|
|
||||||
break;
|
|
||||||
case 'Space':
|
|
||||||
if (event.shiftKey)
|
|
||||||
this.doPageUp();
|
|
||||||
else
|
|
||||||
this.doPageDown();
|
|
||||||
break;
|
|
||||||
case 'PageUp':
|
|
||||||
case 'ArrowLeft':
|
|
||||||
case 'Backspace':
|
|
||||||
this.doPageUp();
|
|
||||||
break;
|
|
||||||
case 'Home':
|
|
||||||
this.doHome();
|
|
||||||
break;
|
|
||||||
case 'End':
|
|
||||||
this.doEnd();
|
|
||||||
break;
|
|
||||||
case 'KeyA':
|
|
||||||
if (event.shiftKey)
|
|
||||||
this.doFontSizeDec();
|
|
||||||
else
|
|
||||||
this.doFontSizeInc();
|
|
||||||
break;
|
|
||||||
case 'Enter':
|
|
||||||
case 'Backquote'://`
|
|
||||||
case 'KeyF':
|
|
||||||
this.doFullScreenToggle();
|
|
||||||
break;
|
|
||||||
case 'Tab':
|
|
||||||
case 'KeyQ':
|
|
||||||
this.doToolBarToggle();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async startClickRepeat(pointX, pointY) {
|
async startClickRepeat(pointX, pointY) {
|
||||||
this.repX = pointX;
|
this.repX = pointX;
|
||||||
this.repY = pointY;
|
this.repY = pointY;
|
||||||
@@ -1089,7 +1017,7 @@ class TextPage extends Vue {
|
|||||||
//движение вправо
|
//движение вправо
|
||||||
this.doScrollingSpeedUp();
|
this.doScrollingSpeedUp();
|
||||||
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
|
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
|
||||||
this.doToolBarToggle();
|
this.doToolBarToggle(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startTouch = null;
|
this.startTouch = null;
|
||||||
@@ -1116,7 +1044,7 @@ class TextPage extends Vue {
|
|||||||
} else if (event.button == 1) {
|
} else if (event.button == 1) {
|
||||||
this.doScrollingToggle();
|
this.doScrollingToggle();
|
||||||
} else if (event.button == 2) {
|
} else if (event.button == 2) {
|
||||||
this.doToolBarToggle();
|
this.doToolBarToggle(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1141,7 +1069,7 @@ class TextPage extends Vue {
|
|||||||
if (url && url.indexOf('file://') != 0) {
|
if (url && url.indexOf('file://') != 0) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else {
|
} else {
|
||||||
this.$alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска', '', {type: 'warning'});
|
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,21 @@ export default class BookParser {
|
|||||||
let isFirstSection = true;
|
let isFirstSection = true;
|
||||||
let isFirstTitlePara = false;
|
let isFirstTitlePara = false;
|
||||||
|
|
||||||
|
//изображения
|
||||||
this.binary = {};
|
this.binary = {};
|
||||||
let binaryId = '';
|
let binaryId = '';
|
||||||
let binaryType = '';
|
let binaryType = '';
|
||||||
let dimPromises = [];
|
let dimPromises = [];
|
||||||
|
|
||||||
|
//оглавление
|
||||||
|
this.contents = [];
|
||||||
|
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
||||||
|
let curSubtitle = {paraIndex: -1, title: ''};
|
||||||
|
let inTitle = false;
|
||||||
|
let inSubtitle = false;
|
||||||
|
let sectionLevel = 0;
|
||||||
|
let bodyIndex = 0;
|
||||||
|
|
||||||
let paraIndex = -1;
|
let paraIndex = -1;
|
||||||
let paraOffset = 0;
|
let paraOffset = 0;
|
||||||
let para = []; /*array of
|
let para = []; /*array of
|
||||||
@@ -59,7 +69,6 @@ export default class BookParser {
|
|||||||
offset: Number, //сумма всех length до этого параграфа
|
offset: Number, //сумма всех length до этого параграфа
|
||||||
length: Number, //длина text без тегов
|
length: Number, //длина text без тегов
|
||||||
text: String, //текст параграфа с вложенными тегами
|
text: String, //текст параграфа с вложенными тегами
|
||||||
cut: Boolean, //параграф - кандидат на сокрытие (cutEmptyParagraphs)
|
|
||||||
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
|
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
@@ -116,10 +125,15 @@ export default class BookParser {
|
|||||||
offset: paraOffset,
|
offset: paraOffset,
|
||||||
length: len,
|
length: len,
|
||||||
text: text,
|
text: text,
|
||||||
cut: (!addIndex && (len == 1 && text[0] == ' ')),
|
|
||||||
addIndex: (addIndex ? addIndex : 0),
|
addIndex: (addIndex ? addIndex : 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (inSubtitle) {
|
||||||
|
curSubtitle.title += '<p>';
|
||||||
|
} else if (inTitle) {
|
||||||
|
curTitle.title += '<p>';
|
||||||
|
}
|
||||||
|
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
paraOffset += p.length;
|
||||||
};
|
};
|
||||||
@@ -131,11 +145,12 @@ export default class BookParser {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prevParaIndex = paraIndex;
|
||||||
let p = para[paraIndex];
|
let p = para[paraIndex];
|
||||||
//добавление пустых (addEmptyParagraphs) параграфов
|
paraOffset -= p.length;
|
||||||
|
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
|
||||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
||||||
paraIndex--;
|
paraIndex--;
|
||||||
paraOffset -= p.length;
|
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
newParagraph(' ', 1, i + 1);
|
newParagraph(' ', 1, i + 1);
|
||||||
}
|
}
|
||||||
@@ -144,20 +159,27 @@ export default class BookParser {
|
|||||||
p.index = paraIndex;
|
p.index = paraIndex;
|
||||||
p.offset = paraOffset;
|
p.offset = paraOffset;
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
paraOffset -= p.length;
|
if (curTitle.paraIndex == prevParaIndex)
|
||||||
//параграф оказался непустой
|
curTitle.paraIndex = paraIndex;
|
||||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
if (curSubtitle.paraIndex == prevParaIndex)
|
||||||
|
curSubtitle.paraIndex = paraIndex;
|
||||||
|
|
||||||
|
//уберем начальный пробел
|
||||||
p.length = 0;
|
p.length = 0;
|
||||||
p.text = p.text.substr(1);
|
p.text = p.text.substr(1);
|
||||||
p.cut = (len == 1 && text[0] == ' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.length += len;
|
p.length += len;
|
||||||
p.text += text;
|
p.text += text;
|
||||||
|
|
||||||
|
|
||||||
|
if (inSubtitle) {
|
||||||
|
curSubtitle.title += text;
|
||||||
|
} else if (inTitle) {
|
||||||
|
curTitle.title += text;
|
||||||
|
}
|
||||||
|
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
paraOffset += p.length;
|
||||||
};
|
};
|
||||||
@@ -167,7 +189,7 @@ export default class BookParser {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
tag = elemName;
|
tag = elemName;
|
||||||
path += '/' + elemName;
|
path += '/' + tag;
|
||||||
|
|
||||||
if (tag == 'binary') {
|
if (tag == 'binary') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
@@ -194,7 +216,7 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
if (tag == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
||||||
if (!fb2.author)
|
if (!fb2.author)
|
||||||
fb2.author = [];
|
fb2.author = [];
|
||||||
fb2.author.push({});
|
fb2.author.push({});
|
||||||
@@ -205,6 +227,7 @@ export default class BookParser {
|
|||||||
if (!isFirstBody)
|
if (!isFirstBody)
|
||||||
newParagraph(' ', 1);
|
newParagraph(' ', 1);
|
||||||
isFirstBody = false;
|
isFirstBody = false;
|
||||||
|
bodyIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'title') {
|
if (tag == 'title') {
|
||||||
@@ -212,12 +235,17 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = true;
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
|
|
||||||
|
inTitle = true;
|
||||||
|
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||||
|
this.contents.push(curTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'section') {
|
if (tag == 'section') {
|
||||||
if (!isFirstSection)
|
if (!isFirstSection)
|
||||||
newParagraph(' ', 1);
|
newParagraph(' ', 1);
|
||||||
isFirstSection = false;
|
isFirstSection = false;
|
||||||
|
sectionLevel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'emphasis' || tag == 'strong') {
|
||||||
@@ -238,9 +266,13 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = true;
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
|
|
||||||
|
inSubtitle = true;
|
||||||
|
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||||
|
curTitle.subtitles.push(curSubtitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'epigraph') {
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = true;
|
italic = true;
|
||||||
space += 1;
|
space += 1;
|
||||||
}
|
}
|
||||||
@@ -267,6 +299,11 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = false;
|
isFirstTitlePara = false;
|
||||||
bold = false;
|
bold = false;
|
||||||
center = false;
|
center = false;
|
||||||
|
inTitle = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'section') {
|
||||||
|
sectionLevel--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'emphasis' || tag == 'strong') {
|
||||||
@@ -281,11 +318,14 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = false;
|
isFirstTitlePara = false;
|
||||||
bold = false;
|
bold = false;
|
||||||
center = false;
|
center = false;
|
||||||
|
inSubtitle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'epigraph') {
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = false;
|
italic = false;
|
||||||
space -= 1;
|
space -= 1;
|
||||||
|
if (tag == 'annotation')
|
||||||
|
newParagraph(' ', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'stanza') {
|
if (tag == 'stanza') {
|
||||||
@@ -605,6 +645,7 @@ export default class BookParser {
|
|||||||
|
|
||||||
if (!this.force &&
|
if (!this.force &&
|
||||||
para.parsed &&
|
para.parsed &&
|
||||||
|
para.parsed.testWidth === this.testWidth &&
|
||||||
para.parsed.w === this.w &&
|
para.parsed.w === this.w &&
|
||||||
para.parsed.p === this.p &&
|
para.parsed.p === this.p &&
|
||||||
para.parsed.wordWrap === this.wordWrap &&
|
para.parsed.wordWrap === this.wordWrap &&
|
||||||
@@ -620,6 +661,7 @@ export default class BookParser {
|
|||||||
return para.parsed;
|
return para.parsed;
|
||||||
|
|
||||||
const parsed = {
|
const parsed = {
|
||||||
|
testWidth: this.testWidth,
|
||||||
w: this.w,
|
w: this.w,
|
||||||
p: this.p,
|
p: this.p,
|
||||||
wordWrap: this.wordWrap,
|
wordWrap: this.wordWrap,
|
||||||
@@ -631,10 +673,7 @@ export default class BookParser {
|
|||||||
imageHeightLines: this.imageHeightLines,
|
imageHeightLines: this.imageHeightLines,
|
||||||
imageFitWidth: this.imageFitWidth,
|
imageFitWidth: this.imageFitWidth,
|
||||||
compactTextPerc: this.compactTextPerc,
|
compactTextPerc: this.compactTextPerc,
|
||||||
visible: !(
|
visible: true, //вычисляется позже
|
||||||
(this.cutEmptyParagraphs && para.cut) ||
|
|
||||||
(para.addIndex > this.addEmptyParagraphs)
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -650,9 +689,12 @@ export default class BookParser {
|
|||||||
text: String,
|
text: String,
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
let parts = this.splitToStyle(para.text);
|
let parts = this.splitToStyle(para.text);
|
||||||
|
|
||||||
|
//инициализация парсера
|
||||||
let line = {begin: para.offset, parts: []};
|
let line = {begin: para.offset, parts: []};
|
||||||
|
let paragraphText = '';//текст параграфа
|
||||||
let partText = '';//накапливаемый кусок со стилем
|
let partText = '';//накапливаемый кусок со стилем
|
||||||
|
|
||||||
let str = '';//измеряемая строка
|
let str = '';//измеряемая строка
|
||||||
@@ -661,14 +703,16 @@ export default class BookParser {
|
|||||||
let style = {};
|
let style = {};
|
||||||
let ofs = 0;//смещение от начала параграфа para.offset
|
let ofs = 0;//смещение от начала параграфа para.offset
|
||||||
let imgW = 0;
|
let imgW = 0;
|
||||||
|
let imageInPara = false;
|
||||||
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
style = part.style;
|
style = part.style;
|
||||||
|
paragraphText += part.text;
|
||||||
|
|
||||||
//изображения
|
//изображения
|
||||||
if (part.image.id && !part.image.inline) {
|
if (part.image.id && !part.image.inline) {
|
||||||
parsed.visible = this.showImages;
|
imageInPara = true;
|
||||||
let bin = this.binary[part.image.id];
|
let bin = this.binary[part.image.id];
|
||||||
if (!bin)
|
if (!bin)
|
||||||
bin = {h: 1, w: 1};
|
bin = {h: 1, w: 1};
|
||||||
@@ -835,6 +879,16 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//parsed.visible
|
||||||
|
if (imageInPara) {
|
||||||
|
parsed.visible = this.showImages;
|
||||||
|
} else {
|
||||||
|
parsed.visible = !(
|
||||||
|
(para.addIndex > this.addEmptyParagraphs) ||
|
||||||
|
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
parsed.lines = lines;
|
parsed.lines = lines;
|
||||||
para.parsed = parsed;
|
para.parsed = parsed;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,123 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
showUntil: '2020-11-12',
|
||||||
|
header: '0.9.8 (2020-11-13)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено окно "Оглавление/закладки"</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-11-11',
|
||||||
|
header: '0.9.7 (2020-11-12)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-11-05',
|
||||||
|
header: '0.9.6 (2020-11-06)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>завершена работа над новым окном "Библиотека"</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-10-31',
|
||||||
|
header: '0.9.5 (2020-11-01)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-10-28',
|
||||||
|
header: '0.9.4 (2020-10-29)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
|
||||||
|
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-05-20',
|
||||||
|
header: '0.9.3 (2020-05-21)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-04-25',
|
||||||
|
header: '0.9.2 (2020-03-15)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
|
||||||
|
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-03-02',
|
||||||
|
header: '0.9.1 (2020-03-03)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>улучшение работы серверной части</li>
|
||||||
|
<li>незначительные изменения интерфейса</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-02-25',
|
||||||
|
header: '0.9.0 (2020-02-26)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>переход на UI-фреймфорк Quasar</li>
|
||||||
|
<li>незначительные изменения интерфейса</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-02-05',
|
||||||
|
header: '0.8.4 (2020-02-06)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлен paypal-адрес для пожертвований</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
showUntil: '2020-01-27',
|
showUntil: '2020-01-27',
|
||||||
header: '0.8.3 (2020-01-28)',
|
header: '0.8.3 (2020-01-28)',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Settings в разработке
|
Раздел Settings в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div>
|
||||||
Раздел Sources в разработке
|
Раздел Sources в разработке
|
||||||
</el-container>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
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>
|
||||||
59
client/components/share/Notify.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hidden"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
})
|
||||||
|
class Notify extends Vue {
|
||||||
|
notify(opts) {
|
||||||
|
let {
|
||||||
|
caption = null,
|
||||||
|
captionColor = 'black',
|
||||||
|
color = 'positive',
|
||||||
|
icon = '',
|
||||||
|
iconColor = 'white',
|
||||||
|
message = '',
|
||||||
|
messageColor = 'black',
|
||||||
|
position = 'top-right',
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
|
||||||
|
return this.$q.notify({
|
||||||
|
position,
|
||||||
|
color,
|
||||||
|
textColor: iconColor,
|
||||||
|
icon,
|
||||||
|
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
|
||||||
|
html: true,
|
||||||
|
|
||||||
|
message:
|
||||||
|
`<div style="max-width: 350px;">
|
||||||
|
${caption}
|
||||||
|
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message, caption, options) {
|
||||||
|
this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(message, caption, options) {
|
||||||
|
this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message, caption, options) {
|
||||||
|
this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message, caption, options) {
|
||||||
|
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
185
client/components/share/NumInput.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<q-input outlined dense
|
||||||
|
v-model="filteredValue"
|
||||||
|
input-style="text-align: center"
|
||||||
|
class="no-mp"
|
||||||
|
:class="(error ? 'error' : '')"
|
||||||
|
:disable="disable"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon :class="(validate(value - step) ? '' : 'disable')"
|
||||||
|
name="la la-minus-circle"
|
||||||
|
class="button"
|
||||||
|
v-ripple="validate(value - step)"
|
||||||
|
@click="minus"
|
||||||
|
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
|
||||||
|
@mouseup.prevent.stop="onMouseUp"
|
||||||
|
@mouseout.prevent.stop="onMouseUp"
|
||||||
|
@touchstart.stop="onTouchStart($event, 'minus')"
|
||||||
|
@touchend.stop="onTouchEnd"
|
||||||
|
@touchcancel.prevent.stop="onTouchEnd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon :class="(validate(value + step) ? '' : 'disable')"
|
||||||
|
name="la la-plus-circle"
|
||||||
|
class="button"
|
||||||
|
v-ripple="validate(value + step)"
|
||||||
|
@click="plus"
|
||||||
|
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
|
||||||
|
@mouseup.prevent.stop="onMouseUp"
|
||||||
|
@mouseout.prevent.stop="onMouseUp"
|
||||||
|
@touchstart.stop="onTouchStart($event, 'plus')"
|
||||||
|
@touchend.stop="onTouchEnd"
|
||||||
|
@touchcancel.prevent.stop="onTouchEnd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
|
const NumInputProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: Number,
|
||||||
|
min: { type: Number, default: -Number.MAX_VALUE },
|
||||||
|
max: { type: Number, default: Number.MAX_VALUE },
|
||||||
|
step: { type: Number, default: 1 },
|
||||||
|
digits: { type: Number, default: 0 },
|
||||||
|
disable: Boolean
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
watch: {
|
||||||
|
filteredValue: function(newValue) {
|
||||||
|
if (this.validate(newValue)) {
|
||||||
|
this.error = false;
|
||||||
|
this.$emit('input', this.string2number(newValue));
|
||||||
|
} else {
|
||||||
|
this.error = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: function(newValue) {
|
||||||
|
this.filteredValue = newValue;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class NumInput extends NumInputProps {
|
||||||
|
filteredValue = 0;
|
||||||
|
error = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.filteredValue = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
string2number(value) {
|
||||||
|
return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(value) {
|
||||||
|
let n = this.string2number(value);
|
||||||
|
if (isNaN(n))
|
||||||
|
return false;
|
||||||
|
if (n < this.min)
|
||||||
|
return false;
|
||||||
|
if (n > this.max)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
plus() {
|
||||||
|
const newValue = this.value + this.step;
|
||||||
|
if (this.validate(newValue))
|
||||||
|
this.filteredValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
minus() {
|
||||||
|
const newValue = this.value - this.step;
|
||||||
|
if (this.validate(newValue))
|
||||||
|
this.filteredValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(event, way) {
|
||||||
|
this.startClickRepeat = true;
|
||||||
|
this.clickRepeat = false;
|
||||||
|
|
||||||
|
if (event.button == 0) {
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(300);
|
||||||
|
if (this.startClickRepeat) {
|
||||||
|
this.clickRepeat = true;
|
||||||
|
while (this.clickRepeat) {
|
||||||
|
if (way == 'plus') {
|
||||||
|
this.plus();
|
||||||
|
} else {
|
||||||
|
this.minus();
|
||||||
|
}
|
||||||
|
await utils.sleep(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp() {
|
||||||
|
if (this.inTouch)
|
||||||
|
return;
|
||||||
|
this.startClickRepeat = false;
|
||||||
|
this.clickRepeat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart(event, way) {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1) {
|
||||||
|
this.inTouch = true;
|
||||||
|
this.onMouseDown({button: 0}, way);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchEnd() {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
this.inTouch = false;
|
||||||
|
this.onMouseUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.no-mp {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: 130%;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #bbb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
color: #616161;
|
||||||
|
background-color: #efebe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ffabab;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disable, .disable:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #bbb;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
342
client/components/share/StdDialog.vue
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
|
||||||
|
<slot></slot>
|
||||||
|
|
||||||
|
<!--------------------------------------------------->
|
||||||
|
<div v-show="type == 'alert'" class="bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md">
|
||||||
|
<div v-html="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons row justify-end q-pa-md">
|
||||||
|
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--------------------------------------------------->
|
||||||
|
<div v-show="type == 'confirm'" class="bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md">
|
||||||
|
<div v-html="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons row justify-end q-pa-md">
|
||||||
|
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--------------------------------------------------->
|
||||||
|
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md">
|
||||||
|
<div v-html="message"></div>
|
||||||
|
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
|
||||||
|
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons row justify-end q-pa-md">
|
||||||
|
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--------------------------------------------------->
|
||||||
|
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md">
|
||||||
|
<div v-html="message"></div>
|
||||||
|
<div class="q-my-md text-center">
|
||||||
|
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
|
||||||
|
<div>{{ hotKeyCode }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons row justify-end q-pa-md">
|
||||||
|
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
watch: {
|
||||||
|
inputValue: function(newValue) {
|
||||||
|
this.validate(newValue);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class StdDialog extends Vue {
|
||||||
|
caption = '';
|
||||||
|
message = '';
|
||||||
|
active = false;
|
||||||
|
type = '';
|
||||||
|
inputValue = '';
|
||||||
|
error = '';
|
||||||
|
iconColor = '';
|
||||||
|
iconName = '';
|
||||||
|
hotKeyCode = '';
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.$root.addKeyHook) {
|
||||||
|
this.$root.addKeyHook(this.keyHook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(message, caption, opts) {
|
||||||
|
this.caption = caption;
|
||||||
|
this.message = message;
|
||||||
|
|
||||||
|
this.ok = false;
|
||||||
|
this.type = '';
|
||||||
|
this.inputValidator = null;
|
||||||
|
this.inputValue = '';
|
||||||
|
this.error = '';
|
||||||
|
this.showed = false;
|
||||||
|
|
||||||
|
this.iconColor = 'text-warning';
|
||||||
|
if (opts && opts.color) {
|
||||||
|
this.iconColor = `text-${opts.color}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iconName = 'las la-exclamation-circle';
|
||||||
|
if (opts && opts.iconName) {
|
||||||
|
this.iconName = opts.iconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hotKeyCode = '';
|
||||||
|
if (opts && opts.hotKeyCode) {
|
||||||
|
this.hotKeyCode = opts.hotKeyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
if (this.hideTrigger) {
|
||||||
|
this.hideTrigger();
|
||||||
|
this.hideTrigger = null;
|
||||||
|
}
|
||||||
|
this.showed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
if (this.type == 'prompt') {
|
||||||
|
this.enableValidator = true;
|
||||||
|
if (this.inputValue)
|
||||||
|
this.validate(this.inputValue);
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
this.showed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(value) {
|
||||||
|
if (!this.enableValidator)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.inputValidator) {
|
||||||
|
const result = this.inputValidator(value);
|
||||||
|
if (result !== true) {
|
||||||
|
this.error = result;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.error = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
okClick() {
|
||||||
|
if (this.type == 'prompt' && !this.validate(this.inputValue)) {
|
||||||
|
this.$refs.dialog.shake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type == 'hotKey' && this.hotKeyCode == '') {
|
||||||
|
this.$refs.dialog.shake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ok = true;
|
||||||
|
this.$refs.dialog.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(message, caption, opts) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.init(message, caption, opts);
|
||||||
|
|
||||||
|
this.hideTrigger = () => {
|
||||||
|
if (this.ok) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'alert';
|
||||||
|
this.active = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm(message, caption, opts) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.init(message, caption, opts);
|
||||||
|
|
||||||
|
this.hideTrigger = () => {
|
||||||
|
if (this.ok) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'confirm';
|
||||||
|
this.active = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt(message, caption, opts) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.enableValidator = false;
|
||||||
|
this.init(message, caption, opts);
|
||||||
|
|
||||||
|
this.hideTrigger = () => {
|
||||||
|
if (this.ok) {
|
||||||
|
resolve({value: this.inputValue});
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'prompt';
|
||||||
|
if (opts) {
|
||||||
|
this.inputValidator = opts.inputValidator || null;
|
||||||
|
this.inputValue = opts.inputValue || '';
|
||||||
|
}
|
||||||
|
this.active = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getHotKey(message, caption, opts) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.init(message, caption, opts);
|
||||||
|
|
||||||
|
this.hideTrigger = () => {
|
||||||
|
if (this.ok) {
|
||||||
|
resolve(this.hotKeyCode);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'hotKey';
|
||||||
|
this.active = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (this.active && this.showed) {
|
||||||
|
let handled = false;
|
||||||
|
if (this.type == 'hotKey') {
|
||||||
|
if (event.type == 'keydown') {
|
||||||
|
this.hotKeyCode = utils.keyEventToCode(event);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.okClick();
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key == 'Escape') {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.dialog.hide();
|
||||||
|
});
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
font-size: 110%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
height: 20px;
|
||||||
|
font-size: 80%;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||||
<div ref="windowBox" class="windowBox" @click.stop>
|
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
|
||||||
<div class="window">
|
<div ref="window" class="window flexfit column no-wrap">
|
||||||
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
|
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
|
||||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
||||||
<span class="header-text"><slot name="header"></slot></span>
|
<span class="header-text col"><slot name="header"></slot></span>
|
||||||
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
|
<slot name="buttons"></slot>
|
||||||
|
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,11 +26,15 @@ export default @Component({
|
|||||||
width: { type: String, default: '100%' },
|
width: { type: String, default: '100%' },
|
||||||
maxWidth: { type: String, default: '' },
|
maxWidth: { type: String, default: '' },
|
||||||
topShift: { type: Number, default: 0 },
|
topShift: { type: Number, default: 0 },
|
||||||
|
margin: '',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
class Window extends Vue {
|
class Window extends Vue {
|
||||||
init() {
|
init() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
this.$refs.main.style.top = 0;
|
||||||
|
this.$refs.main.style.left = 0;
|
||||||
|
|
||||||
this.$refs.windowBox.style.height = this.height;
|
this.$refs.windowBox.style.height = this.height;
|
||||||
this.$refs.windowBox.style.width = this.width;
|
this.$refs.windowBox.style.width = this.width;
|
||||||
if (this.maxWidth)
|
if (this.maxWidth)
|
||||||
@@ -38,6 +44,9 @@ class Window extends Vue {
|
|||||||
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
|
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
|
||||||
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
|
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
|
||||||
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
|
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
|
||||||
|
|
||||||
|
if (this.margin)
|
||||||
|
this.$refs.window.style.margin = this.margin;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,23 +125,20 @@ class Window extends Vue {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
.main {
|
||||||
position: absolute;
|
background-color: transparent !important;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.windowBox {
|
.xyfit {
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.window {
|
.flexfit {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
|
.window {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 3px double black;
|
border: 3px double black;
|
||||||
@@ -141,23 +147,21 @@ class Window extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
background: linear-gradient(to bottom right, green, #59B04F);
|
||||||
justify-content: flex-end;
|
|
||||||
background-color: #59B04F;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
flex: 1;
|
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
color: yellow;
|
||||||
|
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -166,4 +170,5 @@ class Window extends Vue {
|
|||||||
.close-button:hover {
|
.close-button:hover {
|
||||||
background-color: #69C05F;
|
background-color: #69C05F;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
/*
|
|
||||||
import ElementUI from 'element-ui';
|
|
||||||
import './theme/index.css';
|
|
||||||
import locale from 'element-ui/lib/locale/lang/ru-RU';
|
|
||||||
|
|
||||||
Vue.use(ElementUI, { locale });
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------
|
|
||||||
import './theme/index.css';
|
|
||||||
|
|
||||||
import ElMenu from 'element-ui/lib/menu';
|
|
||||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
|
||||||
import ElButton from 'element-ui/lib/button';
|
|
||||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
|
||||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
|
||||||
import ElTabs from 'element-ui/lib/tabs';
|
|
||||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
|
||||||
import ElTooltip from 'element-ui/lib/tooltip';
|
|
||||||
import ElRow from 'element-ui/lib/row';
|
|
||||||
import ElCol from 'element-ui/lib/col';
|
|
||||||
import ElContainer from 'element-ui/lib/container';
|
|
||||||
import ElAside from 'element-ui/lib/aside';
|
|
||||||
import ElHeader from 'element-ui/lib/header';
|
|
||||||
import ElMain from 'element-ui/lib/main';
|
|
||||||
import ElInput from 'element-ui/lib/input';
|
|
||||||
import ElInputNumber from 'element-ui/lib/input-number';
|
|
||||||
import ElSelect from 'element-ui/lib/select';
|
|
||||||
import ElOption from 'element-ui/lib/option';
|
|
||||||
import ElTable from 'element-ui/lib/table';
|
|
||||||
import ElTableColumn from 'element-ui/lib/table-column';
|
|
||||||
import ElProgress from 'element-ui/lib/progress';
|
|
||||||
import ElSlider from 'element-ui/lib/slider';
|
|
||||||
import ElForm from 'element-ui/lib/form';
|
|
||||||
import ElFormItem from 'element-ui/lib/form-item';
|
|
||||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
|
||||||
import ElDialog from 'element-ui/lib/dialog';
|
|
||||||
|
|
||||||
import Notification from 'element-ui/lib/notification';
|
|
||||||
import Loading from 'element-ui/lib/loading';
|
|
||||||
import MessageBox from 'element-ui/lib/message-box';
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
|
||||||
ElRow, ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
|
||||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
|
||||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
|
||||||
ElColorPicker, ElDialog,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let name in components) {
|
|
||||||
Vue.component(name, components[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Vue.use(Loading.directive);
|
|
||||||
|
|
||||||
Vue.prototype.$loading = Loading.service;
|
|
||||||
Vue.prototype.$msgbox = MessageBox;
|
|
||||||
Vue.prototype.$alert = MessageBox.alert;
|
|
||||||
Vue.prototype.$confirm = MessageBox.confirm;
|
|
||||||
Vue.prototype.$prompt = MessageBox.prompt;
|
|
||||||
Vue.prototype.$notify = Notification;
|
|
||||||
//Vue.prototype.$message = Message;
|
|
||||||
|
|
||||||
import lang from 'element-ui/lib/locale/lang/ru-RU';
|
|
||||||
import locale from 'element-ui/lib/locale';
|
|
||||||
locale.use(lang);
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html manifest="/app/manifest.appcache">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<title></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
||||||
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
||||||
<title></title>
|
<script src="/sw-register.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="https://yastatic.net/share2/share.js" async="async"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import Vue from 'vue';
|
|||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './element';
|
import './quasar';
|
||||||
|
|
||||||
|
import vueSanitize from 'vue-sanitize';
|
||||||
|
Vue.use(vueSanitize);
|
||||||
|
|
||||||
import App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
//Vue.config.productionTip = false;
|
//Vue.config.productionTip = false;
|
||||||
|
|||||||
94
client/quasar.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import 'quasar/dist/quasar.css';
|
||||||
|
import Quasar from 'quasar/src/vue-plugin.js'
|
||||||
|
|
||||||
|
//config
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
//components
|
||||||
|
//import {QLayout} from 'quasar/src/components/layout';
|
||||||
|
//import {QPageContainer, QPage} from 'quasar/src/components/page';
|
||||||
|
//import {QDrawer} from 'quasar/src/components/drawer';
|
||||||
|
|
||||||
|
import {QCircularProgress} from 'quasar/src/components/circular-progress';
|
||||||
|
import {QInput} from 'quasar/src/components/input';
|
||||||
|
import {QBtn} from 'quasar/src/components/btn';
|
||||||
|
import {QBtnGroup} from 'quasar/src/components/btn-group';
|
||||||
|
import {QBtnToggle} from 'quasar/src/components/btn-toggle';
|
||||||
|
import {QIcon} from 'quasar/src/components/icon';
|
||||||
|
import {QSlider} from 'quasar/src/components/slider';
|
||||||
|
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||||
|
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||||
|
import {QSeparator} from 'quasar/src/components/separator';
|
||||||
|
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||||
|
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||||
|
import {QSpinner} from 'quasar/src/components/spinner';
|
||||||
|
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||||
|
import {QCheckbox} from 'quasar/src/components/checkbox';
|
||||||
|
import {QSelect} from 'quasar/src/components/select';
|
||||||
|
import {QColor} from 'quasar/src/components/color';
|
||||||
|
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||||
|
import {QDialog} from 'quasar/src/components/dialog';
|
||||||
|
import {QChip} from 'quasar/src/components/chip';
|
||||||
|
import {QTree} from 'quasar/src/components/tree';
|
||||||
|
import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||||
|
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
//QLayout,
|
||||||
|
//QPageContainer, QPage,
|
||||||
|
//QDrawer,
|
||||||
|
|
||||||
|
QCircularProgress,
|
||||||
|
QInput,
|
||||||
|
QBtn,
|
||||||
|
QBtnGroup,
|
||||||
|
QBtnToggle,
|
||||||
|
QIcon,
|
||||||
|
QSlider,
|
||||||
|
QTabs, QTab,
|
||||||
|
//QTabPanels, QTabPanel,
|
||||||
|
QSeparator,
|
||||||
|
QList, QItem, QItemSection, QItemLabel,
|
||||||
|
QTooltip,
|
||||||
|
QSpinner,
|
||||||
|
QTable, QTh, QTr, QTd,
|
||||||
|
QCheckbox,
|
||||||
|
QSelect,
|
||||||
|
QColor,
|
||||||
|
QPopupProxy,
|
||||||
|
QDialog,
|
||||||
|
QChip,
|
||||||
|
QTree,
|
||||||
|
QExpansionItem,
|
||||||
|
};
|
||||||
|
|
||||||
|
//directives
|
||||||
|
import Ripple from 'quasar/src/directives/Ripple';
|
||||||
|
import ClosePopup from 'quasar/src/directives/ClosePopup';
|
||||||
|
|
||||||
|
const directives = {Ripple, ClosePopup};
|
||||||
|
|
||||||
|
//plugins
|
||||||
|
import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
|
||||||
|
import Notify from 'quasar/src/plugins/Notify';
|
||||||
|
|
||||||
|
const plugins = {
|
||||||
|
AppFullscreen,
|
||||||
|
Notify,
|
||||||
|
};
|
||||||
|
|
||||||
|
//use
|
||||||
|
Vue.use(Quasar, { config, components, directives, plugins });
|
||||||
|
|
||||||
|
//icons
|
||||||
|
//import '@quasar/extras/material-icons/material-icons.css';
|
||||||
|
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
|
||||||
|
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
|
||||||
|
|
||||||
|
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||||
|
|
||||||
|
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||||
|
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||||
|
Quasar.iconSet.set(lineAwesome);
|
||||||
@@ -2,25 +2,22 @@ import Vue from 'vue';
|
|||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
//немедленная загрузка
|
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
||||||
import CardIndex from './components/CardIndex/CardIndex.vue';
|
|
||||||
//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
|
||||||
|
|
||||||
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
||||||
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
||||||
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
||||||
const History = () => import('./components/CardIndex/History/History.vue');
|
const History = () => import('./components/CardIndex/History/History.vue');
|
||||||
|
|
||||||
//немедленная загрузка
|
//немедленная загрузка
|
||||||
//const Reader = () => import('./components/Reader/Reader.vue');
|
//import Reader from './components/Reader/Reader.vue';
|
||||||
import Reader from './components/Reader/Reader.vue';
|
const Reader = () => import('./components/Reader/Reader.vue');
|
||||||
|
const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
|
||||||
|
|
||||||
//const Forum = () => import('./components/Forum/Forum.vue');
|
|
||||||
const Income = () => import('./components/Income/Income.vue');
|
const Income = () => import('./components/Income/Income.vue');
|
||||||
const Sources = () => import('./components/Sources/Sources.vue');
|
const Sources = () => import('./components/Sources/Sources.vue');
|
||||||
const Settings = () => import('./components/Settings/Settings.vue');
|
const Settings = () => import('./components/Settings/Settings.vue');
|
||||||
const Help = () => import('./components/Help/Help.vue');
|
const Help = () => import('./components/Help/Help.vue');
|
||||||
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||||
|
|
||||||
const myRoutes = [
|
const myRoutes = [
|
||||||
['/', null, null, '/cardindex'],
|
['/', null, null, '/cardindex'],
|
||||||
@@ -33,10 +30,12 @@ const myRoutes = [
|
|||||||
['/cardindex~history', History],
|
['/cardindex~history', History],
|
||||||
|
|
||||||
['/reader', Reader],
|
['/reader', Reader],
|
||||||
|
['/external-libs', ExternalLibs],
|
||||||
['/income', Income],
|
['/income', Income],
|
||||||
['/sources', Sources],
|
['/sources', Sources],
|
||||||
['/settings', Settings],
|
['/settings', Settings],
|
||||||
['/help', Help],
|
['/help', Help],
|
||||||
|
['/404', NotFound404],
|
||||||
['*', null, null, '/cardindex'],
|
['*', null, null, '/cardindex'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -103,24 +103,48 @@ export function fromBase64(data) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getObjDiff(oldObj, newObj) {
|
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||||
|
const {
|
||||||
|
exclude = [],
|
||||||
|
excludeAdd = [],
|
||||||
|
excludeDel = [],
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const ex = new Set(exclude);
|
||||||
|
const exAdd = new Set(excludeAdd);
|
||||||
|
const exDel = new Set(excludeDel);
|
||||||
|
|
||||||
|
const makeObjDiff = (oldObj, newObj, keyPath) => {
|
||||||
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
||||||
|
|
||||||
|
keyPath = `${keyPath}${keyPath ? '/' : ''}`;
|
||||||
|
|
||||||
for (const key of Object.keys(oldObj)) {
|
for (const key of Object.keys(oldObj)) {
|
||||||
|
const kp = `${keyPath}${key}`;
|
||||||
|
|
||||||
if (newObj.hasOwnProperty(key)) {
|
if (newObj.hasOwnProperty(key)) {
|
||||||
|
if (ex.has(kp))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!_.isEqual(oldObj[key], newObj[key])) {
|
if (!_.isEqual(oldObj[key], newObj[key])) {
|
||||||
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
||||||
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
|
result.change[key] = makeObjDiff(oldObj[key], newObj[key], kp);
|
||||||
} else {
|
} else {
|
||||||
result.change[key] = _.cloneDeep(newObj[key]);
|
result.change[key] = _.cloneDeep(newObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (exDel.has(kp))
|
||||||
|
continue;
|
||||||
result.del.push(key);
|
result.del.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of Object.keys(newObj)) {
|
for (const key of Object.keys(newObj)) {
|
||||||
|
const kp = `${keyPath}${key}`;
|
||||||
|
if (exAdd.has(kp))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!oldObj.hasOwnProperty(key)) {
|
if (!oldObj.hasOwnProperty(key)) {
|
||||||
result.add[key] = _.cloneDeep(newObj[key]);
|
result.add[key] = _.cloneDeep(newObj[key]);
|
||||||
}
|
}
|
||||||
@@ -129,21 +153,57 @@ export function getObjDiff(oldObj, newObj) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return makeObjDiff(oldObj, newObj, '');
|
||||||
|
}
|
||||||
|
|
||||||
export function isObjDiff(diff) {
|
export function isObjDiff(diff) {
|
||||||
return (_.isObject(diff) && diff.__isDiff);
|
return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEmptyObjDiff(diff) {
|
export function isEmptyObjDiff(diff) {
|
||||||
return (!_.isObject(diff) || !diff.__isDiff ||
|
return (!isObjDiff(diff) ||
|
||||||
(!Object.keys(diff.change).length &&
|
!(Object.keys(diff.change).length ||
|
||||||
!Object.keys(diff.add).length &&
|
Object.keys(diff.add).length ||
|
||||||
!diff.del.length
|
diff.del.length
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyObjDiff(obj, diff, isAddChanged) {
|
export function isEmptyObjDiffDeep(diff, opts = {}) {
|
||||||
const result = _.cloneDeep(obj);
|
if (!isObjDiff(diff))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isApplyChange = true,
|
||||||
|
isApplyAdd = true,
|
||||||
|
isApplyDel = true,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
let notEmptyDeep = false;
|
||||||
|
const change = diff.change;
|
||||||
|
for (const key of Object.keys(change)) {
|
||||||
|
if (_.isObject(change[key]))
|
||||||
|
notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
|
||||||
|
else if (isApplyChange)
|
||||||
|
notEmptyDeep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(
|
||||||
|
notEmptyDeep ||
|
||||||
|
(isApplyAdd && Object.keys(diff.add).length) ||
|
||||||
|
(isApplyDel && diff.del.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyObjDiff(obj, diff, opts = {}) {
|
||||||
|
const {
|
||||||
|
isAddChanged = false,
|
||||||
|
isApplyChange = true,
|
||||||
|
isApplyAdd = true,
|
||||||
|
isApplyDel = true,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
let result = _.cloneDeep(obj);
|
||||||
if (!diff.__isDiff)
|
if (!diff.__isDiff)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
@@ -151,8 +211,9 @@ export function applyObjDiff(obj, diff, isAddChanged) {
|
|||||||
for (const key of Object.keys(change)) {
|
for (const key of Object.keys(change)) {
|
||||||
if (result.hasOwnProperty(key)) {
|
if (result.hasOwnProperty(key)) {
|
||||||
if (_.isObject(change[key])) {
|
if (_.isObject(change[key])) {
|
||||||
result[key] = applyObjDiff(result[key], change[key], isAddChanged);
|
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||||
} else {
|
} else {
|
||||||
|
if (isApplyChange)
|
||||||
result[key] = _.cloneDeep(change[key]);
|
result[key] = _.cloneDeep(change[key]);
|
||||||
}
|
}
|
||||||
} else if (isAddChanged) {
|
} else if (isAddChanged) {
|
||||||
@@ -160,13 +221,19 @@ export function applyObjDiff(obj, diff, isAddChanged) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isApplyAdd) {
|
||||||
for (const key of Object.keys(diff.add)) {
|
for (const key of Object.keys(diff.add)) {
|
||||||
result[key] = _.cloneDeep(diff.add[key]);
|
result[key] = _.cloneDeep(diff.add[key]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isApplyDel && diff.del.length) {
|
||||||
for (const key of diff.del) {
|
for (const key of diff.del) {
|
||||||
delete result[key];
|
delete result[key];
|
||||||
}
|
}
|
||||||
|
if (_.isArray(result))
|
||||||
|
result = result.filter(v => v);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -194,3 +261,46 @@ export function parseQuery(str) {
|
|||||||
}
|
}
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function escapeXml(str) {
|
||||||
|
return str.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyEventToCode(event) {
|
||||||
|
let result = [];
|
||||||
|
let code = event.code;
|
||||||
|
|
||||||
|
const modCode = code.substring(0, 3);
|
||||||
|
if (event.metaKey && modCode != 'Met')
|
||||||
|
result.push('Meta');
|
||||||
|
if (event.ctrlKey && modCode != 'Con')
|
||||||
|
result.push('Ctrl');
|
||||||
|
if (event.shiftKey && modCode != 'Shi')
|
||||||
|
result.push('Shift');
|
||||||
|
if (event.altKey && modCode != 'Alt')
|
||||||
|
result.push('Alt');
|
||||||
|
|
||||||
|
if (modCode == 'Dig') {
|
||||||
|
code = code.substring(5, 6);
|
||||||
|
} else if (modCode == 'Key') {
|
||||||
|
code = code.substring(3, 4);
|
||||||
|
}
|
||||||
|
result.push(code);
|
||||||
|
|
||||||
|
return result.join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userHotKeysObjectSwap(userHotKeys) {
|
||||||
|
let result = {};
|
||||||
|
for (const [name, codes] of Object.entries(userHotKeys)) {
|
||||||
|
for (const code of codes) {
|
||||||
|
result[code] = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,84 @@
|
|||||||
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
|
const readerActions = {
|
||||||
|
'help': 'Вызвать cправку',
|
||||||
|
'loader': 'На страницу загрузки',
|
||||||
|
'settings': 'Настроить',
|
||||||
|
'undoAction': 'Действие назад',
|
||||||
|
'redoAction': 'Действие вперед',
|
||||||
|
'fullScreen': 'На весь экран',
|
||||||
|
'scrolling': 'Плавный скроллинг',
|
||||||
|
'stopScrolling': '',
|
||||||
|
'setPosition': 'Установить позицию',
|
||||||
|
'search': 'Найти в тексте',
|
||||||
|
'copyText': 'Скопировать текст со страницы',
|
||||||
|
'splitToPara': 'Обновить с разбиением на параграфы',
|
||||||
|
'refresh': 'Принудительно обновить книгу',
|
||||||
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
|
'contents': 'Оглавление/закладки',
|
||||||
|
'libs': 'Библиотека',
|
||||||
|
'recentBooks': 'Открыть недавние',
|
||||||
|
'switchToolbar': 'Показать/скрыть панель управления',
|
||||||
|
'donate': '',
|
||||||
|
'bookBegin': 'В начало книги',
|
||||||
|
'bookEnd': 'В конец книги',
|
||||||
|
'pageBack': 'Страницу назад',
|
||||||
|
'pageForward': 'Страницу вперед',
|
||||||
|
'lineBack': 'Строчку назад',
|
||||||
|
'lineForward': 'Строчку вперед',
|
||||||
|
'incFontSize': 'Увеличить размер шрифта',
|
||||||
|
'decFontSize': 'Уменьшить размер шрифта',
|
||||||
|
'scrollingSpeedUp': 'Увеличить скорость скроллинга',
|
||||||
|
'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
|
||||||
|
};
|
||||||
|
|
||||||
|
//readerActions[name]
|
||||||
const toolButtons = [
|
const toolButtons = [
|
||||||
{name: 'undoAction', show: true, text: 'Действие назад'},
|
{name: 'undoAction', show: true},
|
||||||
{name: 'redoAction', show: true, text: 'Действие вперед'},
|
{name: 'redoAction', show: true},
|
||||||
{name: 'fullScreen', show: true, text: 'На весь экран'},
|
{name: 'fullScreen', show: true},
|
||||||
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
|
{name: 'scrolling', show: false},
|
||||||
{name: 'setPosition', show: true, text: 'На страницу'},
|
{name: 'setPosition', show: true},
|
||||||
{name: 'search', show: true, text: 'Найти в тексте'},
|
{name: 'search', show: true},
|
||||||
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
{name: 'copyText', show: false},
|
||||||
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
{name: 'splitToPara', show: false},
|
||||||
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
|
{name: 'refresh', show: true},
|
||||||
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
{name: 'contents', show: true},
|
||||||
|
{name: 'libs', show: true},
|
||||||
|
{name: 'recentBooks', show: true},
|
||||||
|
{name: 'offlineMode', show: false},
|
||||||
|
];
|
||||||
|
|
||||||
|
//readerActions[name]
|
||||||
|
const hotKeys = [
|
||||||
|
{name: 'help', codes: ['F1', 'H']},
|
||||||
|
{name: 'loader', codes: ['Escape']},
|
||||||
|
{name: 'settings', codes: ['S']},
|
||||||
|
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||||
|
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||||
|
{name: 'fullScreen', codes: ['Enter', 'Backquote', 'F']},
|
||||||
|
{name: 'scrolling', codes: ['Z']},
|
||||||
|
{name: 'setPosition', codes: ['P']},
|
||||||
|
{name: 'search', codes: ['Ctrl+F']},
|
||||||
|
{name: 'copyText', codes: ['Ctrl+C']},
|
||||||
|
{name: 'splitToPara', codes: ['Shift+R']},
|
||||||
|
{name: 'refresh', codes: ['R']},
|
||||||
|
{name: 'contents', codes: ['C']},
|
||||||
|
{name: 'libs', codes: ['L']},
|
||||||
|
{name: 'recentBooks', codes: ['X']},
|
||||||
|
{name: 'offlineMode', codes: ['O']},
|
||||||
|
|
||||||
|
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||||
|
{name: 'bookBegin', codes: ['Home']},
|
||||||
|
{name: 'bookEnd', codes: ['End']},
|
||||||
|
{name: 'pageBack', codes: ['PageUp', 'ArrowLeft', 'Backspace', 'Shift+Space']},
|
||||||
|
{name: 'pageForward', codes: ['PageDown', 'ArrowRight', 'Space']},
|
||||||
|
{name: 'lineBack', codes: ['ArrowUp']},
|
||||||
|
{name: 'lineForward', codes: ['ArrowDown']},
|
||||||
|
{name: 'incFontSize', codes: ['A']},
|
||||||
|
{name: 'decFontSize', codes: ['Shift+A']},
|
||||||
|
{name: 'scrollingSpeedUp', codes: ['Shift+ArrowDown']},
|
||||||
|
{name: 'scrollingSpeedDown', codes: ['Shift+ArrowUp']},
|
||||||
];
|
];
|
||||||
|
|
||||||
const fonts = [
|
const fonts = [
|
||||||
@@ -136,6 +205,7 @@ const webFonts = [
|
|||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------------------------------------
|
||||||
const settingDefaults = {
|
const settingDefaults = {
|
||||||
textColor: '#000000',
|
textColor: '#000000',
|
||||||
backgroundColor: '#EBE2C9',
|
backgroundColor: '#EBE2C9',
|
||||||
@@ -160,11 +230,12 @@ const settingDefaults = {
|
|||||||
statusBarTop: false,// top, bottom
|
statusBarTop: false,// top, bottom
|
||||||
statusBarHeight: 19,// px
|
statusBarHeight: 19,// px
|
||||||
statusBarColorAlpha: 0.4,
|
statusBarColorAlpha: 0.4,
|
||||||
|
statusBarClickOpen: true,
|
||||||
|
|
||||||
scrollingDelay: 3000,// замедление, ms
|
scrollingDelay: 3000,// замедление, ms
|
||||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
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%
|
pageChangeAnimationSpeed: 80, //0-100%
|
||||||
|
|
||||||
allowUrlParamBookPos: false,
|
allowUrlParamBookPos: false,
|
||||||
@@ -183,10 +254,12 @@ const settingDefaults = {
|
|||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
showDonationDialog2020: true,
|
showDonationDialog2020: true,
|
||||||
|
showLiberamaTopDialog2020: true,
|
||||||
enableSitesFilter: true,
|
enableSitesFilter: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
|
userHotKeys: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
@@ -195,6 +268,49 @@ for (const font of webFonts)
|
|||||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||||
for (const button of toolButtons)
|
for (const button of toolButtons)
|
||||||
settingDefaults.showToolButton[button.name] = button.show;
|
settingDefaults.showToolButton[button.name] = button.show;
|
||||||
|
for (const hotKey of hotKeys)
|
||||||
|
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
||||||
|
|
||||||
|
const excludeDiffHotKeys = [];
|
||||||
|
for (const hotKey of hotKeys)
|
||||||
|
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
|
||||||
|
|
||||||
|
function addDefaultsToSettings(settings) {
|
||||||
|
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
|
||||||
|
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
|
||||||
|
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const libsDefaults = {
|
||||||
|
startLink: 'http://flibusta.is',
|
||||||
|
comment: 'Флибуста | Книжное братство',
|
||||||
|
closeAfterSubmit: false,
|
||||||
|
openInFrameOnEnter: false,
|
||||||
|
openInFrameOnAdd: false,
|
||||||
|
groups: [
|
||||||
|
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||||
|
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||||
|
]},
|
||||||
|
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
||||||
|
{l: 'https://flibs.in', c: 'Flibs'},
|
||||||
|
]},
|
||||||
|
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||||
|
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||||
|
]},
|
||||||
|
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
|
||||||
|
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
|
||||||
|
]},
|
||||||
|
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
|
||||||
|
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
|
||||||
|
]},
|
||||||
|
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
|
||||||
|
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
const state = {
|
const state = {
|
||||||
@@ -209,6 +325,8 @@ const state = {
|
|||||||
currentProfile: '',
|
currentProfile: '',
|
||||||
settings: Object.assign({}, settingDefaults),
|
settings: Object.assign({}, settingDefaults),
|
||||||
settingsRev: {},
|
settingsRev: {},
|
||||||
|
libs: Object.assign({}, libsDefaults),
|
||||||
|
libsRev: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
@@ -247,18 +365,34 @@ const mutations = {
|
|||||||
state.currentProfile = value;
|
state.currentProfile = value;
|
||||||
},
|
},
|
||||||
setSettings(state, value) {
|
setSettings(state, value) {
|
||||||
state.settings = Object.assign({}, state.settings, value);
|
const newSettings = Object.assign({}, state.settings, value);
|
||||||
|
const added = addDefaultsToSettings(newSettings);
|
||||||
|
if (added) {
|
||||||
|
state.settings = added;
|
||||||
|
} else {
|
||||||
|
state.settings = newSettings;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setSettingsRev(state, value) {
|
setSettingsRev(state, value) {
|
||||||
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
||||||
},
|
},
|
||||||
|
setLibs(state, value) {
|
||||||
|
state.libs = value;
|
||||||
|
},
|
||||||
|
setLibsRev(state, value) {
|
||||||
|
state.libsRev = value;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
readerActions,
|
||||||
toolButtons,
|
toolButtons,
|
||||||
|
hotKeys,
|
||||||
fonts,
|
fonts,
|
||||||
webFonts,
|
webFonts,
|
||||||
settingDefaults,
|
settingDefaults,
|
||||||
|
addDefaultsToSettings,
|
||||||
|
libsDefaults,
|
||||||
|
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
|
|||||||
85
docs/beta/beta.liberama
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
server {
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/beta.liberama.top/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/beta.liberama.top/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
server_name beta.liberama.top;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:34082;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:34082;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/beta.liberama/public;
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name beta.liberama.top;
|
||||||
|
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name b.beta.liberama.top;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:34082;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:34082;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/beta.liberama/public;
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
docs/beta/beta.omnireader
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
server {
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/beta.omnireader.ru/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/beta.omnireader.ru/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
server_name beta.omnireader.ru;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:34081;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:34081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/beta.liberama/public;
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name beta.omnireader.ru;
|
||||||
|
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
4
docs/beta/deploy.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
npm run build:linux
|
||||||
|
sudo -u www-data cp -r ../../dist/linux/* /home/beta.liberama
|
||||||
3
docs/beta/run_server.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sudo -H -u www-data /home/beta.liberama/liberama
|
||||||
143
docs/liberama.top/liberama
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
server {
|
||||||
|
server_name _;
|
||||||
|
listen 80 default_server;
|
||||||
|
listen 443 ssl default_server;
|
||||||
|
|
||||||
|
#openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt
|
||||||
|
ssl_certificate /etc/nginx/ssl/nginx.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/nginx.key;
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/liberama.top/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/liberama.top/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
server_name liberama.top;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:55081;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:55081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/liberama/public;
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name liberama.top;
|
||||||
|
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name b.liberama.top;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://127.0.0.1:55081;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
proxy_pass http://127.0.0.1:55081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/liberama/public;
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 23480;
|
||||||
|
server_name flibusta_proxy;
|
||||||
|
|
||||||
|
valid_referers liberama.top b.liberama.top;
|
||||||
|
|
||||||
|
if ($invalid_referer) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://flibusta.is;
|
||||||
|
proxy_redirect http://static.flibusta.is:443 http://b.liberama.top:23481;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 23481;
|
||||||
|
server_name flibusta_proxy_static;
|
||||||
|
|
||||||
|
valid_referers liberama.top b.liberama.top;
|
||||||
|
|
||||||
|
if ($invalid_referer) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://static.flibusta.is:443;
|
||||||
|
proxy_set_header Referer "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 23580;
|
||||||
|
server_name fw_proxy;
|
||||||
|
|
||||||
|
valid_referers liberama.top b.liberama.top;
|
||||||
|
|
||||||
|
if ($invalid_referer) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://fantasy-worlds.org;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,11 +39,11 @@ sudo apt install poppler-utils
|
|||||||
```
|
```
|
||||||
|
|
||||||
### nginx, server config
|
### nginx, server config
|
||||||
Для своего домена необходимо будет подправить docs/omnireader/omnireader.
|
Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader.
|
||||||
Можно также настроить сервер для HTTP, без SSL.
|
Можно также настроить сервер для HTTP, без SSL.
|
||||||
```
|
```
|
||||||
sudo apt install nginx
|
sudo apt install nginx
|
||||||
sudo cp docs/omnireader/omnireader /etc/nginx/sites-available/omnireader
|
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
|
||||||
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
|
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
|
||||||
sudo rm /etc/nginx/sites-enabled/default
|
sudo rm /etc/nginx/sites-enabled/default
|
||||||
sudo service nginx reload
|
sudo service nginx reload
|
||||||
@@ -59,14 +59,20 @@ sudo service php7.2-fpm restart
|
|||||||
|
|
||||||
sudo mkdir /home/oldreader
|
sudo mkdir /home/oldreader
|
||||||
sudo chown www-data.www-data /home/oldreader
|
sudo chown www-data.www-data /home/oldreader
|
||||||
sudo -u www-data cp -r docs/omnireader/old/* /home/oldreader
|
sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск по крону
|
||||||
|
```
|
||||||
|
* * * * * /root/liberama/docs/omnireader.ru/cron_server.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Деплой и запуск
|
## Деплой и запуск
|
||||||
```
|
```
|
||||||
cd docs/omnireader
|
cd docs/omnireader.ru
|
||||||
|
./stop_server.sh
|
||||||
./deploy.sh
|
./deploy.sh
|
||||||
./run_server.sh
|
./start_server.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
|
После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
|
||||||
@@ -81,4 +87,4 @@ cd docs/omnireader
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
и перезапустить `run_server.sh`
|
и перезапустить сервер
|
||||||
8
docs/omnireader.ru/cron_server.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if ! pgrep -x "liberama" > /dev/null ; then
|
||||||
|
sudo -H -u www-data /home/liberama/liberama
|
||||||
|
else
|
||||||
|
echo "Process 'liberama' already running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 246 B After Width: | Height: | Size: 246 B |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |