Compare commits
299 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b453c3efe5 | ||
|
|
56590ef8a4 | ||
|
|
7c133136b9 | ||
|
|
41881639aa | ||
|
|
416003f078 | ||
|
|
bbfcd0efa3 | ||
|
|
150e4332c3 | ||
|
|
49649765c7 | ||
|
|
726b7bfa93 | ||
|
|
265f838868 | ||
|
|
6e2e5b5520 | ||
|
|
100ea2f64a | ||
|
|
4e7ed1ee33 | ||
|
|
8ab6aed1aa | ||
|
|
4ff096014c | ||
|
|
03b60b6ca9 | ||
|
|
e30b832e05 | ||
|
|
e646de85a7 | ||
|
|
70a7a0e344 | ||
|
|
b444abeb3e | ||
|
|
c72f56917d | ||
|
|
192283d6b2 | ||
|
|
6be6fa1966 | ||
|
|
510553b055 | ||
|
|
6c4616892e | ||
|
|
1e79a099b8 | ||
|
|
31a22327f1 | ||
|
|
c1712bebc6 | ||
|
|
cd91541245 | ||
|
|
4c1fc83256 | ||
|
|
34c7a33576 | ||
|
|
23ecfeeb4f | ||
|
|
9703f83eb3 | ||
|
|
0f3cc03d00 | ||
|
|
6f7ba1f9fc | ||
|
|
e1b85e4a1b | ||
|
|
b308dd58cc | ||
|
|
9f4c0479ce | ||
|
|
2c57817dde | ||
|
|
ba85c54d7c | ||
|
|
a80e5c3a65 | ||
|
|
22e2c34da8 | ||
|
|
00a8e4c2c5 | ||
|
|
10d0a4079c | ||
|
|
589f7f3c22 | ||
|
|
d1126a7eb0 | ||
|
|
9f4e72a0e1 | ||
|
|
a024295379 | ||
|
|
dc2b2ec488 | ||
|
|
0c5f5975aa | ||
|
|
dc3f682d2d | ||
|
|
2db8876c66 | ||
|
|
8f6201b0f7 | ||
|
|
4b146c70ad | ||
|
|
0118034b4b | ||
|
|
39217053ca | ||
|
|
fba190c826 | ||
|
|
5e9d528e16 | ||
|
|
c5921d88fc | ||
|
|
eb980b0ea1 | ||
|
|
de5b4216f7 | ||
|
|
495ff57b19 | ||
|
|
57948cf6e3 | ||
|
|
1aebbbcabd | ||
|
|
25b4cb072d | ||
|
|
1cdacc3a08 | ||
|
|
34d9466d09 | ||
|
|
c182c4ce66 | ||
|
|
dbb9bd1282 | ||
|
|
8019d2d6cc | ||
|
|
459cdb2e0b | ||
|
|
a230cd9513 | ||
|
|
0c44a25e85 | ||
|
|
34f3d04370 | ||
|
|
1f3e6b7e16 | ||
|
|
47d49a200a | ||
|
|
e1767d6e52 | ||
|
|
0f8e343cd2 | ||
|
|
23ab487baf | ||
|
|
22e5d38ef5 | ||
|
|
5819ccb528 | ||
|
|
42a2fd77cf | ||
|
|
ab93a8b0b3 | ||
|
|
84437eafa6 | ||
|
|
0107d848e0 | ||
|
|
5eeac96a0d | ||
|
|
9351c115be | ||
|
|
f95a11096c | ||
|
|
4203d179e6 | ||
|
|
78dfc9cb1c | ||
|
|
0bef307d77 | ||
|
|
b0da806f7a | ||
|
|
badecd1d81 | ||
|
|
6418e8ee30 | ||
|
|
09115c9658 | ||
|
|
74e3866bd7 | ||
|
|
408de78c13 | ||
|
|
c0451c18b3 | ||
|
|
f303d26c1e | ||
|
|
1b58a34859 | ||
|
|
82ea416e67 | ||
|
|
efd4fbad70 | ||
|
|
01bd15121b | ||
|
|
a9c2495349 | ||
|
|
e7c50b50ed | ||
|
|
6e25b289d2 | ||
|
|
157267eaf7 | ||
|
|
a317f9137a | ||
|
|
5dad3d22ea | ||
|
|
be85df456b | ||
|
|
2e172a08c7 | ||
|
|
bb1069ca60 | ||
|
|
d8141a1628 | ||
|
|
de9f7c4baf | ||
|
|
fa9b3116f1 | ||
|
|
dcf9d52961 | ||
|
|
1da93e2cc7 | ||
|
|
1d1bab988e | ||
|
|
dcc6ad3af3 | ||
|
|
d57f266789 | ||
|
|
c3395e1eff | ||
|
|
ca59ec2dbe | ||
|
|
79788125f3 | ||
|
|
2154f20fa4 | ||
|
|
afe40b6a89 | ||
|
|
ba4b3bd6b8 | ||
|
|
e423b5d745 | ||
|
|
6de8eca7ea | ||
|
|
9d68cfcaf0 | ||
|
|
225de11e6a | ||
|
|
916581bbd0 | ||
|
|
1cbb35840f | ||
|
|
7a1d769e39 | ||
|
|
8254bf934c | ||
|
|
5e2f20542f | ||
|
|
551a707ee4 | ||
|
|
024b15b4f9 | ||
|
|
1935df4143 | ||
|
|
3f99f90076 | ||
|
|
53cb445dde | ||
|
|
6e46947220 | ||
|
|
9b65e1671b | ||
|
|
d5c741db35 | ||
|
|
11e0780b6e | ||
|
|
f153541570 | ||
|
|
f066af88e7 | ||
|
|
97e1eef799 | ||
|
|
1bcd902817 | ||
|
|
2484568b21 | ||
|
|
085cc47ea5 | ||
|
|
aac36a88f3 | ||
|
|
1f2ebc82b7 | ||
|
|
9781949064 | ||
|
|
b06ef3781a | ||
|
|
b32213cb7b | ||
|
|
ac4c7d2421 | ||
|
|
824a49b80f | ||
|
|
13efd50d80 | ||
|
|
6fb091d20f | ||
|
|
518ab85cae | ||
|
|
f5124ad8b5 | ||
|
|
6f80900aa8 | ||
|
|
06b80e9281 | ||
|
|
51b39d9365 | ||
|
|
f7d2d8fc95 | ||
|
|
f34fb94c1a | ||
|
|
3107224e50 | ||
|
|
e1c481c534 | ||
|
|
945a2dd3eb | ||
|
|
e318945eb1 | ||
|
|
926709568d | ||
|
|
da040e799c | ||
|
|
694976cb6e | ||
|
|
3f7bd1846a | ||
|
|
714898b4c3 | ||
|
|
4efc9b6990 | ||
|
|
73c3beaff1 | ||
|
|
a6bdccd4ef | ||
|
|
8007991e7d | ||
|
|
0e5d1ed1c3 | ||
|
|
91dc2f4f71 | ||
|
|
950bab3023 | ||
|
|
29082a10e6 | ||
|
|
65c1227d88 | ||
|
|
5d121a68cf | ||
|
|
ad07d2b8b1 | ||
|
|
c5aef78085 | ||
|
|
522ebc8aa2 | ||
|
|
199b3761b5 | ||
|
|
daf7b45e45 | ||
|
|
fc71b953c7 | ||
|
|
74ccd4a001 | ||
|
|
3c09f6ca55 | ||
|
|
c7dbe8599d | ||
|
|
ca036b6676 | ||
|
|
5ae87c8e03 | ||
|
|
9774fc4f65 | ||
|
|
d0891fb652 | ||
|
|
e388e2a1c7 | ||
|
|
d9ab354338 | ||
|
|
9ea0a0e214 | ||
|
|
131ddf0355 | ||
|
|
8abe71a0fe | ||
|
|
43e27a7e68 | ||
|
|
b784d277e4 | ||
|
|
cb443157da | ||
|
|
c886015d92 | ||
|
|
3161247da9 | ||
|
|
743a250131 | ||
|
|
4fb4b21a9e | ||
|
|
e1a7d3ebc5 | ||
|
|
72b8b156ac | ||
|
|
134dafb608 | ||
|
|
d5102b6422 | ||
|
|
a2cfb9d423 | ||
|
|
bef70f94ab | ||
|
|
4233fffe74 | ||
|
|
81c214748d | ||
|
|
c6a61dc8c8 | ||
|
|
483092d40d | ||
|
|
88cb02f6bc | ||
|
|
9628188730 | ||
|
|
2e66134bf8 | ||
|
|
424fe4d1e9 | ||
|
|
2b6f9568de | ||
|
|
4b270bce8b | ||
|
|
6b077e67db | ||
|
|
4c79ea0679 | ||
|
|
8c4c4c25aa | ||
|
|
a37dbe2c06 | ||
|
|
5e10cb2d16 | ||
|
|
58316c5c1d | ||
|
|
55f092f161 | ||
|
|
ab5049127a | ||
|
|
5f99067e56 | ||
|
|
3a89e61bd8 | ||
|
|
06edfa2fee | ||
|
|
77bfd72458 | ||
|
|
5ddf19be4d | ||
|
|
6657b47746 | ||
|
|
5690efb07a | ||
|
|
05600cba08 | ||
|
|
e3b4120b2c | ||
|
|
1059245fd9 | ||
|
|
87c8d310b3 | ||
|
|
fdc4999556 | ||
|
|
d28a8db4ff | ||
|
|
ab9e7d10dd | ||
|
|
3ff72b26b9 | ||
|
|
107ae70651 | ||
|
|
04de19033e | ||
|
|
089ac70cd3 | ||
|
|
ae40a9ead9 | ||
|
|
152806b7f6 | ||
|
|
06beb8e704 | ||
|
|
64f2b94685 | ||
|
|
5a42eb98ab | ||
|
|
404b87d78d | ||
|
|
dcb8fbdbf4 | ||
|
|
0fe513d7f5 | ||
|
|
0be05325e4 | ||
|
|
75b39308cd | ||
|
|
35ded81713 | ||
|
|
07c85280cd | ||
|
|
43f1d86be0 | ||
|
|
82f5ed4c44 | ||
|
|
0b53ad4b4d | ||
|
|
56ad41d10c | ||
|
|
249a4564e0 | ||
|
|
efb2413720 | ||
|
|
1226acefd6 | ||
|
|
76f7d7bc90 | ||
|
|
a5cb2641fd | ||
|
|
57fc64af79 | ||
|
|
f8b7b8b698 | ||
|
|
3da6befe10 | ||
|
|
a50d61c3ce | ||
|
|
b7568975e7 | ||
|
|
4b9475310f | ||
|
|
639f726c83 | ||
|
|
7997c486cf | ||
|
|
2569d00bd0 | ||
|
|
2cd80d8fa1 | ||
|
|
eedca4db9b | ||
|
|
1d352a76ce | ||
|
|
17670aabf9 | ||
|
|
3456b3d90e | ||
|
|
f3da5a9026 | ||
|
|
00cc63b7cd | ||
|
|
8df80ce738 | ||
|
|
12e7a783b0 | ||
|
|
be86a15351 | ||
|
|
2c5022e7b4 | ||
|
|
7d4baa7046 | ||
|
|
a24eaaed50 | ||
|
|
26813c582f | ||
|
|
6067ac73e2 | ||
|
|
b1d94b67f4 | ||
|
|
452f4e69fd |
31
build/includer.js
Normal file
31
build/includer.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
//пример в коде:
|
||||||
|
// @@include('./test/testFile.inc');
|
||||||
|
|
||||||
|
function includeRecursive(self, parentFile, source, depth) {
|
||||||
|
depth = (depth ? depth : 0);
|
||||||
|
if (depth > 50)
|
||||||
|
throw new Error('includer: stack too big');
|
||||||
|
const lines = source.split('\n');
|
||||||
|
let result = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const m = trimmed.match(/^@@[\s]*?include[\s]*?\(['"](.*)['"]\)/);
|
||||||
|
if (m) {
|
||||||
|
const includedFile = path.resolve(path.dirname(parentFile), m[1]);
|
||||||
|
self.addDependency(includedFile);
|
||||||
|
|
||||||
|
const fileContent = fs.readFileSync(includedFile, 'utf8');
|
||||||
|
result = result.concat(includeRecursive(self, includedFile, fileContent, depth + 1));
|
||||||
|
} else {
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.default = function includer(source) {
|
||||||
|
return includeRecursive(this, this.resourcePath, source).join('\n');
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api'
|
baseURL: '/api'
|
||||||
@@ -6,9 +7,23 @@ const api = axios.create({
|
|||||||
|
|
||||||
class Misc {
|
class Misc {
|
||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
const response = await api.post('/config', {params: [
|
|
||||||
|
const query = {params: [
|
||||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||||
]});
|
]};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wsc.open();
|
||||||
|
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||||
|
if (config.error)
|
||||||
|
throw new Error(config.error);
|
||||||
|
return config;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//если с WebSocket проблема, работаем по http
|
||||||
|
const response = await api.post('/config', query);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as utils from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
import WebSocketConnection from './WebSocketConnection';
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api/reader'
|
baseURL: '/api/reader'
|
||||||
@@ -12,22 +12,28 @@ const workerApi = axios.create({
|
|||||||
|
|
||||||
class Reader {
|
class Reader {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.wsc = new WebSocketConnection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStateFinish(workerId, callback) {
|
async getWorkerStateFinish(workerId, callback) {
|
||||||
if (!callback) callback = () => {};
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
let response = {};
|
let response = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsc = this.wsc;
|
|
||||||
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);
|
||||||
|
|
||||||
|
if (!response.state && prevResponse !== false) {//экономия траффика
|
||||||
|
callback(prevResponse);
|
||||||
|
} else {//были изменения worker state
|
||||||
|
if (!response.state)
|
||||||
|
throw new Error('Неверный ответ api');
|
||||||
callback(response);
|
callback(response);
|
||||||
|
prevResponse = response;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.state == 'finish' || response.state == 'error') {
|
if (response.state == 'finish' || response.state == 'error') {
|
||||||
break;
|
break;
|
||||||
@@ -35,11 +41,10 @@ class Reader {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
//с WebSocket проблема, проверяем по http
|
//если с WebSocket проблема, работаем по http
|
||||||
const refreshPause = 500;
|
const refreshPause = 500;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
response = {};
|
response = {};
|
||||||
@@ -50,6 +55,9 @@ class Reader {
|
|||||||
response = response.data;
|
response = response.data;
|
||||||
callback(response);
|
callback(response);
|
||||||
|
|
||||||
|
if (!response.state)
|
||||||
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
if (response.state == 'finish' || response.state == 'error') {
|
if (response.state == 'finish' || response.state == 'error') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -80,12 +88,12 @@ class Reader {
|
|||||||
callback({totalSteps: 4});
|
callback({totalSteps: 4});
|
||||||
callback(response.data);
|
callback(response.data);
|
||||||
|
|
||||||
response = await this.getStateFinish(workerId, callback);
|
response = await this.getWorkerStateFinish(workerId, callback);
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
|
||||||
callback({step: 4});
|
callback({step: 4});
|
||||||
const book = await this.loadCachedBook(response.path, callback, false, (response.size ? response.size : -1));
|
const book = await this.loadCachedBook(response.path, callback, response.size);
|
||||||
return Object.assign({}, response, {data: book.data});
|
return Object.assign({}, response, {data: book.data});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,75 +111,61 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkUrl(url) {
|
async checkCachedBook(url) {
|
||||||
let fileExists = false;
|
let estSize = -1;
|
||||||
try {
|
try {
|
||||||
await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
||||||
fileExists = true;
|
|
||||||
} catch (e) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
//восстановим при необходимости файл на сервере из удаленного облака
|
|
||||||
if (!fileExists) {
|
|
||||||
let response = await api.post('/restore-cached-file', {path: url});
|
|
||||||
|
|
||||||
const workerId = response.data.workerId;
|
|
||||||
if (!workerId)
|
|
||||||
throw new Error('Неверный ответ api');
|
|
||||||
|
|
||||||
response = await this.getStateFinish(workerId);
|
|
||||||
if (response.state == 'error') {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadCachedBook(url, callback, restore = true, estSize = -1) {
|
|
||||||
if (!callback) callback = () => {};
|
|
||||||
let response = null;
|
|
||||||
|
|
||||||
callback({state: 'loading', progress: 0});
|
|
||||||
|
|
||||||
//получение размера файла
|
|
||||||
let fileExists = false;
|
|
||||||
if (estSize < 0) {
|
|
||||||
try {
|
|
||||||
response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
|
|
||||||
|
|
||||||
if (response.headers['content-length']) {
|
if (response.headers['content-length']) {
|
||||||
estSize = response.headers['content-length'];
|
estSize = response.headers['content-length'];
|
||||||
}
|
}
|
||||||
fileExists = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//восстановим при необходимости файл на сервере из удаленного облака
|
//восстановим при необходимости файл на сервере из удаленного облака
|
||||||
if (restore && !fileExists) {
|
let response = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wsc.open();
|
||||||
|
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
//если с WebSocket проблема, работаем по http
|
||||||
response = await api.post('/restore-cached-file', {path: url});
|
response = await api.post('/restore-cached-file', {path: url});
|
||||||
|
response = response.data;
|
||||||
const workerId = response.data.workerId;
|
}
|
||||||
if (!workerId)
|
|
||||||
throw new Error('Неверный ответ api');
|
|
||||||
|
|
||||||
response = await this.getStateFinish(workerId);
|
|
||||||
if (response.state == 'error') {
|
if (response.state == 'error') {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workerId = response.workerId;
|
||||||
|
if (!workerId)
|
||||||
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
|
response = await this.getWorkerStateFinish(workerId);
|
||||||
|
if (response.state == 'error') {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
if (response.size && estSize < 0) {
|
if (response.size && estSize < 0) {
|
||||||
estSize = response.size;
|
estSize = response.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return estSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCachedBook(url, callback, estSize = -1) {
|
||||||
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
|
callback({state: 'loading', progress: 0});
|
||||||
|
|
||||||
|
//получение размера файла
|
||||||
|
if (estSize && estSize < 0) {
|
||||||
|
estSize = await this.checkCachedBook(url);
|
||||||
|
}
|
||||||
|
|
||||||
//получение файла
|
//получение файла
|
||||||
estSize = (estSize > 0 ? estSize : 1000000);
|
estSize = (estSize > 0 ? estSize : 1000000);
|
||||||
const options = {
|
const options = {
|
||||||
onDownloadProgress: progress => {
|
onDownloadProgress: (progress) => {
|
||||||
while (progress.loaded > estSize) estSize *= 1.5;
|
while (progress.loaded > estSize) estSize *= 1.5;
|
||||||
|
|
||||||
if (callback)
|
if (callback)
|
||||||
@@ -215,13 +209,25 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async storage(request) {
|
async storage(request) {
|
||||||
let response = await api.post('/storage', request);
|
let response = null;
|
||||||
|
try {
|
||||||
|
await wsc.open();
|
||||||
|
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
//если с WebSocket проблема, работаем по http
|
||||||
|
response = await api.post('/storage', request);
|
||||||
|
response = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
const state = response.data.state;
|
const state = response.state;
|
||||||
if (!state)
|
if (!state)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
|
if (response.state == 'error') {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,4 +169,4 @@ class WebSocketConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WebSocketConnection;
|
export default new WebSocketConnection();
|
||||||
5
client/assets/sw-register.js
Normal file
5
client/assets/sw-register.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
(function() {
|
||||||
|
if('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<!--q-layout view="lhr lpr lfr">
|
||||||
<el-aside v-if="showAsideBar" :width="asideWidth">
|
<q-drawer v-model="showAsideBar" :width="asideWidth">
|
||||||
<div class="app-name"><span v-html="appName"></span></div>
|
<div class="app-name"><span v-html="appName"></span></div>
|
||||||
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
|
<q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
|
||||||
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable v-ripple>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="inbox" />
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>Inbox</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list-->
|
||||||
|
<!--el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
||||||
<el-menu-item index="/cardindex">
|
<el-menu-item index="/cardindex">
|
||||||
<i class="el-icon-search"></i>
|
<i class="el-icon-search"></i>
|
||||||
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
|
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
|
||||||
@@ -32,24 +42,37 @@
|
|||||||
<i class="el-icon-question"></i>
|
<i class="el-icon-question"></i>
|
||||||
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu-->
|
||||||
</el-aside>
|
<!--/q-drawer>
|
||||||
|
|
||||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
<q-page-container>
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</el-main>
|
</q-page-container>
|
||||||
</el-container>
|
</q-layout-->
|
||||||
|
<div class="fit row">
|
||||||
|
<Notify ref="notify"/>
|
||||||
|
<StdDialog ref="stdDialog"/>
|
||||||
|
<keep-alive>
|
||||||
|
<router-view class="col"></router-view>
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
</template>
|
</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();
|
||||||
@@ -75,6 +98,18 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
// set-app-title
|
// set-app-title
|
||||||
this.$root.$on('set-app-title', this.setAppTitle);
|
this.$root.$on('set-app-title', this.setAppTitle);
|
||||||
|
|
||||||
@@ -108,17 +143,16 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,9 +171,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,10 +197,7 @@ 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) {
|
||||||
@@ -193,12 +224,11 @@ class App extends Vue {
|
|||||||
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
|
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
|
||||||
}
|
}
|
||||||
|
|
||||||
get isReaderActive() {
|
set showAsideBar(value) {
|
||||||
return this.rootRoute == '/reader';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get showMain() {
|
get isReaderActive() {
|
||||||
return (this.showAsideBar || this.isReaderActive);
|
return this.rootRoute == '/reader';
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectIfNeeded() {
|
redirectIfNeeded() {
|
||||||
@@ -228,68 +258,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 yellow !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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ 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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -25,10 +25,10 @@
|
|||||||
<div v-show="mode == 'omnireader'">
|
<div v-show="mode == 'omnireader'">
|
||||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||||
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||||
<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="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
|
||||||
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||||
@@ -60,9 +60,9 @@ class CommonHelpPage extends Vue {
|
|||||||
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 +70,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>
|
||||||
|
|||||||
@@ -1,30 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
|
<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>
|
<div class="para">{{ yandexAddress }}
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="address">
|
||||||
|
<img class="logo" src="./assets/paypal.png">
|
||||||
|
<div class="para">{{ paypalAddress }}
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/bitcoin.png">
|
<img class="logo" src="./assets/bitcoin.png">
|
||||||
<el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
|
<div class="para">{{ bitcoinAddress }}
|
||||||
<div class="para">{{ bitcoinAddress }}</div>
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/litecoin.png">
|
<img class="logo" src="./assets/litecoin.png">
|
||||||
<el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
|
<div class="para">{{ litecoinAddress }}
|
||||||
<div class="para">{{ litecoinAddress }}</div>
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/monero.png">
|
<img class="logo" src="./assets/monero.png">
|
||||||
<el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
|
<div class="para">{{ moneroAddress }}
|
||||||
<div class="para">{{ moneroAddress }}</div>
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,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';
|
||||||
@@ -54,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('Копирование не удалось');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
@@ -64,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 {
|
||||||
@@ -79,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;
|
||||||
@@ -97,13 +112,16 @@ h5 {
|
|||||||
margin: 10px 10px 10px 40px;
|
margin: 10px 10px 10px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
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,32 +30,54 @@ 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) {
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,31 +1,36 @@
|
|||||||
<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 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">
|
||||||
|
Из буфера обмена
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<div class="q-my-md"></div>
|
||||||
<div v-if="mode == 'omnireader'">
|
<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"
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -143,12 +148,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() {
|
||||||
@@ -168,80 +173,37 @@ class LoaderPage extends Vue {
|
|||||||
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.code == '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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +27,13 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
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': ' ',
|
||||||
'finish': ' ',
|
'finish': ' ',
|
||||||
'error': ' ',
|
'error': ' ',
|
||||||
|
'queue': 'очередь',
|
||||||
'download': 'скачивание',
|
'download': 'скачивание',
|
||||||
'decompress': 'распаковка',
|
'decompress': 'распаковка',
|
||||||
'convert': 'конвертирование',
|
'convert': 'конвертирование',
|
||||||
@@ -32,68 +50,51 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
this.text = '';
|
||||||
|
this.iconAngle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(state) {
|
setState(state) {
|
||||||
if (state.state)
|
if (state.state) {
|
||||||
|
if (state.state == 'queue') {
|
||||||
|
this.text = (state.place ? 'Номер в очереди: ' + state.place : '');
|
||||||
|
} else {
|
||||||
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.step = (state.step ? state.step : this.step);
|
this.step = (state.step ? state.step : this.step);
|
||||||
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
||||||
this.progress = state.progress || 0;
|
this.progress = state.progress || 0;
|
||||||
|
|
||||||
|
if (!this.ani) {
|
||||||
|
(async() => {
|
||||||
|
this.ani = true;
|
||||||
|
this.iconAngle += 30;
|
||||||
|
this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
|
||||||
|
await utils.sleep(150);
|
||||||
|
this.ani = false;
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get percentage() {
|
get percentage() {
|
||||||
let circle = document.querySelector('path[class="el-progress-circle__path"]');
|
|
||||||
if (circle)
|
|
||||||
circle.style.transition = '';
|
|
||||||
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</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>
|
|
||||||
|
|||||||
@@ -1,97 +1,148 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div class="column no-wrap">
|
||||||
<el-header v-show="toolBarActive" height='50px'>
|
<div ref="header" class="header" v-show="toolBarActive">
|
||||||
<div ref="header" class="header">
|
<div ref="buttons" class="row justify-between no-wrap">
|
||||||
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
|
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
|
||||||
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
|
<q-icon name="la la-arrow-left" size="32px"/>
|
||||||
</el-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
|
<button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
|
||||||
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
|
<q-icon name="la la-angle-left" size="32px"/>
|
||||||
</el-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['undoAction'] }}</q-tooltip>
|
||||||
<el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
|
</button>
|
||||||
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
|
<button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
|
||||||
</el-tooltip>
|
<q-icon name="la la-angle-right" size="32px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
|
||||||
|
</button>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
|
<button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
|
||||||
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
|
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
|
||||||
</el-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['fullScreen'] }}</q-tooltip>
|
||||||
<el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
|
</button>
|
||||||
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
|
<button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
|
||||||
</el-tooltip>
|
<q-icon name="la la-film" size="32px"/>
|
||||||
<el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['scrolling'] }}</q-tooltip>
|
||||||
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
|
</button>
|
||||||
</el-tooltip>
|
<button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
|
||||||
<el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
|
<q-icon name="la la-angle-double-right" size="32px"/>
|
||||||
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['setPosition'] }}</q-tooltip>
|
||||||
</el-tooltip>
|
</button>
|
||||||
<el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
|
<button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
|
||||||
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
|
<q-icon name="la la-search" size="32px"/>
|
||||||
</el-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['search'] }}</q-tooltip>
|
||||||
<el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
|
</button>
|
||||||
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
|
<button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
|
||||||
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
|
<q-icon name="la la-copy" size="32px"/>
|
||||||
</el-button>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
|
||||||
</el-tooltip>
|
</button>
|
||||||
|
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
||||||
|
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
|
||||||
|
</button>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<el-tooltip v-show="showToolButton['offlineMode']" content="Автономный режим (без интернета)" :open-delay="1000" effect="light">
|
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
|
||||||
<el-button ref="offlineMode" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"><i class="el-icon-connection"></i></el-button>
|
<q-icon name="la la-unlink" size="32px"/>
|
||||||
</el-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
|
||||||
<el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
|
</button>
|
||||||
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
|
<button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
|
||||||
</el-tooltip>
|
<q-icon name="la la-book-open" size="32px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-tooltip content="Настроить" :open-delay="1000" effect="light">
|
<button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
|
||||||
<el-button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')"><i class="el-icon-setting"></i></el-button>
|
<q-icon name="la la-cog" size="32px"/>
|
||||||
</el-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<el-main>
|
<div class="main col row relative-position">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component ref="page" :is="activePage"
|
<component ref="page" class="col" :is="activePage"
|
||||||
@load-book="loadBook"
|
@load-book="loadBook"
|
||||||
@load-file="loadFile"
|
@load-file="loadFile"
|
||||||
@book-pos-changed="bookPosChanged"
|
@book-pos-changed="bookPosChanged"
|
||||||
@tool-bar-toggle="toolBarToggle"
|
@do-action="doAction"
|
||||||
@full-screen-toogle="fullScreenToggle"
|
|
||||||
@stop-scrolling="stopScrolling"
|
|
||||||
@scrolling-toggle="scrollingToggle"
|
|
||||||
@help-toggle="helpToggle"
|
|
||||||
@donate-toggle="donateToggle"
|
|
||||||
></component>
|
></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
|
|
||||||
<SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
|
<SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
|
||||||
<SearchPage v-show="searchActive" ref="searchPage"
|
<SearchPage v-show="searchActive" ref="searchPage"
|
||||||
@search-toggle="searchToggle"
|
@do-action="doAction"
|
||||||
@book-pos-changed="bookPosChanged"
|
@book-pos-changed="bookPosChanged"
|
||||||
@start-text-search="startTextSearch"
|
@start-text-search="startTextSearch"
|
||||||
@stop-text-search="stopTextSearch">
|
@stop-text-search="stopTextSearch">
|
||||||
</SearchPage>
|
</SearchPage>
|
||||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
||||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
|
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
||||||
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
||||||
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
|
|
||||||
<el-dialog
|
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||||
title="Что нового:"
|
<template slot="header">
|
||||||
:visible.sync="whatsNewVisible"
|
Что нового:
|
||||||
width="80%">
|
</template>
|
||||||
|
|
||||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||||
|
|
||||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer">
|
||||||
<el-button @click="whatsNewDisable">Больше не показывать</el-button>
|
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</Dialog>
|
||||||
|
|
||||||
</el-main>
|
<Dialog ref="dialog2" v-model="donationVisible">
|
||||||
</el-container>
|
<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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -113,8 +164,10 @@ import SettingsPage from './SettingsPage/SettingsPage.vue';
|
|||||||
import HelpPage from './HelpPage/HelpPage.vue';
|
import HelpPage from './HelpPage/HelpPage.vue';
|
||||||
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
||||||
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
||||||
|
import Dialog from '../share/Dialog.vue';
|
||||||
|
|
||||||
import bookManager from './share/bookManager';
|
import bookManager from './share/bookManager';
|
||||||
|
import rstore from '../../store/modules/reader';
|
||||||
import readerApi from '../../api/reader';
|
import readerApi from '../../api/reader';
|
||||||
import * as utils from '../../share/utils';
|
import * as utils from '../../share/utils';
|
||||||
import {versionHistory} from './versionHistory';
|
import {versionHistory} from './versionHistory';
|
||||||
@@ -133,6 +186,7 @@ export default @Component({
|
|||||||
HelpPage,
|
HelpPage,
|
||||||
ClickMapPage,
|
ClickMapPage,
|
||||||
ServerStorage,
|
ServerStorage,
|
||||||
|
Dialog,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
bookPos: function(newValue) {
|
bookPos: function(newValue) {
|
||||||
@@ -174,6 +228,7 @@ export default @Component({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
class Reader extends Vue {
|
class Reader extends Vue {
|
||||||
|
rstore = {};
|
||||||
loaderActive = false;
|
loaderActive = false;
|
||||||
progressActive = false;
|
progressActive = false;
|
||||||
fullScreenActive = false;
|
fullScreenActive = false;
|
||||||
@@ -200,8 +255,10 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
whatsNewVisible = false;
|
whatsNewVisible = false;
|
||||||
whatsNewContent = '';
|
whatsNewContent = '';
|
||||||
|
donationVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
this.rstore = rstore;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
this.dispatch = this.$store.dispatch;
|
||||||
@@ -245,7 +302,7 @@ class Reader extends Vue {
|
|||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
bookManager.addEventListener(this.bookManagerEvent);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
|
|
||||||
if (this.$root.rootRoute == '/reader') {
|
if (this.$root.rootRoute() == '/reader') {
|
||||||
if (this.routeParamUrl) {
|
if (this.routeParamUrl) {
|
||||||
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
|
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
|
||||||
} else {
|
} else {
|
||||||
@@ -258,9 +315,10 @@ class Reader extends Vue {
|
|||||||
this.checkActivateDonateHelpPage();
|
this.checkActivateDonateHelpPage();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
await this.showWhatsNew();
|
|
||||||
|
|
||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
|
|
||||||
|
await this.showWhatsNew();
|
||||||
|
await this.showDonation();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,16 +330,27 @@ class Reader extends Vue {
|
|||||||
this.clickControl = settings.clickControl;
|
this.clickControl = settings.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
|
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
this.enableSitesFilter = settings.enableSitesFilter;
|
this.enableSitesFilter = settings.enableSitesFilter;
|
||||||
|
|
||||||
|
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||||
|
this.$root.readerActionByKeyEvent = (event) => {
|
||||||
|
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
||||||
|
}
|
||||||
|
|
||||||
this.updateHeaderMinWidth();
|
this.updateHeaderMinWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderMinWidth() {
|
updateHeaderMinWidth() {
|
||||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
||||||
|
if (this.$refs.buttons)
|
||||||
|
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(1000);
|
||||||
if (this.$refs.header)
|
if (this.$refs.header)
|
||||||
this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
|
this.$refs.header.style.overflowX = 'auto';
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSetStorageAccessKey() {
|
checkSetStorageAccessKey() {
|
||||||
@@ -337,6 +406,41 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showDonation() {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
const today = utils.formatDate(new Date(), 'coDate');
|
||||||
|
|
||||||
|
if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
||||||
|
this.donationVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donationDialogDisable() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
if (this.showDonationDialog2020) {
|
||||||
|
const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donationDialogRemind() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
openDonate() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
this.donateToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyLink(link) {
|
||||||
|
const result = await utils.copyTextToClipboard(link);
|
||||||
|
if (result)
|
||||||
|
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
||||||
|
else
|
||||||
|
this.$root.notify.error('Копирование не удалось');
|
||||||
|
}
|
||||||
|
|
||||||
openVersionHistory() {
|
openVersionHistory() {
|
||||||
this.whatsNewVisible = false;
|
this.whatsNewVisible = false;
|
||||||
this.versionHistoryToggle();
|
this.versionHistoryToggle();
|
||||||
@@ -455,6 +559,10 @@ class Reader extends Vue {
|
|||||||
return this.$store.state.reader.whatsNewContentHash;
|
return this.$store.state.reader.whatsNewContentHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get donationRemindDate() {
|
||||||
|
return this.$store.state.reader.donationRemindDate;
|
||||||
|
}
|
||||||
|
|
||||||
addAction(pos) {
|
addAction(pos) {
|
||||||
let a = this.actionList;
|
let a = this.actionList;
|
||||||
if (!a.length || a[a.length - 1] != pos) {
|
if (!a.length || a[a.length - 1] != pos) {
|
||||||
@@ -473,22 +581,9 @@ class Reader extends Vue {
|
|||||||
fullScreenToggle() {
|
fullScreenToggle() {
|
||||||
this.fullScreenActive = !this.fullScreenActive;
|
this.fullScreenActive = !this.fullScreenActive;
|
||||||
if (this.fullScreenActive) {
|
if (this.fullScreenActive) {
|
||||||
const element = document.documentElement;
|
this.$q.fullscreen.request();
|
||||||
if (element.requestFullscreen) {
|
|
||||||
element.requestFullscreen();
|
|
||||||
} else if (element.webkitrequestFullscreen) {
|
|
||||||
element.webkitRequestFullscreen();
|
|
||||||
} else if (element.mozRequestFullscreen) {
|
|
||||||
element.mozRequestFullScreen();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (document.cancelFullScreen) {
|
this.$q.fullscreen.exit();
|
||||||
document.cancelFullScreen();
|
|
||||||
} else if (document.mozCancelFullScreen) {
|
|
||||||
document.mozCancelFullScreen();
|
|
||||||
} else if (document.webkitCancelFullScreen) {
|
|
||||||
document.webkitCancelFullScreen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +606,8 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
setPositionToggle() {
|
setPositionToggle() {
|
||||||
this.setPositionActive = !this.setPositionActive;
|
this.setPositionActive = !this.setPositionActive;
|
||||||
if (this.setPositionActive && this.activePage == 'TextPage' && this.mostRecentBook()) {
|
const page = this.$refs.page;
|
||||||
|
if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
|
||||||
this.closeAllTextPages();
|
this.closeAllTextPages();
|
||||||
this.setPositionActive = true;
|
this.setPositionActive = true;
|
||||||
|
|
||||||
@@ -591,6 +687,10 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recentBooksClose() {
|
||||||
|
this.recentBooksActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
recentBooksToggle() {
|
recentBooksToggle() {
|
||||||
this.recentBooksActive = !this.recentBooksActive;
|
this.recentBooksActive = !this.recentBooksActive;
|
||||||
if (this.recentBooksActive) {
|
if (this.recentBooksActive) {
|
||||||
@@ -653,81 +753,53 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonClick(button) {
|
undoAction() {
|
||||||
const activeClass = this.buttonActiveClass(button);
|
|
||||||
|
|
||||||
this.$refs[button].$el.blur();
|
|
||||||
|
|
||||||
if (activeClass['tool-button-disabled'])
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (button) {
|
|
||||||
case 'loader':
|
|
||||||
this.loaderToggle();
|
|
||||||
break;
|
|
||||||
case 'undoAction':
|
|
||||||
if (this.actionCur > 0) {
|
if (this.actionCur > 0) {
|
||||||
this.actionCur--;
|
this.actionCur--;
|
||||||
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case 'redoAction':
|
|
||||||
|
redoAction() {
|
||||||
if (this.actionCur < this.actionList.length - 1) {
|
if (this.actionCur < this.actionList.length - 1) {
|
||||||
this.actionCur++;
|
this.actionCur++;
|
||||||
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case 'fullScreen':
|
|
||||||
this.fullScreenToggle();
|
|
||||||
break;
|
|
||||||
case 'setPosition':
|
|
||||||
this.setPositionToggle();
|
|
||||||
break;
|
|
||||||
case 'scrolling':
|
|
||||||
this.scrollingToggle();
|
|
||||||
break;
|
|
||||||
case 'search':
|
|
||||||
this.searchToggle();
|
|
||||||
break;
|
|
||||||
case 'copyText':
|
|
||||||
this.copyTextToggle();
|
|
||||||
break;
|
|
||||||
case 'refresh':
|
|
||||||
this.refreshBook();
|
|
||||||
break;
|
|
||||||
case 'recentBooks':
|
|
||||||
this.recentBooksToggle();
|
|
||||||
break;
|
|
||||||
case 'offlineMode':
|
|
||||||
this.offlineModeToggle();
|
|
||||||
break;
|
|
||||||
case 'settings':
|
|
||||||
this.settingsToggle();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonActiveClass(button) {
|
buttonClick(action) {
|
||||||
|
const activeClass = this.buttonActiveClass(action);
|
||||||
|
|
||||||
|
this.$refs[action].blur();
|
||||||
|
|
||||||
|
if (activeClass['tool-button-disabled'])
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.doAction({action});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonActiveClass(action) {
|
||||||
const classActive = { 'tool-button-active': true, 'tool-button-active:hover': true };
|
const classActive = { 'tool-button-active': true, 'tool-button-active:hover': true };
|
||||||
const classDisabled = { 'tool-button-disabled': true, 'tool-button-disabled:hover': true };
|
const classDisabled = { 'tool-button-disabled': true, 'tool-button-disabled:hover': true };
|
||||||
let classResult = {};
|
let classResult = {};
|
||||||
|
|
||||||
switch (button) {
|
switch (action) {
|
||||||
case 'loader':
|
case 'loader':
|
||||||
case 'fullScreen':
|
case 'fullScreen':
|
||||||
case 'setPosition':
|
case 'setPosition':
|
||||||
case 'scrolling':
|
case 'scrolling':
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
case 'recentBooks':
|
case 'refresh':
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
|
case 'recentBooks':
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (this[`${button}Active`])
|
if (this.progressActive) {
|
||||||
|
classResult = classDisabled;
|
||||||
|
} else if (this[`${action}Active`]) {
|
||||||
classResult = classActive;
|
classResult = classActive;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
switch (button) {
|
|
||||||
case 'undoAction':
|
case 'undoAction':
|
||||||
if (this.actionCur <= 0)
|
if (this.actionCur <= 0)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
@@ -739,7 +811,7 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
|
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
|
||||||
switch (button) {
|
switch (action) {
|
||||||
case 'undoAction':
|
case 'undoAction':
|
||||||
case 'redoAction':
|
case 'redoAction':
|
||||||
case 'setPosition':
|
case 'setPosition':
|
||||||
@@ -824,6 +896,8 @@ class Reader extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.closeAllTextPages();
|
||||||
|
|
||||||
let url = encodeURI(decodeURI(opts.url));
|
let url = encodeURI(decodeURI(opts.url));
|
||||||
|
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||||
@@ -930,7 +1004,7 @@ class Reader extends Vue {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -954,7 +1028,7 @@ class Reader extends Vue {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,8 +1060,119 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doAction(opts) {
|
||||||
|
let result = true;
|
||||||
|
let {action = '', event = false} = opts;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'loader':
|
||||||
|
this.loaderToggle();
|
||||||
|
break;
|
||||||
|
case 'help':
|
||||||
|
this.helpToggle();
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
this.settingsToggle();
|
||||||
|
break;
|
||||||
|
case 'undoAction':
|
||||||
|
this.undoAction();
|
||||||
|
break;
|
||||||
|
case 'redoAction':
|
||||||
|
this.redoAction();
|
||||||
|
break;
|
||||||
|
case 'fullScreen':
|
||||||
|
this.fullScreenToggle();
|
||||||
|
break;
|
||||||
|
case 'scrolling':
|
||||||
|
this.scrollingToggle();
|
||||||
|
break;
|
||||||
|
case 'stopScrolling':
|
||||||
|
this.stopScrolling();
|
||||||
|
break;
|
||||||
|
case 'setPosition':
|
||||||
|
this.setPositionToggle();
|
||||||
|
break;
|
||||||
|
case 'search':
|
||||||
|
this.searchToggle();
|
||||||
|
break;
|
||||||
|
case 'copyText':
|
||||||
|
this.copyTextToggle();
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
this.refreshBook();
|
||||||
|
break;
|
||||||
|
case 'offlineMode':
|
||||||
|
this.offlineModeToggle();
|
||||||
|
break;
|
||||||
|
case 'recentBooks':
|
||||||
|
this.recentBooksToggle();
|
||||||
|
break;
|
||||||
|
case 'switchToolbar':
|
||||||
|
this.toolBarToggle();
|
||||||
|
break;
|
||||||
|
case 'donate':
|
||||||
|
this.donateToggle();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result && this.activePage == 'TextPage' && this.$refs.page) {
|
||||||
|
result = true;
|
||||||
|
const textPage = this.$refs.page;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'bookBegin':
|
||||||
|
textPage.doHome();
|
||||||
|
break;
|
||||||
|
case 'bookEnd':
|
||||||
|
textPage.doEnd();
|
||||||
|
break;
|
||||||
|
case 'pageBack':
|
||||||
|
textPage.doPageUp();
|
||||||
|
break;
|
||||||
|
case 'pageForward':
|
||||||
|
textPage.doPageDown();
|
||||||
|
break;
|
||||||
|
case 'lineBack':
|
||||||
|
textPage.doUp();
|
||||||
|
break;
|
||||||
|
case 'lineForward':
|
||||||
|
textPage.doDown();
|
||||||
|
break;
|
||||||
|
case 'incFontSize':
|
||||||
|
textPage.doFontSizeInc();
|
||||||
|
break;
|
||||||
|
case 'decFontSize':
|
||||||
|
textPage.doFontSizeDec();
|
||||||
|
break;
|
||||||
|
case 'scrollingSpeedUp':
|
||||||
|
textPage.doScrollingSpeedUp();
|
||||||
|
break;
|
||||||
|
case 'scrollingSpeedDown':
|
||||||
|
textPage.doScrollingSpeedDown();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (this.$root.rootRoute == '/reader') {
|
let result = false;
|
||||||
|
if (this.$root.rootRoute() == '/reader') {
|
||||||
|
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||||
|
return result;
|
||||||
|
|
||||||
let handled = false;
|
let handled = false;
|
||||||
if (!handled && this.helpActive)
|
if (!handled && this.helpActive)
|
||||||
handled = this.$refs.helpPage.keyHook(event);
|
handled = this.$refs.helpPage.keyHook(event);
|
||||||
@@ -1011,92 +1196,40 @@ class Reader extends Vue {
|
|||||||
handled = this.$refs.page.keyHook(event);
|
handled = this.$refs.page.keyHook(event);
|
||||||
|
|
||||||
if (!handled && event.type == 'keydown') {
|
if (!handled && event.type == 'keydown') {
|
||||||
if (event.code == 'Escape')
|
const action = this.$root.readerActionByKeyEvent(event);
|
||||||
this.loaderToggle();
|
|
||||||
|
|
||||||
if (this.activePage == 'TextPage') {
|
if (action == 'loader') {
|
||||||
switch (event.code) {
|
result = this.doAction({action, event});
|
||||||
case 'KeyH':
|
|
||||||
case 'F1':
|
|
||||||
this.helpToggle();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
break;
|
|
||||||
case 'KeyZ':
|
|
||||||
this.scrollingToggle();
|
|
||||||
break;
|
|
||||||
case 'KeyP':
|
|
||||||
this.setPositionToggle();
|
|
||||||
break;
|
|
||||||
case 'KeyF':
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
this.searchToggle();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'KeyC':
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
this.copyTextToggle();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'KeyR':
|
|
||||||
this.refreshBook();
|
|
||||||
break;
|
|
||||||
case 'KeyX':
|
|
||||||
this.recentBooksToggle();
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
break;
|
|
||||||
case 'KeyO':
|
|
||||||
this.offlineModeToggle();
|
|
||||||
break;
|
|
||||||
case 'KeyS':
|
|
||||||
this.settingsToggle();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!result && this.activePage == 'TextPage') {
|
||||||
|
result = this.doAction({action, event});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.el-container {
|
.header {
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-header {
|
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
background-color: #1B695F;
|
background-color: #1B695F;
|
||||||
color: #000;
|
color: #000;
|
||||||
overflow-x: auto;
|
overflow: hidden;
|
||||||
overflow-y: hidden;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.main {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-main {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
background-color: #EBE2C9;
|
background-color: #EBE2C9;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-button {
|
.tool-button {
|
||||||
margin: 0 2px 0 2px;
|
margin: 0px 2px 0 2px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #3E843E;
|
color: #3E843E;
|
||||||
background-color: #E6EDF4;
|
background-color: #E6EDF4;
|
||||||
@@ -1104,15 +1237,14 @@ class Reader extends Vue {
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
width: 38px;
|
width: 38px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
box-shadow: 3px 3px 5px black;
|
box-shadow: 3px 3px 5px black;
|
||||||
}
|
outline: 0;
|
||||||
|
|
||||||
.tool-button + .tool-button {
|
|
||||||
margin: 0 2px 0 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-button:hover {
|
.tool-button:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-button-active {
|
.tool-button-active {
|
||||||
@@ -1127,20 +1259,19 @@ class Reader extends Vue {
|
|||||||
.tool-button-active:hover {
|
.tool-button-active:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #81C581;
|
background-color: #81C581;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-button-disabled {
|
.tool-button-disabled {
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-button-disabled:hover {
|
.tool-button-disabled:hover {
|
||||||
color: lightgray;
|
color: lightgray;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
}
|
cursor: default;
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.space {
|
.space {
|
||||||
@@ -1157,4 +1288,10 @@ i {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
</style>
|
</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,44 +272,39 @@ 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) {
|
||||||
try {
|
try {
|
||||||
await readerApi.checkUrl(fb2path);
|
await readerApi.checkCachedBook(fb2path);
|
||||||
|
|
||||||
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.code == '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,15 +164,16 @@ 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 (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
|
|
||||||
this.showNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -177,17 +177,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) {
|
||||||
|
|||||||
@@ -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.code == '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>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
|||||||
|
<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTableData() {
|
||||||
|
let result = rstore.hotKeys.map(hk => hk.name);
|
||||||
|
|
||||||
|
const search = this.search.toLowerCase();
|
||||||
|
const codesIncludeSearch = (action) => {
|
||||||
|
for (const code of this.value[action]) {
|
||||||
|
if (code.toLowerCase().includes(search))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result = result.filter(item => {
|
||||||
|
return !search ||
|
||||||
|
rstore.readerActions[item].toLowerCase().includes(search) ||
|
||||||
|
codesIncludeSearch(item)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tableData = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCollisions() {
|
||||||
|
const cols = {};
|
||||||
|
for (const [action, codes] of Object.entries(this.value)) {
|
||||||
|
codes.forEach(code => {
|
||||||
|
if (!cols[code])
|
||||||
|
cols[code] = [];
|
||||||
|
if (cols[code].indexOf(action) < 0)
|
||||||
|
cols[code].push(action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const [code, actions] of Object.entries(cols)) {
|
||||||
|
if (actions.length > 1)
|
||||||
|
result[code] = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collisions = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
collisionWarning(code) {
|
||||||
|
if (this.collisions[code]) {
|
||||||
|
const descs = this.collisions[code].map(action => `<b>${rstore.readerActions[action]}</b>`);
|
||||||
|
this.$root.stdDialog.alert(`Сочетание '${code}' одновременно назначено<br>следующим командам:<br>${descs.join('<br>')}<br><br>
|
||||||
|
Возможно неожиданное поведение.`, 'Предупреждение');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCode(action, code) {
|
||||||
|
let codes = Array.from(this.value[action]);
|
||||||
|
const index = codes.indexOf(code);
|
||||||
|
if (index >= 0) {
|
||||||
|
codes.splice(index, 1);
|
||||||
|
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addHotKey(action) {
|
||||||
|
if (this.value[action].length >= this.maxCodesLength)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
|
||||||
|
if (result) {
|
||||||
|
let codes = Array.from(this.value[action]);
|
||||||
|
if (codes.indexOf(result) < 0) {
|
||||||
|
codes.push(result);
|
||||||
|
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.collisionWarning(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async defaultHotKey(action) {
|
||||||
|
try {
|
||||||
|
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
|
||||||
|
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
|
||||||
|
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async defaultHotKeyAll() {
|
||||||
|
try {
|
||||||
|
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
|
||||||
|
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
|
||||||
|
this.$emit('input', newValue);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table {
|
||||||
|
border-left: 1px solid grey;
|
||||||
|
border-top: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
border-right: 1px solid grey;
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:nth-child(even) {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
width: 130px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotKeys {
|
||||||
|
border-left: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: 25px;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const defPalette = [
|
||||||
|
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
|
||||||
|
'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
|
||||||
|
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
|
||||||
|
'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
|
||||||
|
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
|
||||||
|
'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
|
||||||
|
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
|
||||||
|
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
|
||||||
|
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
|
||||||
|
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
|
||||||
|
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
|
||||||
|
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
|
||||||
|
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defPalette;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="part-header">Показывать кнопки панели</div>
|
||||||
|
|
||||||
|
<div class="item row" v-for="item in toolButtons" :key="item.name">
|
||||||
|
<div class="label-3"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="rstore.readerActions[item.name]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<div class="bg-grey-3 row">
|
||||||
|
<q-tabs
|
||||||
|
v-model="selectedKeysTab"
|
||||||
|
active-color="black"
|
||||||
|
active-bg-color="white"
|
||||||
|
indicator-color="white"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
|
>
|
||||||
|
<q-tab name="mouse" label="Мышь/тачскрин" />
|
||||||
|
<q-tab name="keyboard" label="Клавиатура" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mb-sm"/>
|
||||||
|
|
||||||
|
<div class="col tab-panel">
|
||||||
|
<div v-if="selectedKeysTab == 'mouse'">
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-4"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedKeysTab == 'keyboard'">
|
||||||
|
<div class="item row">
|
||||||
|
<UserHotKeys v-model="userHotKeys" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Подсказки, уведомления</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-6">Подсказка</div>
|
||||||
|
<q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать или нет подсказку при каждой загрузке книги
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Подсказка</div>
|
||||||
|
<q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Мерцать сообщением в строке статуса и на кнопке<br>
|
||||||
|
обновления при загрузке книги из кэша
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать уведомления и ошибки от<br>
|
||||||
|
синхронизатора данных с сервером
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showWhatsNewDialog">
|
||||||
|
Показывать уведомление "Что нового"
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать уведомления "Что нового"<br>
|
||||||
|
при каждом выходе новой версии читалки
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||||
|
Показывать "Оплатим хостинг вместе"
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Показывать уведомление "Оплатим хостинг вместе"
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Другое</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Обработка</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Html-фильтр вырезает лишние элементы со<br>
|
||||||
|
страницы для определенных сайтов, таких как:<br>
|
||||||
|
samlib.ru<br>
|
||||||
|
www.fanfiction.net<br>
|
||||||
|
archiveofourown.org<br>
|
||||||
|
и других
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Обработка</div>
|
||||||
|
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Включение этой опции позволяет делать предварительную<br>
|
||||||
|
подготовку всего текста в ленивом режиме сразу после<br>
|
||||||
|
загрузки книги. Это может повысить отзывчивость читалки,<br>
|
||||||
|
но нагружает процессор каждый раз при открытии книги.
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Парам. в URL</div>
|
||||||
|
<q-checkbox size="xs" v-model="allowUrlParamBookPos">
|
||||||
|
Добавлять параметр "__p"
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Добавление параметра "__p" в строке браузера<br>
|
||||||
|
позволяет передавать ссылку на книгу в читалке<br>
|
||||||
|
без потери текущей позиции. Однако в этом случае<br>
|
||||||
|
при листании забивается история браузера, т.к. на<br>
|
||||||
|
каждое изменение позиции происходит смена URL.
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Копирование</div>
|
||||||
|
<q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Загружать весь текст в окно<br>
|
||||||
|
копирования текста со страницы
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Анимация</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-5">Тип</div>
|
||||||
|
<q-select class="col-left" v-model="pageChangeAnimation" :options="pageChangeAnimationOptions"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-5">Скорость</div>
|
||||||
|
<NumInput class="col-left" v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disable="pageChangeAnimation == ''"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Другое</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-5">Страница</div>
|
||||||
|
<q-checkbox v-model="keepLastToFirst" size="xs" label="Переносить последнюю строку">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Переносить последнюю строку страницы<br>
|
||||||
|
в начало следующей при листании
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<div class="part-header">Управление синхронизацией данных</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="serverSyncEnabled">
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Профили устройств</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div class="text col">
|
||||||
|
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
|
||||||
|
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1">Устройство</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
||||||
|
style="width: 275px"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" dense no-caps @click="addProfile">Добавить</q-btn>
|
||||||
|
<q-btn class="button" dense no-caps @click="delProfile">Удалить</q-btn>
|
||||||
|
<q-btn class="button" dense no-caps @click="delAllProfiles">Удалить все</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Ключ доступа</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div class="text col">
|
||||||
|
Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
|
||||||
|
Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" style="width: 250px" dense no-caps @click="showServerStorageKey">
|
||||||
|
<span v-show="serverStorageKeyVisible">Скрыть</span>
|
||||||
|
<span v-show="!serverStorageKeyVisible">Показать</span>
|
||||||
|
ключ доступа
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div v-if="!serverStorageKeyVisible" class="col">
|
||||||
|
<hr/>
|
||||||
|
<b>{{ partialStorageKey }}</b> (часть вашего ключа)
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="col" style="line-height: 100%">
|
||||||
|
<hr/>
|
||||||
|
<div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
|
||||||
|
<b>{{ serverStorageKey }}</b>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
<div v-if="mode == 'omnireader'">
|
||||||
|
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
|
||||||
|
<br><div class="text-center" style="margin-top: 5px">
|
||||||
|
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-1"></div>
|
||||||
|
<div class="text col">
|
||||||
|
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
|
||||||
|
например, после переустановки ОС или чистки/смены браузера.<br>
|
||||||
|
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
|
||||||
|
и шифруются ключом доступа перед отправкой на сервер.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="item row">
|
||||||
|
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
||||||
|
</div>
|
||||||
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<q-tabs
|
||||||
|
v-model="selectedViewTab"
|
||||||
|
active-color="black"
|
||||||
|
active-bg-color="white"
|
||||||
|
indicator-color="white"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
|
>
|
||||||
|
<q-tab name="color" label="Цвет" />
|
||||||
|
<q-tab name="font" label="Шрифт" />
|
||||||
|
<q-tab name="text" label="Текст" />
|
||||||
|
<q-tab name="status" label="Строка статуса" />
|
||||||
|
</q-tabs>
|
||||||
|
|
||||||
|
<div class="q-mb-sm"/>
|
||||||
|
|
||||||
|
<div class="col tab-panel">
|
||||||
|
<div v-if="selectedViewTab == 'color'">
|
||||||
|
@@include('./ViewTab/Color.inc');
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedViewTab == 'font'">
|
||||||
|
@@include('./ViewTab/Font.inc');
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedViewTab == 'text'">
|
||||||
|
@@include('./ViewTab/Text.inc');
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedViewTab == 'status'">
|
||||||
|
@@include('./ViewTab/Status.inc');
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="hidden part-header">Цвет</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Текст</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-input class="col-left no-mp"
|
||||||
|
outlined dense
|
||||||
|
v-model="textColorFiltered"
|
||||||
|
:rules="['hexColor']"
|
||||||
|
style="max-width: 150px"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||||
|
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||||
|
<div>
|
||||||
|
<q-color v-model="textColor"
|
||||||
|
no-header default-view="palette" :palette="predefineTextColors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mt-md"/>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Фон</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-input class="col-left no-mp"
|
||||||
|
outlined dense
|
||||||
|
v-model="bgColorFiltered"
|
||||||
|
:rules="['hexColor']"
|
||||||
|
style="max-width: 150px"
|
||||||
|
:disable="wallpaper != ''"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||||
|
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||||
|
<div>
|
||||||
|
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="hidden part-header">Шрифт</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Локальный/веб</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-select class="col-left" v-model="fontName" :options="fontsOptions" :disable="webFontName != ''"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<q-select class="col" v-model="webFontName" :options="webFontsOptions"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Веб шрифты дают большое разнообразие,<br>
|
||||||
|
однако есть шанс, что шрифт будет загружаться<br>
|
||||||
|
очень медленно или вовсе не загрузится
|
||||||
|
</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Размер</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="fontSize" :min="5" :max="200"/>
|
||||||
|
|
||||||
|
<div class="col q-pt-xs text-right">
|
||||||
|
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Сдвиг</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="vertShift" :min="-100" :max="100">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Сдвиг шрифта по вертикали в процентах от размера.<br>
|
||||||
|
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||||
|
вниз. Значение зависит от метрики шрифта.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Стиль</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="fontBold" size="xs" label="Жирный" />
|
||||||
|
<q-checkbox class="q-ml-sm" v-model="fontItalic" size="xs" label="Курсив" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="hidden part-header">Строка статуса</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Статус</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||||
|
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Высота</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Прозрачность</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
По клику на автора-название в строке статуса<br>
|
||||||
|
открывать оригинал произведения в новой вкладке
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="hidden part-header">Текст</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Интервал</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="lineInterval" :min="0" :max="200"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Параграф</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="p" :min="0" :max="2000"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Отступ</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Слева/справа
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Сверху/снизу
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Сдвиг</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="textVertShift" :min="-100" :max="100">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
|
||||||
|
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||||
|
вниз.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Скроллинг</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="scrollingDelay" :min="1" :max="10000">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Замедление скроллинга в миллисекундах.<br>
|
||||||
|
Определяет время, за которое текст<br>
|
||||||
|
прокручивается на одну строку.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<q-select class="col" v-model="scrollingType" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Вид скроллинга: линейный,<br>
|
||||||
|
ускорение-замедление и пр.
|
||||||
|
</q-tooltip>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Выравнивание</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="textAlignJustify" size="xs" label="По ширине" />
|
||||||
|
<q-checkbox class="q-ml-sm" v-model="wordWrap" size="xs" label="Перенос по слогам" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col-left column justify-center text-right">
|
||||||
|
Компактность
|
||||||
|
</div>
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<NumInput class="col" v-model="compactTextPerc" :min="0" :max="100">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Степень компактности текста в процентах.<br>
|
||||||
|
Чем больше компактность, тем хуже выравнивание<br>
|
||||||
|
по правому краю.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Обработка</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col-left column justify-center text-right">
|
||||||
|
Добавлять пустые
|
||||||
|
</div>
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<NumInput class="col" v-model="addEmptyParagraphs" :min="0" :max="2"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Изображения</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="showImages" size="xs" label="Показывать" />
|
||||||
|
<q-checkbox class="q-ml-sm" v-model="showInlineImagesInCenter" @input="needReload" :disable="!showImages" size="xs" label="Инлайн в центр">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Выносить все изображения в центр экрана
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col-left column justify-center text-right">
|
||||||
|
Высота не более
|
||||||
|
</div>
|
||||||
|
<div class="q-px-sm"/>
|
||||||
|
<NumInput class="col" v-model="imageHeightLines" :min="1" :max="100" :disable="!showImages">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Определяет высоту изображения количеством строк.<br>
|
||||||
|
В случае превышения высоты, изображение будет<br>
|
||||||
|
уменьшено с сохранением пропорций так, чтобы<br>
|
||||||
|
помещаться в указанное количество строк.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
@@ -21,13 +21,15 @@
|
|||||||
@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="width: 0px; height: 0px"></canvas>
|
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||||
|
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -37,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';
|
||||||
@@ -130,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() {
|
||||||
@@ -143,6 +149,8 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
calcDrawProps() {
|
calcDrawProps() {
|
||||||
|
const wideLetter = 'Щ';
|
||||||
|
|
||||||
//preloaded fonts
|
//preloaded fonts
|
||||||
this.fontList = [`12px ${this.fontName}`];
|
this.fontList = [`12px ${this.fontName}`];
|
||||||
|
|
||||||
@@ -199,6 +207,22 @@ class TextPage extends Vue {
|
|||||||
this.drawHelper.lineHeight = this.lineHeight;
|
this.drawHelper.lineHeight = this.lineHeight;
|
||||||
this.drawHelper.context = this.context;
|
this.drawHelper.context = this.context;
|
||||||
|
|
||||||
|
//альтернатива context.measureText
|
||||||
|
if (!this.context.measureText(wideLetter).width) {
|
||||||
|
const ctx = this.$refs.measureWidth;
|
||||||
|
this.drawHelper.measureText = function(text, style) {
|
||||||
|
ctx.innerText = text;
|
||||||
|
ctx.style.font = this.fontByStyle(style);
|
||||||
|
return ctx.clientWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.drawHelper.measureTextFont = function(text, font) {
|
||||||
|
ctx.innerText = text;
|
||||||
|
ctx.style.font = font;
|
||||||
|
return ctx.clientWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//statusBar
|
//statusBar
|
||||||
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
||||||
|
|
||||||
@@ -211,8 +235,10 @@ class TextPage extends Vue {
|
|||||||
this.parsed.wordWrap = this.wordWrap;
|
this.parsed.wordWrap = this.wordWrap;
|
||||||
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
|
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
|
||||||
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
|
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
|
||||||
let t = '';
|
let t = wideLetter;
|
||||||
while (this.drawHelper.measureText(t, {}) < this.w) t += 'Щ';
|
if (!this.drawHelper.measureText(t, {}))
|
||||||
|
throw new Error('Ошибка measureText');
|
||||||
|
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
|
||||||
this.parsed.maxWordLength = t.length - 1;
|
this.parsed.maxWordLength = t.length - 1;
|
||||||
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
|
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
|
||||||
this.parsed.lineHeight = this.lineHeight;
|
this.parsed.lineHeight = this.lineHeight;
|
||||||
@@ -221,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
|
||||||
@@ -247,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)
|
||||||
@@ -277,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() {
|
||||||
@@ -330,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +380,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
if (this.lastBook) {
|
if (this.lastBook) {
|
||||||
(async() => {
|
(async() => {
|
||||||
|
try {
|
||||||
//подождем ленивый парсинг
|
//подождем ленивый парсинг
|
||||||
this.stopLazyParse = true;
|
this.stopLazyParse = true;
|
||||||
while (this.doingLazyParse) await sleep(10);
|
while (this.doingLazyParse) await sleep(10);
|
||||||
@@ -404,11 +417,14 @@ class TextPage extends Vue {
|
|||||||
this.statusBar = null;
|
this.statusBar = null;
|
||||||
await this.stopTextScrolling();
|
await this.stopTextScrolling();
|
||||||
|
|
||||||
this.calcPropsAndLoadFonts();
|
await this.calcPropsAndLoadFonts();
|
||||||
|
|
||||||
this.refreshTime();
|
this.refreshTime();
|
||||||
if (this.lazyParseEnabled)
|
if (this.lazyParseEnabled)
|
||||||
this.lazyParsePara();
|
this.lazyParsePara();
|
||||||
|
} catch (e) {
|
||||||
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,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() {
|
||||||
@@ -488,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,22 +884,26 @@ 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});
|
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
||||||
this.commit('reader/setSettings', newSettings);
|
this.commit('reader/setSettings', newSettings);
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
@@ -924,69 +944,6 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -1064,7 +1021,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;
|
||||||
@@ -1091,7 +1048,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,7 +1073,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'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,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,7 +115,6 @@ 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),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,10 +130,10 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,15 +142,10 @@ 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 (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
|
||||||
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;
|
||||||
@@ -605,6 +598,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 +614,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 +626,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 +642,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 = '';//измеряемая строка
|
||||||
@@ -665,6 +660,7 @@ export default class BookParser {
|
|||||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||||
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) {
|
||||||
@@ -835,6 +831,12 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//parsed.visible
|
||||||
|
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,76 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
showUntil: '2020-05-20',
|
||||||
|
header: '0.9.3 (2020-05-21)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-04-25',
|
||||||
|
header: '0.9.2 (2020-03-15)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
|
||||||
|
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-03-02',
|
||||||
|
header: '0.9.1 (2020-03-03)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>улучшение работы серверной части</li>
|
||||||
|
<li>незначительные изменения интерфейса</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-02-25',
|
||||||
|
header: '0.9.0 (2020-02-26)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>переход на UI-фреймфорк Quasar</li>
|
||||||
|
<li>незначительные изменения интерфейса</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-02-05',
|
||||||
|
header: '0.8.4 (2020-02-06)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлен paypal-адрес для пожертвований</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-01-27',
|
||||||
|
header: '0.8.3 (2020-01-28)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||||
|
<li>внутренние оптимизации</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
showUntil: '2020-01-19',
|
showUntil: '2020-01-19',
|
||||||
header: '0.8.2 (2020-01-20)',
|
header: '0.8.2 (2020-01-20)',
|
||||||
|
|||||||
@@ -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
64
client/components/share/Dialog.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog v-model="active">
|
||||||
|
<div class="column bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col q-mx-md">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-end q-pa-md">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
const DialogProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
})
|
||||||
|
class Dialog extends DialogProps {
|
||||||
|
get active() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set active(value) {
|
||||||
|
this.$emit('input', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
font-size: 110%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
client/components/share/Notify.vue
Normal file
58
client/components/share/Notify.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hidden"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
})
|
||||||
|
class Notify extends Vue {
|
||||||
|
notify(opts) {
|
||||||
|
let {
|
||||||
|
caption = null,
|
||||||
|
captionColor = 'black',
|
||||||
|
color = 'positive',
|
||||||
|
icon = '',
|
||||||
|
iconColor = 'white',
|
||||||
|
message = '',
|
||||||
|
messageColor = 'black',
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
|
||||||
|
return this.$q.notify({
|
||||||
|
position: 'top-right',
|
||||||
|
color,
|
||||||
|
textColor: iconColor,
|
||||||
|
icon,
|
||||||
|
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
|
||||||
|
html: true,
|
||||||
|
|
||||||
|
message:
|
||||||
|
`<div style="max-width: 350px;">
|
||||||
|
${caption}
|
||||||
|
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message, caption) {
|
||||||
|
this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(message, caption) {
|
||||||
|
this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message, caption) {
|
||||||
|
this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message, caption) {
|
||||||
|
this.notify({color: 'info', icon: 'la la-bell', message, caption});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
185
client/components/share/NumInput.vue
Normal file
185
client/components/share/NumInput.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<q-input outlined dense
|
||||||
|
v-model="filteredValue"
|
||||||
|
input-style="text-align: center"
|
||||||
|
class="no-mp"
|
||||||
|
:class="(error ? 'error' : '')"
|
||||||
|
:disable="disable"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon :class="(validate(value - step) ? '' : 'disable')"
|
||||||
|
name="la la-minus-circle"
|
||||||
|
class="button"
|
||||||
|
v-ripple="validate(value - step)"
|
||||||
|
@click="minus"
|
||||||
|
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
|
||||||
|
@mouseup.prevent.stop="onMouseUp"
|
||||||
|
@mouseout.prevent.stop="onMouseUp"
|
||||||
|
@touchstart.stop="onTouchStart($event, 'minus')"
|
||||||
|
@touchend.stop="onTouchEnd"
|
||||||
|
@touchcancel.prevent.stop="onTouchEnd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon :class="(validate(value + step) ? '' : 'disable')"
|
||||||
|
name="la la-plus-circle"
|
||||||
|
class="button"
|
||||||
|
v-ripple="validate(value + step)"
|
||||||
|
@click="plus"
|
||||||
|
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
|
||||||
|
@mouseup.prevent.stop="onMouseUp"
|
||||||
|
@mouseout.prevent.stop="onMouseUp"
|
||||||
|
@touchstart.stop="onTouchStart($event, 'plus')"
|
||||||
|
@touchend.stop="onTouchEnd"
|
||||||
|
@touchcancel.prevent.stop="onTouchEnd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
|
const NumInputProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
value: Number,
|
||||||
|
min: { type: Number, default: -Number.MAX_VALUE },
|
||||||
|
max: { type: Number, default: Number.MAX_VALUE },
|
||||||
|
step: { type: Number, default: 1 },
|
||||||
|
digits: { type: Number, default: 0 },
|
||||||
|
disable: Boolean
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
watch: {
|
||||||
|
filteredValue: function(newValue) {
|
||||||
|
if (this.validate(newValue)) {
|
||||||
|
this.error = false;
|
||||||
|
this.$emit('input', this.string2number(newValue));
|
||||||
|
} else {
|
||||||
|
this.error = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: function(newValue) {
|
||||||
|
this.filteredValue = newValue;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class NumInput extends NumInputProps {
|
||||||
|
filteredValue = 0;
|
||||||
|
error = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.filteredValue = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
string2number(value) {
|
||||||
|
return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(value) {
|
||||||
|
let n = this.string2number(value);
|
||||||
|
if (isNaN(n))
|
||||||
|
return false;
|
||||||
|
if (n < this.min)
|
||||||
|
return false;
|
||||||
|
if (n > this.max)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
plus() {
|
||||||
|
const newValue = this.value + this.step;
|
||||||
|
if (this.validate(newValue))
|
||||||
|
this.filteredValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
minus() {
|
||||||
|
const newValue = this.value - this.step;
|
||||||
|
if (this.validate(newValue))
|
||||||
|
this.filteredValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(event, way) {
|
||||||
|
this.startClickRepeat = true;
|
||||||
|
this.clickRepeat = false;
|
||||||
|
|
||||||
|
if (event.button == 0) {
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(300);
|
||||||
|
if (this.startClickRepeat) {
|
||||||
|
this.clickRepeat = true;
|
||||||
|
while (this.clickRepeat) {
|
||||||
|
if (way == 'plus') {
|
||||||
|
this.plus();
|
||||||
|
} else {
|
||||||
|
this.minus();
|
||||||
|
}
|
||||||
|
await utils.sleep(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp() {
|
||||||
|
if (this.inTouch)
|
||||||
|
return;
|
||||||
|
this.startClickRepeat = false;
|
||||||
|
this.clickRepeat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart(event, way) {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1) {
|
||||||
|
this.inTouch = true;
|
||||||
|
this.onMouseDown({button: 0}, way);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchEnd() {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
this.inTouch = false;
|
||||||
|
this.onMouseUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.no-mp {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: 130%;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #bbb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
color: #616161;
|
||||||
|
background-color: #efebe9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ffabab;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disable, .disable:hover {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #bbb;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
333
client/components/share/StdDialog.vue
Normal file
333
client/components/share/StdDialog.vue
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
<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="las la-exclamation-circle" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="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="las la-exclamation-circle" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="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="las la-exclamation-circle" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="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="las la-exclamation-circle" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn flat round dense v-close-popup>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="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 = '';
|
||||||
|
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.iconColor = 'text-warning';
|
||||||
|
if (opts && opts.color) {
|
||||||
|
this.iconColor = `text-${opts.color}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hotKeyCode = '';
|
||||||
|
if (opts && opts.hotKeyCode) {
|
||||||
|
this.hotKeyCode = opts.hotKeyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHide() {
|
||||||
|
if (this.hideTrigger) {
|
||||||
|
this.hideTrigger();
|
||||||
|
this.hideTrigger = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow() {
|
||||||
|
if (this.type == 'prompt') {
|
||||||
|
this.enableValidator = true;
|
||||||
|
if (this.inputValue)
|
||||||
|
this.validate(this.inputValue);
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(value) {
|
||||||
|
if (!this.enableValidator)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.inputValidator) {
|
||||||
|
const result = this.inputValidator(value);
|
||||||
|
if (result !== true) {
|
||||||
|
this.error = result;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.error = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
okClick() {
|
||||||
|
if (this.type == 'prompt' && !this.validate(this.inputValue)) {
|
||||||
|
this.$refs.dialog.shake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let handled = false;
|
||||||
|
if (this.type == 'hotKey') {
|
||||||
|
if (event.type == 'keydown') {
|
||||||
|
this.hotKeyCode = utils.keyEventToCode(event);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.code == 'Enter') {
|
||||||
|
this.okClick();
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code == '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,13 @@
|
|||||||
<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 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>
|
<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>
|
||||||
@@ -116,23 +117,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 +139,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 +162,5 @@ class Window extends Vue {
|
|||||||
.close-button:hover {
|
.close-button:hover {
|
||||||
background-color: #69C05F;
|
background-color: #69C05F;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
/*
|
|
||||||
import ElementUI from 'element-ui';
|
|
||||||
import './theme/index.css';
|
|
||||||
import locale from 'element-ui/lib/locale/lang/ru-RU';
|
|
||||||
|
|
||||||
Vue.use(ElementUI, { locale });
|
|
||||||
*/
|
|
||||||
|
|
||||||
//------------------------------------------------------
|
|
||||||
import './theme/index.css';
|
|
||||||
|
|
||||||
import ElMenu from 'element-ui/lib/menu';
|
|
||||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
|
||||||
import ElButton from 'element-ui/lib/button';
|
|
||||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
|
||||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
|
||||||
import ElTabs from 'element-ui/lib/tabs';
|
|
||||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
|
||||||
import ElTooltip from 'element-ui/lib/tooltip';
|
|
||||||
import ElCol from 'element-ui/lib/col';
|
|
||||||
import ElContainer from 'element-ui/lib/container';
|
|
||||||
import ElAside from 'element-ui/lib/aside';
|
|
||||||
import ElHeader from 'element-ui/lib/header';
|
|
||||||
import ElMain from 'element-ui/lib/main';
|
|
||||||
import ElInput from 'element-ui/lib/input';
|
|
||||||
import ElInputNumber from 'element-ui/lib/input-number';
|
|
||||||
import ElSelect from 'element-ui/lib/select';
|
|
||||||
import ElOption from 'element-ui/lib/option';
|
|
||||||
import ElTable from 'element-ui/lib/table';
|
|
||||||
import ElTableColumn from 'element-ui/lib/table-column';
|
|
||||||
import ElProgress from 'element-ui/lib/progress';
|
|
||||||
import ElSlider from 'element-ui/lib/slider';
|
|
||||||
import ElForm from 'element-ui/lib/form';
|
|
||||||
import ElFormItem from 'element-ui/lib/form-item';
|
|
||||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
|
||||||
import ElDialog from 'element-ui/lib/dialog';
|
|
||||||
|
|
||||||
import Notification from 'element-ui/lib/notification';
|
|
||||||
import Loading from 'element-ui/lib/loading';
|
|
||||||
import MessageBox from 'element-ui/lib/message-box';
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
|
||||||
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
|
||||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
|
||||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
|
||||||
ElColorPicker, ElDialog,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let name in components) {
|
|
||||||
Vue.component(name, components[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Vue.use(Loading.directive);
|
|
||||||
|
|
||||||
Vue.prototype.$loading = Loading.service;
|
|
||||||
Vue.prototype.$msgbox = MessageBox;
|
|
||||||
Vue.prototype.$alert = MessageBox.alert;
|
|
||||||
Vue.prototype.$confirm = MessageBox.confirm;
|
|
||||||
Vue.prototype.$prompt = MessageBox.prompt;
|
|
||||||
Vue.prototype.$notify = Notification;
|
|
||||||
//Vue.prototype.$message = Message;
|
|
||||||
|
|
||||||
import lang from 'element-ui/lib/locale/lang/ru-RU';
|
|
||||||
import locale from 'element-ui/lib/locale';
|
|
||||||
locale.use(lang);
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<!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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
//Vue.config.productionTip = false;
|
//Vue.config.productionTip = false;
|
||||||
|
|||||||
89
client/quasar.js
Normal file
89
client/quasar.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
//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);
|
||||||
@@ -194,3 +194,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,73 @@
|
|||||||
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
|
const readerActions = {
|
||||||
|
'help': 'Вызвать cправку',
|
||||||
|
'loader': 'На страницу загрузки',
|
||||||
|
'settings': 'Настроить',
|
||||||
|
'undoAction': 'Действие назад',
|
||||||
|
'redoAction': 'Действие вперед',
|
||||||
|
'fullScreen': 'На весь экран',
|
||||||
|
'scrolling': 'Плавный скроллинг',
|
||||||
|
'stopScrolling': '',
|
||||||
|
'setPosition': 'Установить позицию',
|
||||||
|
'search': 'Найти в тексте',
|
||||||
|
'copyText': 'Скопировать текст со страницы',
|
||||||
|
'refresh': 'Принудительно обновить книгу',
|
||||||
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
|
'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: 'refresh', show: true},
|
||||||
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
|
{name: 'offlineMode', show: false},
|
||||||
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
{name: 'recentBooks', show: true},
|
||||||
|
];
|
||||||
|
|
||||||
|
//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: 'refresh', codes: ['R']},
|
||||||
|
{name: 'offlineMode', codes: ['O']},
|
||||||
|
{name: 'recentBooks', codes: ['X']},
|
||||||
|
|
||||||
|
{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 +194,7 @@ const webFonts = [
|
|||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//----------------------------------------------------------------------------------------------------------
|
||||||
const settingDefaults = {
|
const settingDefaults = {
|
||||||
textColor: '#000000',
|
textColor: '#000000',
|
||||||
backgroundColor: '#EBE2C9',
|
backgroundColor: '#EBE2C9',
|
||||||
@@ -160,11 +219,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,
|
||||||
@@ -182,10 +242,12 @@ const settingDefaults = {
|
|||||||
imageFitWidth: true,
|
imageFitWidth: true,
|
||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
|
showDonationDialog2020: true,
|
||||||
enableSitesFilter: true,
|
enableSitesFilter: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
|
userHotKeys: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
@@ -194,6 +256,8 @@ 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;
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
const state = {
|
const state = {
|
||||||
@@ -204,6 +268,7 @@ const state = {
|
|||||||
profilesRev: 0,
|
profilesRev: 0,
|
||||||
allowProfilesSave: false,//подстраховка для разработки
|
allowProfilesSave: false,//подстраховка для разработки
|
||||||
whatsNewContentHash: '',
|
whatsNewContentHash: '',
|
||||||
|
donationRemindDate: '',
|
||||||
currentProfile: '',
|
currentProfile: '',
|
||||||
settings: Object.assign({}, settingDefaults),
|
settings: Object.assign({}, settingDefaults),
|
||||||
settingsRev: {},
|
settingsRev: {},
|
||||||
@@ -238,6 +303,9 @@ const mutations = {
|
|||||||
setWhatsNewContentHash(state, value) {
|
setWhatsNewContentHash(state, value) {
|
||||||
state.whatsNewContentHash = value;
|
state.whatsNewContentHash = value;
|
||||||
},
|
},
|
||||||
|
setDonationRemindDate(state, value) {
|
||||||
|
state.donationRemindDate = value;
|
||||||
|
},
|
||||||
setCurrentProfile(state, value) {
|
setCurrentProfile(state, value) {
|
||||||
state.currentProfile = value;
|
state.currentProfile = value;
|
||||||
},
|
},
|
||||||
@@ -250,7 +318,9 @@ const mutations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
readerActions,
|
||||||
toolButtons,
|
toolButtons,
|
||||||
|
hotKeys,
|
||||||
fonts,
|
fonts,
|
||||||
webFonts,
|
webFonts,
|
||||||
settingDefaults,
|
settingDefaults,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
48
docs/beta.omnireader/beta.omnireader
Normal file
48
docs/beta.omnireader/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.omnireader/deploy.sh
Executable file
4
docs/beta.omnireader/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
|
||||||
11
docs/beta.omnireader/run_server.sh
Executable file
11
docs/beta.omnireader/run_server.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
sudo -H -u www-data bash -c "\
|
||||||
|
while true; do\
|
||||||
|
trap '' 2;\
|
||||||
|
cd /var/www;\
|
||||||
|
/home/beta.liberama/liberama;\
|
||||||
|
trap 2;\
|
||||||
|
echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
|
||||||
|
sleep 5;\
|
||||||
|
done;"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
npm run build:linux
|
npm run build:linux
|
||||||
sudo -u www-data cp -r ../../dist/linux/* /home/liberama
|
sudo -u www-data cp -r ../../dist/linux/* /home/liberama
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ server {
|
|||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
@@ -29,7 +30,7 @@ server {
|
|||||||
root /home/liberama/public;
|
root /home/liberama/public;
|
||||||
|
|
||||||
location /tmp {
|
location /tmp {
|
||||||
add_header Content-Type text/xml;
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ server {
|
|||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
@@ -24,7 +25,7 @@ server {
|
|||||||
root /home/liberama/public;
|
root /home/liberama/public;
|
||||||
|
|
||||||
location /tmp {
|
location /tmp {
|
||||||
add_header Content-Type text/xml;
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data sh -c "cd /var/www; /home/liberama/liberama"
|
sudo -H -u www-data bash -c "\
|
||||||
|
while true; do\
|
||||||
|
trap '' 2;\
|
||||||
|
cd /var/www;\
|
||||||
|
/home/liberama/liberama;\
|
||||||
|
trap 2;\
|
||||||
|
echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
|
||||||
|
sleep 5;\
|
||||||
|
done;"
|
||||||
|
|||||||
3836
package-lock.json
generated
3836
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.8.2",
|
"version": "0.9.3",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.22.1",
|
"babel-core": "^6.22.1",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^7.1.1",
|
"babel-loader": "^7.1.1",
|
||||||
"babel-plugin-component": "^1.1.1",
|
"babel-plugin-component": "^1.1.1",
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
"clean-webpack-plugin": "^1.0.1",
|
"clean-webpack-plugin": "^1.0.1",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"element-theme-chalk": "^2.12.0",
|
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-plugin-html": "^5.0.5",
|
"eslint-plugin-html": "^5.0.5",
|
||||||
"eslint-plugin-node": "^8.0.0",
|
"eslint-plugin-node": "^8.0.0",
|
||||||
@@ -41,26 +40,26 @@
|
|||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"mini-css-extract-plugin": "^0.5.0",
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"pkg": "^4.4.2",
|
"pkg": "^4.4.4",
|
||||||
"terser-webpack-plugin": "^1.4.1",
|
"terser-webpack-plugin": "^1.4.1",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "^1.1.2",
|
||||||
"vue-class-component": "^6.3.2",
|
"vue-class-component": "^6.3.2",
|
||||||
"vue-loader": "^15.7.1",
|
"vue-loader": "^15.9.0",
|
||||||
"vue-style-loader": "^4.1.2",
|
"vue-style-loader": "^4.1.2",
|
||||||
"vue-template-compiler": "^2.6.10",
|
"vue-template-compiler": "^2.6.11",
|
||||||
"webpack": "^4.39.3",
|
"webpack": "^4.42.0",
|
||||||
"webpack-cli": "^3.3.7",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-middleware": "^3.7.1",
|
"webpack-dev-middleware": "^3.7.2",
|
||||||
"webpack-hot-middleware": "^2.25.0",
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.2",
|
||||||
|
"workbox-webpack-plugin": "^5.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"appcache-webpack-plugin": "^1.4.0",
|
"@quasar/extras": "^1.5.2",
|
||||||
"axios": "^0.18.1",
|
"axios": "^0.18.1",
|
||||||
"base-x": "^3.0.6",
|
"base-x": "^3.0.8",
|
||||||
"chardet": "^0.7.0",
|
"chardet": "^0.7.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"element-ui": "^2.12.0",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fg-loadcss": "^2.1.0",
|
"fg-loadcss": "^2.1.0",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
@@ -71,21 +70,21 @@
|
|||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"node-stream-zip": "^1.8.2",
|
"pako": "^1.0.11",
|
||||||
"pako": "^1.0.10",
|
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
|
"quasar": "^1.11.3",
|
||||||
"safe-buffer": "^5.2.0",
|
"safe-buffer": "^5.2.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"sql-template-strings": "^2.2.2",
|
"sql-template-strings": "^2.2.2",
|
||||||
"sqlite": "^3.0.3",
|
"sqlite": "^3.0.3",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"unbzip2-stream": "^1.3.3",
|
"unbzip2-stream": "^1.3.3",
|
||||||
"vue": "github:paulkamer/vue#fix_palemoon_clickhandlers_dist",
|
"vue": "github:bookpauk/vue",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.6",
|
||||||
"vuex": "^3.1.1",
|
"vuex": "^3.1.2",
|
||||||
"vuex-persistedstate": "^2.5.4",
|
"vuex-persistedstate": "^2.7.1",
|
||||||
"webdav": "^2.10.1",
|
"webdav": "^2.10.2",
|
||||||
"ws": "^7.2.1",
|
"ws": "^7.2.1",
|
||||||
"zip-stream": "^2.1.2"
|
"zip-stream": "^2.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ module.exports = {
|
|||||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||||
|
|
||||||
useExternalBookConverter: false,
|
useExternalBookConverter: false,
|
||||||
|
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
|
||||||
|
|
||||||
db: [
|
db: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
class MiscController extends BaseController {
|
class MiscController extends BaseController {
|
||||||
async getConfig(req, res) {
|
async getConfig(req, res) {
|
||||||
if (Array.isArray(req.body.params))
|
if (Array.isArray(req.body.params)) {
|
||||||
return _.pick(this.config, req.body.params);
|
const paramsSet = new Set(req.body.params);
|
||||||
|
|
||||||
|
return _.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x)));
|
||||||
|
}
|
||||||
//bad request
|
//bad request
|
||||||
res.status(400).send({error: 'params is not an array'});
|
res.status(400).send({error: 'params is not an array'});
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
const WebSocket = require ('ws');
|
const WebSocket = require ('ws');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
||||||
|
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
|
||||||
const WorkerState = require('../core/WorkerState');//singleton
|
const WorkerState = require('../core/WorkerState');//singleton
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
const utils = require('../core/utils');
|
const utils = require('../core/utils');
|
||||||
|
|
||||||
const cleanPeriod = 1*60*1000;//1 минута
|
const cleanPeriod = 1*60*1000;//1 минута
|
||||||
@@ -8,6 +13,10 @@ const closeSocketOnIdle = 5*60*1000;//5 минут
|
|||||||
class WebSocketController {
|
class WebSocketController {
|
||||||
constructor(wss, config) {
|
constructor(wss, config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
|
this.readerStorage = new ReaderStorage();
|
||||||
|
this.readerWorker = new ReaderWorker(config);
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
|
|
||||||
this.wss = wss;
|
this.wss = wss;
|
||||||
@@ -37,15 +46,25 @@ class WebSocketController {
|
|||||||
async onMessage(ws, message) {
|
async onMessage(ws, message) {
|
||||||
let req = {};
|
let req = {};
|
||||||
try {
|
try {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
ws.lastActivity = Date.now();
|
ws.lastActivity = Date.now();
|
||||||
req = JSON.parse(message);
|
req = JSON.parse(message);
|
||||||
switch (req.action) {
|
switch (req.action) {
|
||||||
case 'test':
|
case 'test':
|
||||||
this.test(req, ws); break;
|
await this.test(req, ws); break;
|
||||||
|
case 'get-config':
|
||||||
|
await this.getConfig(req, ws); break;
|
||||||
case 'worker-get-state':
|
case 'worker-get-state':
|
||||||
this.workerGetState(req, ws); break;
|
await this.workerGetState(req, ws); break;
|
||||||
case 'worker-get-state-finish':
|
case 'worker-get-state-finish':
|
||||||
this.workerGetStateFinish(req, ws); break;
|
await this.workerGetStateFinish(req, ws); break;
|
||||||
|
case 'reader-restore-cached-file':
|
||||||
|
await this.readerRestoreCachedFile(req, ws); break;
|
||||||
|
case 'reader-storage':
|
||||||
|
await this.readerStorageDo(req, ws); break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Action not found: ${req.action}`);
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
@@ -58,10 +77,17 @@ class WebSocketController {
|
|||||||
send(res, req, ws) {
|
send(res, req, ws) {
|
||||||
if (ws.readyState == WebSocket.OPEN) {
|
if (ws.readyState == WebSocket.OPEN) {
|
||||||
ws.lastActivity = Date.now();
|
ws.lastActivity = Date.now();
|
||||||
let r = Object.assign({}, res);
|
let r = res;
|
||||||
if (req.requestId)
|
if (req.requestId)
|
||||||
r.requestId = req.requestId;
|
r = Object.assign({requestId: req.requestId}, r);
|
||||||
ws.send(JSON.stringify(r));
|
|
||||||
|
const message = JSON.stringify(r);
|
||||||
|
ws.send(message);
|
||||||
|
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +96,16 @@ class WebSocketController {
|
|||||||
this.send({message: 'Liberama project is awesome'}, req, ws);
|
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getConfig(req, ws) {
|
||||||
|
if (Array.isArray(req.params)) {
|
||||||
|
const paramsSet = new Set(req.params);
|
||||||
|
|
||||||
|
this.send(_.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))), req, ws);
|
||||||
|
} else {
|
||||||
|
throw new Error('params is not an array');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async workerGetState(req, ws) {
|
async workerGetState(req, ws) {
|
||||||
if (!req.workerId)
|
if (!req.workerId)
|
||||||
throw new Error(`key 'workerId' is wrong`);
|
throw new Error(`key 'workerId' is wrong`);
|
||||||
@@ -88,9 +124,10 @@ class WebSocketController {
|
|||||||
while (1) {// eslint-disable-line no-constant-condition
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
const prevProgress = state.progress || -1;
|
const prevProgress = state.progress || -1;
|
||||||
const prevState = state.state || '';
|
const prevState = state.state || '';
|
||||||
|
const lastModified = state.lastModified || 0;
|
||||||
state = this.workerState.getState(req.workerId);
|
state = this.workerState.getState(req.workerId);
|
||||||
|
|
||||||
this.send((state ? state : {}), req, ws);
|
this.send((state && lastModified != state.lastModified ? state : {}), req, ws);
|
||||||
if (!state) break;
|
if (!state) break;
|
||||||
|
|
||||||
if (state.state != 'finish' && state.state != 'error')
|
if (state.state != 'finish' && state.state != 'error')
|
||||||
@@ -106,6 +143,25 @@ class WebSocketController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async readerRestoreCachedFile(req, ws) {
|
||||||
|
if (!req.path)
|
||||||
|
throw new Error(`key 'path' is empty`);
|
||||||
|
|
||||||
|
const workerId = this.readerWorker.restoreCachedFile(req.path);
|
||||||
|
const state = this.workerState.getState(workerId);
|
||||||
|
this.send((state ? state : {}), req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readerStorageDo(req, ws) {
|
||||||
|
if (!req.body)
|
||||||
|
throw new Error(`key 'body' is empty`);
|
||||||
|
if (!req.body.action)
|
||||||
|
throw new Error(`key 'action' is empty`);
|
||||||
|
if (!req.body.items || Array.isArray(req.body.data))
|
||||||
|
throw new Error(`key 'items' is empty`);
|
||||||
|
|
||||||
|
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WebSocketController;
|
module.exports = WebSocketController;
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ class AppLogger {
|
|||||||
loggerParams = [
|
loggerParams = [
|
||||||
{log: 'ConsoleLog'},
|
{log: 'ConsoleLog'},
|
||||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`},
|
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`},
|
||||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO]},
|
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
|
||||||
|
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.fatal.log`, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ const zlib = require('zlib');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const unbzip2Stream = require('unbzip2-stream');
|
const unbzip2Stream = require('unbzip2-stream');
|
||||||
const tar = require('tar-fs');
|
const tar = require('tar-fs');
|
||||||
const ZipStreamer = require('./ZipStreamer');
|
const iconv = require('iconv-lite');
|
||||||
|
|
||||||
|
const ZipStreamer = require('./Zip/ZipStreamer');
|
||||||
const appLogger = new (require('./AppLogger'))();//singleton
|
const appLogger = new (require('./AppLogger'))();//singleton
|
||||||
const utils = require('./utils');
|
|
||||||
const FileDetector = require('./FileDetector');
|
const FileDetector = require('./FileDetector');
|
||||||
|
const textUtils = require('./Reader/BookConverter/textUtils');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
class FileDecompressor {
|
class FileDecompressor {
|
||||||
constructor() {
|
constructor(limitFileSize = 0) {
|
||||||
this.detector = new FileDetector();
|
this.detector = new FileDetector();
|
||||||
|
this.limitFileSize = limitFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
async decompressNested(filename, outputDir) {
|
async decompressNested(filename, outputDir) {
|
||||||
@@ -113,7 +116,25 @@ class FileDecompressor {
|
|||||||
|
|
||||||
async unZip(filename, outputDir) {
|
async unZip(filename, outputDir) {
|
||||||
const zip = new ZipStreamer();
|
const zip = new ZipStreamer();
|
||||||
return await zip.unpack(filename, outputDir);
|
try {
|
||||||
|
return await zip.unpack(filename, outputDir, {
|
||||||
|
limitFileSize: this.limitFileSize,
|
||||||
|
limitFileCount: 1000
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
fs.emptyDir(outputDir);
|
||||||
|
return await zip.unpack(filename, outputDir, {
|
||||||
|
limitFileSize: this.limitFileSize,
|
||||||
|
limitFileCount: 1000,
|
||||||
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
|
const enc = textUtils.getEncodingLite(nameRaw);
|
||||||
|
if (enc.indexOf('ISO-8859') < 0) {
|
||||||
|
return iconv.decode(nameRaw, enc);
|
||||||
|
}
|
||||||
|
return nameRaw;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unBz2(filename, outputDir) {
|
unBz2(filename, outputDir) {
|
||||||
@@ -125,9 +146,16 @@ class FileDecompressor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unTar(filename, outputDir) {
|
unTar(filename, outputDir) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => { (async() => {
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|
||||||
|
if (this.limitFileSize) {
|
||||||
|
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
||||||
|
reject('Файл слишком большой');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tarExtract = tar.extract(outputDir, {
|
const tarExtract = tar.extract(outputDir, {
|
||||||
map: (header) => {
|
map: (header) => {
|
||||||
files.push({path: header.name, size: header.size});
|
files.push({path: header.name, size: header.size});
|
||||||
@@ -149,7 +177,7 @@ class FileDecompressor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
inputStream.pipe(tarExtract);
|
inputStream.pipe(tarExtract);
|
||||||
});
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
decompressByStream(stream, filename, outputDir) {
|
decompressByStream(stream, filename, outputDir) {
|
||||||
@@ -174,6 +202,16 @@ class FileDecompressor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
stream.on('error', reject);
|
stream.on('error', reject);
|
||||||
|
|
||||||
|
if (this.limitFileSize) {
|
||||||
|
let readSize = 0;
|
||||||
|
stream.on('data', (buffer) => {
|
||||||
|
readSize += buffer.length;
|
||||||
|
if (readSize > this.limitFileSize)
|
||||||
|
stream.destroy(new Error('Файл слишком большой'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
inputStream.on('error', reject);
|
inputStream.on('error', reject);
|
||||||
outputStream.on('error', reject);
|
outputStream.on('error', reject);
|
||||||
|
|
||||||
@@ -203,15 +241,16 @@ class FileDecompressor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async gzipFileIfNotExists(filename, outDir) {
|
async gzipFileIfNotExists(filename, outDir, isMaxCompression) {
|
||||||
const hash = await utils.getFileHash(filename, 'sha256', 'hex');
|
const hash = await utils.getFileHash(filename, 'sha256', 'hex');
|
||||||
|
|
||||||
const outFilename = `${outDir}/${hash}`;
|
const outFilename = `${outDir}/${hash}`;
|
||||||
|
|
||||||
if (!await fs.pathExists(outFilename)) {
|
if (!await fs.pathExists(outFilename)) {
|
||||||
await this.gzipFile(filename, outFilename, 1);
|
await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
|
||||||
|
|
||||||
// переупакуем через некоторое время на максималках
|
// переупакуем через некоторое время на максималках, если упаковали плохо
|
||||||
|
if (!isMaxCompression) {
|
||||||
const filenameCopy = `${filename}.copy`;
|
const filenameCopy = `${filename}.copy`;
|
||||||
await fs.copy(filename, filenameCopy);
|
await fs.copy(filename, filenameCopy);
|
||||||
|
|
||||||
@@ -224,6 +263,7 @@ class FileDecompressor {
|
|||||||
|
|
||||||
await fs.remove(filenameCopy);
|
await fs.remove(filenameCopy);
|
||||||
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
|
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await utils.touchFile(outFilename);
|
await utils.touchFile(outFilename);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const signatures = require('./signatures.json');
|
|||||||
class FileDetector {
|
class FileDetector {
|
||||||
detectFile(filename) {
|
detectFile(filename) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.fromFile(filename, 2000, (err, result) => {
|
this.fromFile(filename, 10000, (err, result) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -653,40 +653,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
"type": "svg",
|
|
||||||
"ext": "svg",
|
|
||||||
"mime": "image/svg+xml",
|
|
||||||
"rules": [
|
|
||||||
{ "type": "contains", "bytes": "3c737667" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "html",
|
|
||||||
"ext": "html",
|
|
||||||
"mime": "text/html",
|
|
||||||
"rules": [
|
|
||||||
{ "type": "or", "rules":
|
|
||||||
[
|
|
||||||
{ "type": "contains", "bytes": "3c68746d6c" },
|
|
||||||
{ "type": "contains", "bytes": "3c00680074006d006c00" },
|
|
||||||
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
|
|
||||||
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
|
|
||||||
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
|
|
||||||
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
|
|
||||||
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
|
|
||||||
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
|
|
||||||
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
|
|
||||||
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
|
|
||||||
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
|
|
||||||
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
|
|
||||||
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "docx",
|
"type": "docx",
|
||||||
"ext": "docx",
|
"ext": "docx",
|
||||||
@@ -708,7 +674,9 @@
|
|||||||
{ "type": "or", "rules":
|
{ "type": "or", "rules":
|
||||||
[
|
[
|
||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" }
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||||
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -721,6 +689,53 @@
|
|||||||
"rules": [
|
"rules": [
|
||||||
{ "type": "equal", "start": 64, "end": 68, "bytes": "4d4f4249" }
|
{ "type": "equal", "start": 64, "end": 68, "bytes": "4d4f4249" }
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "djvu",
|
||||||
|
"ext": "djvu",
|
||||||
|
"mime": "image/vnd.djvu",
|
||||||
|
"rules": [
|
||||||
|
{ "type": "equal", "start": 0, "end": 8, "bytes": "41542654464f524d" },
|
||||||
|
{ "type": "equal", "start": 12, "end": 15, "bytes": "444a56" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "html",
|
||||||
|
"ext": "html",
|
||||||
|
"mime": "text/html",
|
||||||
|
"rules": [
|
||||||
|
{ "type": "or", "rules":
|
||||||
|
[
|
||||||
|
{ "type": "contains", "bytes": "3c68746d6c" },
|
||||||
|
{ "type": "contains", "bytes": "3c00680074006d006c00" },
|
||||||
|
{ "type": "contains", "bytes": "3c48544d4c" },
|
||||||
|
{ "type": "contains", "bytes": "3c00480054004d004c00" },
|
||||||
|
|
||||||
|
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
|
||||||
|
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
|
||||||
|
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
|
||||||
|
{ "type": "equal", "end": 9, "bytes": "3c21444f4354595045" },
|
||||||
|
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
|
||||||
|
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
|
||||||
|
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
|
||||||
|
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
|
||||||
|
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
|
||||||
|
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
|
||||||
|
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
|
||||||
|
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "svg",
|
||||||
|
"ext": "svg",
|
||||||
|
"mime": "image/svg+xml",
|
||||||
|
"rules": [
|
||||||
|
{ "type": "contains", "bytes": "3c737667" }
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
const got = require('got');
|
const got = require('got');
|
||||||
|
|
||||||
const maxDownloadSize = 50*1024*1024;
|
|
||||||
|
|
||||||
class FileDownloader {
|
class FileDownloader {
|
||||||
constructor() {
|
constructor(limitDownloadSize = 0) {
|
||||||
|
this.limitDownloadSize = limitDownloadSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(url, callback) {
|
async load(url, callback, abort) {
|
||||||
let errMes = '';
|
let errMes = '';
|
||||||
const options = {
|
const options = {
|
||||||
encoding: null,
|
encoding: null,
|
||||||
@@ -23,11 +22,15 @@ class FileDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let prevProg = 0;
|
let prevProg = 0;
|
||||||
const request = got(url, options).on('downloadProgress', progress => {
|
const request = got(url, options);
|
||||||
if (progress.transferred > maxDownloadSize) {
|
|
||||||
errMes = 'file too big';
|
request.on('downloadProgress', progress => {
|
||||||
|
if (this.limitDownloadSize) {
|
||||||
|
if (progress.transferred > this.limitDownloadSize) {
|
||||||
|
errMes = 'Файл слишком большой';
|
||||||
request.cancel();
|
request.cancel();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let prog = 0;
|
let prog = 0;
|
||||||
if (estSize)
|
if (estSize)
|
||||||
@@ -38,8 +41,12 @@ class FileDownloader {
|
|||||||
if (prog != prevProg && callback)
|
if (prog != prevProg && callback)
|
||||||
callback(prog);
|
callback(prog);
|
||||||
prevProg = prog;
|
prevProg = prog;
|
||||||
});
|
|
||||||
|
|
||||||
|
if (abort && abort()) {
|
||||||
|
errMes = 'abort';
|
||||||
|
request.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (await request).body;
|
return (await request).body;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const fs = require('fs-extra');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const log = new (require('../AppLogger'))().log;//singleton
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
const ZipStreamer = require('../ZipStreamer');
|
const ZipStreamer = require('../Zip/ZipStreamer');
|
||||||
|
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
|
|
||||||
|
|||||||
119
server/core/LimitedQueue.js
Normal file
119
server/core/LimitedQueue.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
class LimitedQueue {
|
||||||
|
constructor(enqueueAfter = 10, size = 100, timeout = 60*60*1000) {//timeout в ms
|
||||||
|
this.size = size;
|
||||||
|
this.timeout = timeout;
|
||||||
|
|
||||||
|
this.abortCount = 0;
|
||||||
|
this.enqueueAfter = enqueueAfter;
|
||||||
|
this.freed = enqueueAfter;
|
||||||
|
this.listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_addListener(listener) {
|
||||||
|
this.listeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
//отсылаем сообщение первому ожидающему и удаляем его из списка
|
||||||
|
_emitFree() {
|
||||||
|
if (this.listeners.length > 0) {
|
||||||
|
let listener = this.listeners.shift();
|
||||||
|
listener.onFree();
|
||||||
|
|
||||||
|
for (let i = 0; i < this.listeners.length; i++) {
|
||||||
|
this.listeners[i].onPlaceChange(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(onPlaceChange) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.destroyed)
|
||||||
|
reject('destroyed');
|
||||||
|
|
||||||
|
const take = () => {
|
||||||
|
if (this.freed <= 0)
|
||||||
|
throw new Error('Ошибка получения ресурсов в очереди ожидания');
|
||||||
|
|
||||||
|
this.freed--;
|
||||||
|
this.resetTimeout();
|
||||||
|
|
||||||
|
let aCount = this.abortCount;
|
||||||
|
return {
|
||||||
|
ret: () => {
|
||||||
|
if (aCount == this.abortCount) {
|
||||||
|
this.freed++;
|
||||||
|
this._emitFree();
|
||||||
|
aCount = -1;
|
||||||
|
this.resetTimeout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
abort: () => {
|
||||||
|
return (aCount != this.abortCount);
|
||||||
|
},
|
||||||
|
resetTimeout: this.resetTimeout.bind(this)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.freed > 0) {
|
||||||
|
resolve(take());
|
||||||
|
} else {
|
||||||
|
if (this.listeners.length < this.size) {
|
||||||
|
this._addListener({
|
||||||
|
onFree: () => {
|
||||||
|
resolve(take());
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
onPlaceChange: (i) => {
|
||||||
|
if (onPlaceChange)
|
||||||
|
onPlaceChange(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (onPlaceChange)
|
||||||
|
onPlaceChange(this.listeners.length);
|
||||||
|
} else {
|
||||||
|
reject('Превышен размер очереди ожидания');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTimeout() {
|
||||||
|
if (this.timer)
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = setTimeout(() => { this.clean(); }, this.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
clean() {
|
||||||
|
this.timer = null;
|
||||||
|
|
||||||
|
if (this.freed < this.enqueueAfter) {
|
||||||
|
this.abortCount++;
|
||||||
|
//чистка listeners
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener.onError('Время ожидания в очереди истекло');
|
||||||
|
}
|
||||||
|
this.listeners = [];
|
||||||
|
|
||||||
|
this.freed = this.enqueueAfter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener.onError('destroy');
|
||||||
|
}
|
||||||
|
this.listeners = [];
|
||||||
|
this.abortCount++;
|
||||||
|
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = LimitedQueue;
|
||||||
@@ -226,12 +226,12 @@ class Logger {
|
|||||||
|
|
||||||
// catch ctrl+c event and exit normally
|
// catch ctrl+c event and exit normally
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
this.log(LM_WARN, 'Ctrl-C pressed, exiting...');
|
this.log(LM_FATAL, 'Ctrl-C pressed, exiting...');
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
this.log(LM_WARN, 'Kill signal, exiting...');
|
this.log(LM_FATAL, 'Kill signal, exiting...');
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1
server/core/Reader/BookConverter/.gitignore
vendored
Normal file
1
server/core/Reader/BookConverter/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
const chardet = require('chardet');
|
|
||||||
const he = require('he');
|
const he = require('he');
|
||||||
|
|
||||||
|
const LimitedQueue = require('../../LimitedQueue');
|
||||||
const textUtils = require('./textUtils');
|
const textUtils = require('./textUtils');
|
||||||
const utils = require('../../utils');
|
const utils = require('../../utils');
|
||||||
|
|
||||||
let execConverterCounter = 0;
|
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
|
||||||
|
|
||||||
class ConvertBase {
|
class ConvertBase {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@@ -32,13 +32,26 @@ class ConvertBase {
|
|||||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||||
}
|
}
|
||||||
|
|
||||||
async execConverter(path, args, onData) {
|
async execConverter(path, args, onData, abort) {
|
||||||
execConverterCounter++;
|
onData = (onData ? onData : () => {});
|
||||||
try {
|
|
||||||
if (execConverterCounter > 10)
|
|
||||||
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
|
||||||
|
|
||||||
const result = await utils.spawnProcess(path, {args, onData});
|
let q = null;
|
||||||
|
try {
|
||||||
|
q = await queue.get(() => {onData();});
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await utils.spawnProcess(path, {
|
||||||
|
killAfter: 600,
|
||||||
|
args,
|
||||||
|
onData: (data) => {
|
||||||
|
q.resetTimeout();
|
||||||
|
onData(data);
|
||||||
|
},
|
||||||
|
abort
|
||||||
|
});
|
||||||
if (result.code != 0) {
|
if (result.code != 0) {
|
||||||
let error = result.code;
|
let error = result.code;
|
||||||
if (this.config.branch == 'development')
|
if (this.config.branch == 'development')
|
||||||
@@ -48,29 +61,21 @@ class ConvertBase {
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e.status == 'killed') {
|
if (e.status == 'killed') {
|
||||||
throw new Error('Слишком долгое ожидание конвертера');
|
throw new Error('Слишком долгое ожидание конвертера');
|
||||||
|
} else if (e.status == 'abort') {
|
||||||
|
throw new Error('abort');
|
||||||
} else if (e.status == 'error') {
|
} else if (e.status == 'error') {
|
||||||
throw new Error(e.error);
|
throw new Error(e.error);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
execConverterCounter--;
|
q.ret();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decode(data) {
|
decode(data) {
|
||||||
let selected = textUtils.getEncoding(data);
|
let selected = textUtils.getEncoding(data);
|
||||||
|
|
||||||
if (selected == 'ISO-8859-5') {
|
|
||||||
const charsetAll = chardet.detectAll(data.slice(0, 20000));
|
|
||||||
for (const charset of charsetAll) {
|
|
||||||
if (charset.name.indexOf('ISO-8859') < 0) {
|
|
||||||
selected = charset.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected.toLowerCase() != 'utf-8')
|
if (selected.toLowerCase() != 'utf-8')
|
||||||
return iconv.decode(data, selected);
|
return iconv.decode(data, selected);
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ConvertDoc extends ConvertDocX {
|
|||||||
return false;
|
return false;
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback} = opts;
|
const {inputFiles, callback, abort} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
||||||
const docFile = `${outFile}.doc`;
|
const docFile = `${outFile}.doc`;
|
||||||
@@ -24,9 +24,9 @@ class ConvertDoc extends ConvertDocX {
|
|||||||
const fb2File = `${outFile}.fb2`;
|
const fb2File = `${outFile}.fb2`;
|
||||||
|
|
||||||
await fs.copy(inputFiles.sourceFile, docFile);
|
await fs.copy(inputFiles.sourceFile, docFile);
|
||||||
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile]);
|
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile], null, abort);
|
||||||
|
|
||||||
return await super.convert(docxFile, fb2File, callback);
|
return await super.convert(docxFile, fb2File, callback, abort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ class ConvertDocX extends ConvertBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async convert(docxFile, fb2File, callback) {
|
async convert(docxFile, fb2File, callback, abort) {
|
||||||
let perc = 0;
|
let perc = 0;
|
||||||
await this.execConverter(this.calibrePath, [docxFile, fb2File], () => {
|
await this.execConverter(this.calibrePath, [docxFile, fb2File, '-vv'], () => {
|
||||||
perc = (perc < 100 ? perc + 5 : 50);
|
perc = (perc < 100 ? perc + 1 : 50);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
});
|
}, abort);
|
||||||
|
|
||||||
return await fs.readFile(fb2File);
|
return await fs.readFile(fb2File);
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ class ConvertDocX extends ConvertBase {
|
|||||||
return false;
|
return false;
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback} = opts;
|
const {inputFiles, callback, abort} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
||||||
const docxFile = `${outFile}.docx`;
|
const docxFile = `${outFile}.docx`;
|
||||||
@@ -43,7 +43,7 @@ class ConvertDocX extends ConvertBase {
|
|||||||
|
|
||||||
await fs.copy(inputFiles.sourceFile, docxFile);
|
await fs.copy(inputFiles.sourceFile, docxFile);
|
||||||
|
|
||||||
return await this.convert(docxFile, fb2File, callback);
|
return await this.convert(docxFile, fb2File, callback, abort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ConvertEpub extends ConvertBase {
|
|||||||
return false;
|
return false;
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback} = opts;
|
const {inputFiles, callback, abort} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
||||||
const epubFile = `${outFile}.epub`;
|
const epubFile = `${outFile}.epub`;
|
||||||
@@ -37,10 +37,10 @@ class ConvertEpub extends ConvertBase {
|
|||||||
await fs.copy(inputFiles.sourceFile, epubFile);
|
await fs.copy(inputFiles.sourceFile, epubFile);
|
||||||
|
|
||||||
let perc = 0;
|
let perc = 0;
|
||||||
await this.execConverter(this.calibrePath, [epubFile, fb2File], () => {
|
await this.execConverter(this.calibrePath, [epubFile, fb2File, '-vv'], () => {
|
||||||
perc = (perc < 100 ? perc + 5 : 50);
|
perc = (perc < 100 ? perc + 1 : 50);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
});
|
}, abort);
|
||||||
|
|
||||||
return await fs.readFile(fb2File);
|
return await fs.readFile(fb2File);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
check(data, opts) {
|
check(data, opts) {
|
||||||
const {dataType} = opts;
|
const {dataType} = opts;
|
||||||
|
|
||||||
|
//html?
|
||||||
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
||||||
return {isText: false};
|
return {isText: false};
|
||||||
|
|
||||||
@@ -14,6 +15,11 @@ class ConvertHtml extends ConvertBase {
|
|||||||
return {isText: true};
|
return {isText: true};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//из буфера обмена?
|
||||||
|
if (data.toString().indexOf('<buffer>') == 0) {
|
||||||
|
return {isText: false};
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ConvertMobi extends ConvertBase {
|
|||||||
return false;
|
return false;
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback} = opts;
|
const {inputFiles, callback, abort} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
||||||
const mobiFile = `${outFile}.mobi`;
|
const mobiFile = `${outFile}.mobi`;
|
||||||
@@ -25,10 +25,10 @@ class ConvertMobi extends ConvertBase {
|
|||||||
await fs.copy(inputFiles.sourceFile, mobiFile);
|
await fs.copy(inputFiles.sourceFile, mobiFile);
|
||||||
|
|
||||||
let perc = 0;
|
let perc = 0;
|
||||||
await this.execConverter(this.calibrePath, [mobiFile, fb2File], () => {
|
await this.execConverter(this.calibrePath, [mobiFile, fb2File, '-vv'], () => {
|
||||||
perc = (perc < 100 ? perc + 5 : 50);
|
perc = (perc < 100 ? perc + 1 : 50);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
});
|
}, abort);
|
||||||
|
|
||||||
return await fs.readFile(fb2File);
|
return await fs.readFile(fb2File);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
return false;
|
return false;
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback} = opts;
|
const {inputFiles, callback, abort} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
|
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
|
||||||
perc = (perc < 80 ? perc + 10 : 40);
|
perc = (perc < 80 ? perc + 10 : 40);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
});
|
}, abort);
|
||||||
callback(80);
|
callback(80);
|
||||||
|
|
||||||
const data = await fs.readFile(outFile);
|
const data = await fs.readFile(outFile);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ConvertRtf extends ConvertDocX {
|
|||||||
return false;
|
return false;
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback} = opts;
|
const {inputFiles, callback, abort} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
|
||||||
const rtfFile = `${outFile}.rtf`;
|
const rtfFile = `${outFile}.rtf`;
|
||||||
@@ -24,9 +24,9 @@ class ConvertRtf extends ConvertDocX {
|
|||||||
const fb2File = `${outFile}.fb2`;
|
const fb2File = `${outFile}.fb2`;
|
||||||
|
|
||||||
await fs.copy(inputFiles.sourceFile, rtfFile);
|
await fs.copy(inputFiles.sourceFile, rtfFile);
|
||||||
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, rtfFile]);
|
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, rtfFile], null, abort);
|
||||||
|
|
||||||
return await super.convert(docxFile, fb2File, callback);
|
return await super.convert(docxFile, fb2File, callback, abort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ class BookConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertToFb2(inputFiles, outputFile, opts, callback) {
|
async convertToFb2(inputFiles, outputFile, opts, callback, abort) {
|
||||||
|
if (abort && abort())
|
||||||
|
throw new Error('abort');
|
||||||
|
|
||||||
const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile);
|
const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile);
|
||||||
const data = await fs.readFile(inputFiles.selectedFile);
|
const data = await fs.readFile(inputFiles.selectedFile);
|
||||||
|
|
||||||
const convertOpts = Object.assign({}, opts, {inputFiles, callback, dataType: selectedFileType});
|
const convertOpts = Object.assign({}, opts, {inputFiles, callback, abort, dataType: selectedFileType});
|
||||||
let result = false;
|
let result = false;
|
||||||
for (const convert of this.convertFactory) {
|
for (const convert of this.convertFactory) {
|
||||||
result = await convert.run(data, convertOpts);
|
result = await convert.run(data, convertOpts);
|
||||||
@@ -41,7 +44,7 @@ class BookConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!result && inputFiles.nesting) {
|
if (!result && inputFiles.nesting) {
|
||||||
result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback);
|
result = await this.convertToFb2(inputFiles.nesting, outputFile, opts, callback, abort);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
function getEncoding(buf, returnAll) {
|
const chardet = require('chardet');
|
||||||
|
|
||||||
|
function getEncoding(buf) {
|
||||||
|
let selected = getEncodingLite(buf);
|
||||||
|
|
||||||
|
if (selected == 'ISO-8859-5') {
|
||||||
|
const charsetAll = chardet.detectAll(buf.slice(0, 20000));
|
||||||
|
for (const charset of charsetAll) {
|
||||||
|
if (charset.name.indexOf('ISO-8859') < 0) {
|
||||||
|
selected = charset.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getEncodingLite(buf, returnAll) {
|
||||||
const lowerCase = 3;
|
const lowerCase = 3;
|
||||||
const upperCase = 1;
|
const upperCase = 1;
|
||||||
|
|
||||||
@@ -81,7 +100,7 @@ function getEncoding(buf, returnAll) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkIfText(buf) {
|
function checkIfText(buf) {
|
||||||
const enc = getEncoding(buf, true);
|
const enc = getEncodingLite(buf, true);
|
||||||
if (enc[0].c > enc[0].totalChecked*0.9)
|
if (enc[0].c > enc[0].totalChecked*0.9)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -106,5 +125,6 @@ function checkIfText(buf) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getEncoding,
|
getEncoding,
|
||||||
|
getEncodingLite,
|
||||||
checkIfText,
|
checkIfText,
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
const LimitedQueue = require('../LimitedQueue');
|
||||||
const WorkerState = require('../WorkerState');//singleton
|
const WorkerState = require('../WorkerState');//singleton
|
||||||
const FileDownloader = require('../FileDownloader');
|
const FileDownloader = require('../FileDownloader');
|
||||||
const FileDecompressor = require('../FileDecompressor');
|
const FileDecompressor = require('../FileDecompressor');
|
||||||
@@ -11,6 +12,7 @@ const utils = require('../utils');
|
|||||||
const log = new (require('../AppLogger'))().log;//singleton
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
||||||
|
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
@@ -27,8 +29,8 @@ class ReaderWorker {
|
|||||||
fs.ensureDirSync(this.config.tempPublicDir);
|
fs.ensureDirSync(this.config.tempPublicDir);
|
||||||
|
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
this.down = new FileDownloader();
|
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||||
this.decomp = new FileDecompressor();
|
this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
|
||||||
this.bookConverter = new BookConverter(this.config);
|
this.bookConverter = new BookConverter(this.config);
|
||||||
|
|
||||||
this.remoteWebDavStorage = false;
|
this.remoteWebDavStorage = false;
|
||||||
@@ -52,30 +54,61 @@ class ReaderWorker {
|
|||||||
let decompDir = '';
|
let decompDir = '';
|
||||||
let downloadedFilename = '';
|
let downloadedFilename = '';
|
||||||
let isUploaded = false;
|
let isUploaded = false;
|
||||||
|
let isRestored = false;
|
||||||
let convertFilename = '';
|
let convertFilename = '';
|
||||||
|
|
||||||
|
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
|
||||||
|
const overLoadErr = new Error(overLoadMes);
|
||||||
|
|
||||||
|
let q = null;
|
||||||
try {
|
try {
|
||||||
|
wState.set({state: 'queue', step: 1, totalSteps: 1});
|
||||||
|
try {
|
||||||
|
let qSize = 0;
|
||||||
|
q = await queue.get((place) => {
|
||||||
|
wState.set({place, progress: (qSize ? Math.round((qSize - place)/qSize*100) : 0)});
|
||||||
|
if (!qSize)
|
||||||
|
qSize = place;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw overLoadErr;
|
||||||
|
}
|
||||||
|
|
||||||
wState.set({state: 'download', step: 1, totalSteps: 3, url});
|
wState.set({state: 'download', step: 1, totalSteps: 3, url});
|
||||||
|
|
||||||
const tempFilename = utils.randomHexString(30);
|
const tempFilename = utils.randomHexString(30);
|
||||||
const tempFilename2 = utils.randomHexString(30);
|
const tempFilename2 = utils.randomHexString(30);
|
||||||
const decompDirname = utils.randomHexString(30);
|
const decompDirname = utils.randomHexString(30);
|
||||||
|
|
||||||
|
//download or use uploaded
|
||||||
if (url.indexOf('file://') != 0) {//download
|
if (url.indexOf('file://') != 0) {//download
|
||||||
const downdata = await this.down.load(url, (progress) => {
|
const downdata = await this.down.load(url, (progress) => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
});
|
}, q.abort);
|
||||||
|
|
||||||
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
|
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
|
||||||
await fs.writeFile(downloadedFilename, downdata);
|
await fs.writeFile(downloadedFilename, downdata);
|
||||||
} else {//uploaded file
|
} else {//uploaded file
|
||||||
downloadedFilename = `${this.config.uploadDir}/${url.substr(7)}`;
|
const fileHash = url.substr(7);
|
||||||
if (!await fs.pathExists(downloadedFilename))
|
downloadedFilename = `${this.config.uploadDir}/${fileHash}`;
|
||||||
|
if (!await fs.pathExists(downloadedFilename)) {
|
||||||
|
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
||||||
|
try {
|
||||||
|
downloadedFilename = await this.restoreRemoteFile(fileHash);
|
||||||
|
isRestored = true;
|
||||||
|
} catch(e) {
|
||||||
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
||||||
|
}
|
||||||
|
}
|
||||||
await utils.touchFile(downloadedFilename);
|
await utils.touchFile(downloadedFilename);
|
||||||
isUploaded = true;
|
isUploaded = true;
|
||||||
}
|
}
|
||||||
wState.set({progress: 100});
|
wState.set({progress: 100});
|
||||||
|
|
||||||
|
if (q.abort())
|
||||||
|
throw overLoadErr;
|
||||||
|
q.resetTimeout();
|
||||||
|
|
||||||
//decompress
|
//decompress
|
||||||
wState.set({state: 'decompress', step: 2, progress: 0});
|
wState.set({state: 'decompress', step: 2, progress: 0});
|
||||||
decompDir = `${this.config.tempDownloadDir}/${decompDirname}`;
|
decompDir = `${this.config.tempDownloadDir}/${decompDirname}`;
|
||||||
@@ -88,12 +121,16 @@ class ReaderWorker {
|
|||||||
}
|
}
|
||||||
wState.set({progress: 100});
|
wState.set({progress: 100});
|
||||||
|
|
||||||
|
if (q.abort())
|
||||||
|
throw overLoadErr;
|
||||||
|
q.resetTimeout();
|
||||||
|
|
||||||
//конвертирование в fb2
|
//конвертирование в fb2
|
||||||
wState.set({state: 'convert', step: 3, progress: 0});
|
wState.set({state: 'convert', step: 3, progress: 0});
|
||||||
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
||||||
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
});
|
}, q.abort);
|
||||||
|
|
||||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||||
const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, this.config.tempPublicDir);
|
const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, this.config.tempPublicDir);
|
||||||
@@ -118,11 +155,29 @@ class ReaderWorker {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
|
||||||
|
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(30*1000);
|
||||||
|
try {
|
||||||
|
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||||
|
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
|
||||||
|
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
if (e.message == 'abort')
|
||||||
|
e.message = overLoadMes;
|
||||||
wState.set({state: 'error', error: e.message});
|
wState.set({state: 'error', error: e.message});
|
||||||
} finally {
|
} finally {
|
||||||
//clean
|
//clean
|
||||||
|
if (q)
|
||||||
|
q.ret();
|
||||||
if (decompDir)
|
if (decompDir)
|
||||||
await fs.remove(decompDir);
|
await fs.remove(decompDir);
|
||||||
if (downloadedFilename && !isUploaded)
|
if (downloadedFilename && !isUploaded)
|
||||||
@@ -156,15 +211,7 @@ class ReaderWorker {
|
|||||||
return `file://${hash}`;
|
return `file://${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreCachedFile(filename) {
|
async restoreRemoteFile(filename) {
|
||||||
const workerId = this.workerState.generateWorkerId();
|
|
||||||
const wState = this.workerState.getControl(workerId);
|
|
||||||
wState.set({state: 'start'});
|
|
||||||
|
|
||||||
(async() => {
|
|
||||||
try {
|
|
||||||
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
|
|
||||||
|
|
||||||
const basename = path.basename(filename);
|
const basename = path.basename(filename);
|
||||||
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
||||||
|
|
||||||
@@ -179,7 +226,22 @@ class ReaderWorker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return targetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreCachedFile(filename) {
|
||||||
|
const workerId = this.workerState.generateWorkerId();
|
||||||
|
const wState = this.workerState.getControl(workerId);
|
||||||
|
wState.set({state: 'start'});
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
try {
|
||||||
|
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
|
||||||
|
|
||||||
|
const targetName = await this.restoreRemoteFile(filename);
|
||||||
const stat = await fs.stat(targetName);
|
const stat = await fs.stat(targetName);
|
||||||
|
|
||||||
|
const basename = path.basename(filename);
|
||||||
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
|
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.indexOf('404') < 0)
|
if (e.message.indexOf('404') < 0)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const zipStream = require('zip-stream');
|
const zipStream = require('zip-stream');
|
||||||
const unzipStream = require('node-stream-zip');
|
const unzipStream = require('./node_stream_zip');
|
||||||
|
|
||||||
class ZipStreamer {
|
class ZipStreamer {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -52,9 +52,15 @@ class ZipStreamer {
|
|||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
unpack(zipFile, outputDir, entryCallback) {
|
unpack(zipFile, outputDir, options, entryCallback) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
entryCallback = (entryCallback ? entryCallback : () => {});
|
entryCallback = (entryCallback ? entryCallback : () => {});
|
||||||
|
const {
|
||||||
|
limitFileSize = 0,
|
||||||
|
limitFileCount = 0,
|
||||||
|
decodeEntryNameCallback = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const unzip = new unzipStream({file: zipFile});
|
const unzip = new unzipStream({file: zipFile});
|
||||||
|
|
||||||
unzip.on('error', reject);
|
unzip.on('error', reject);
|
||||||
@@ -67,14 +73,41 @@ class ZipStreamer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
unzip.on('ready', () => {
|
unzip.on('ready', () => {
|
||||||
unzip.extract(null, outputDir, (err) => {
|
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
||||||
if (err) reject(err);
|
const entries = Object.values(unzip.entries());
|
||||||
unzip.close();
|
if (limitFileCount && entries.length > limitFileCount) {
|
||||||
resolve(files);
|
reject('Слишком много файлов');
|
||||||
});
|
return;
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
||||||
|
reject('Файл слишком большой');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodeEntryNameCallback) {
|
||||||
|
entry.name = (decodeEntryNameCallback(entry.nameRaw)).toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unzip.extract(null, outputDir, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unzip.close();
|
||||||
|
resolve(files);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = ZipStreamer;
|
module.exports = ZipStreamer;
|
||||||
1055
server/core/Zip/node_stream_zip.js
Normal file
1055
server/core/Zip/node_stream_zip.js
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user