Compare commits
263 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
113ab3e596 | ||
|
|
c95870bfe5 | ||
|
|
e69e9335f9 | ||
|
|
fd21cd77dd | ||
|
|
d1880acaf9 | ||
|
|
428b507257 | ||
|
|
043dab0731 | ||
|
|
a7b4d9c0d8 | ||
|
|
6f9c95e351 | ||
|
|
7a53063ea8 | ||
|
|
ec4d5cac4f | ||
|
|
f8557cba88 | ||
|
|
5dead039f5 | ||
|
|
ea38392df4 | ||
|
|
0cc9d90a94 | ||
|
|
8c7b86c458 | ||
|
|
0e29546fc5 | ||
|
|
c9fa90d07c | ||
|
|
7d8e0525b1 | ||
|
|
ddf69876a6 | ||
|
|
1d78e75e38 | ||
|
|
7ed58fe3c6 | ||
|
|
058c79570b | ||
|
|
ec8fbcdf38 | ||
|
|
76673295bf | ||
|
|
084401b9c3 | ||
|
|
49038b10f7 | ||
|
|
45ea26810a | ||
|
|
18c8b2d803 | ||
|
|
f4a7482b3b | ||
|
|
32dff128f4 | ||
|
|
a00b2d6574 | ||
|
|
10c6e7d522 | ||
|
|
df6a256d51 | ||
|
|
fbdb74ee68 | ||
|
|
9ad7250da0 | ||
|
|
8c86984ea1 | ||
|
|
834b3f6210 | ||
|
|
105b8d5042 | ||
|
|
7ca8fd9ca1 | ||
|
|
0067c2800a | ||
|
|
688c8796f4 | ||
|
|
56af65742b | ||
|
|
629ad26d40 | ||
|
|
4b0e499c10 | ||
|
|
4697b46cba | ||
|
|
7f17e7daed | ||
|
|
a1fcb7597b | ||
|
|
35e46d0685 | ||
|
|
e2c0f3658b | ||
|
|
a3541ec16a | ||
|
|
08d0d3e7f3 | ||
|
|
2c47b2bee3 | ||
|
|
e6008b5ec4 | ||
|
|
e214ddf8d5 | ||
|
|
52927c6188 | ||
|
|
92ca9dd983 | ||
|
|
ed8be34c12 | ||
|
|
93bddfd05e | ||
|
|
8c99101bb3 | ||
|
|
d874f9ded4 | ||
|
|
d7be4d3d94 | ||
|
|
a2fa312839 | ||
|
|
f7e1e09928 | ||
|
|
f0832b07cb | ||
|
|
7c253df291 | ||
|
|
bb7cd9cbde | ||
|
|
56c4182985 | ||
|
|
cb6c7536bf | ||
|
|
fbfe8cbda0 | ||
|
|
6129d2d7eb | ||
|
|
16b30c922a | ||
|
|
c42ad66be6 | ||
|
|
f36c13fea1 | ||
|
|
4fd9d579e0 | ||
|
|
e65a8a13ea | ||
|
|
6ddb97d43e | ||
|
|
89082603de | ||
|
|
a9a3227433 | ||
|
|
60cb3514b2 | ||
|
|
4aeaa05f0b | ||
|
|
9c06552278 | ||
|
|
000f8dde82 | ||
|
|
9ffc218002 | ||
|
|
68a188f099 | ||
|
|
8829bb3810 | ||
|
|
5164d2f536 | ||
|
|
451538fcf7 | ||
|
|
82a02ef339 | ||
|
|
b834d4951f | ||
|
|
edc3b669be | ||
|
|
522826311d | ||
|
|
e69b9951d5 | ||
|
|
c6300222ea | ||
|
|
5aa6ee899c | ||
|
|
4b76f97d2b | ||
|
|
5ccfe71c55 | ||
|
|
97fc902cdb | ||
|
|
7e935951d7 | ||
|
|
810c6d68d2 | ||
|
|
003dc70f4f | ||
|
|
371ff64a95 | ||
|
|
b0de5adbf3 | ||
|
|
d1d2b07c33 | ||
|
|
d9b2444c1a | ||
|
|
e7fae27031 | ||
|
|
eb0c7b0a32 | ||
|
|
3d7ad0dd9a | ||
|
|
ae04feb311 | ||
|
|
7b59f911ef | ||
|
|
d3444da647 | ||
|
|
66738d0c9c | ||
|
|
7e187acd68 | ||
|
|
c751372a54 | ||
|
|
7fc98fc7da | ||
|
|
b56f45694e | ||
|
|
091ca521ef | ||
|
|
c7a17b0a76 | ||
|
|
26468b996a | ||
|
|
c4e240d87c | ||
|
|
04713f47c8 | ||
|
|
37ab3493db | ||
|
|
a4cb3c628e | ||
|
|
8492da8a13 | ||
|
|
98d7c64a56 | ||
|
|
25f121e5ed | ||
|
|
4c8797c99c | ||
|
|
1155aa285d | ||
|
|
239bbb8263 | ||
|
|
e6b9330108 | ||
|
|
935b767c2e | ||
|
|
8acf3295b5 | ||
|
|
48c3a12fa0 | ||
|
|
a1dea514b7 | ||
|
|
d4788439cb | ||
|
|
0a60ad354c | ||
|
|
c565a20344 | ||
|
|
735ee88f0b | ||
|
|
9405ce2cc0 | ||
|
|
115277d88a | ||
|
|
6925c11dbd | ||
|
|
984d835892 | ||
|
|
23353a4960 | ||
|
|
955bcda032 | ||
|
|
81ad5d7a2c | ||
|
|
dada7980ec | ||
|
|
511a308646 | ||
|
|
65c8f2cc81 | ||
|
|
238c18bc48 | ||
|
|
873a08fee1 | ||
|
|
7e89228803 | ||
|
|
fc630923a4 | ||
|
|
928f911d03 | ||
|
|
7ffcd3fe1b | ||
|
|
0efbaf643a | ||
|
|
f1bf8e54ae | ||
|
|
b4aa6ab6c8 | ||
|
|
72431f0202 | ||
|
|
04a326c0e4 | ||
|
|
931966f4f3 | ||
|
|
8808cc4779 | ||
|
|
988c959eba | ||
|
|
c0b658d9e6 | ||
|
|
3190246f34 | ||
|
|
d957b4a5f9 | ||
|
|
bef9e5705c | ||
|
|
eb2affa518 | ||
|
|
07b9a3c033 | ||
|
|
3ca14ae06a | ||
|
|
7caa0c2112 | ||
|
|
9c69f5bc01 | ||
|
|
125a2e0f17 | ||
|
|
1b4360b897 | ||
|
|
4775d6e47b | ||
|
|
33fc553c55 | ||
|
|
25cad81c50 | ||
|
|
02a2099c1f | ||
|
|
1cda186b1a | ||
|
|
f10291b6c6 | ||
|
|
26ab5d6765 | ||
|
|
5edeed0747 | ||
|
|
c878ce432f | ||
|
|
81798897c8 | ||
|
|
63840fadbc | ||
|
|
36aa057035 | ||
|
|
30afd2421c | ||
|
|
53a1d90bd8 | ||
|
|
2ecf6beef2 | ||
|
|
85910a20e9 | ||
|
|
66cf7790b3 | ||
|
|
4a9eb7e4bb | ||
|
|
07446696c1 | ||
|
|
a29f9d9a4b | ||
|
|
d49c9baec3 | ||
|
|
8c9d4a12ee | ||
|
|
fce69e4657 | ||
|
|
b387509f88 | ||
|
|
8dc8bdc0d6 | ||
|
|
00caae8363 | ||
|
|
2ead8570a7 | ||
|
|
408315466b | ||
|
|
c651836554 | ||
|
|
03a1e70fce | ||
|
|
ab5a11a24f | ||
|
|
8cd6ed472c | ||
|
|
055181b744 | ||
|
|
e331a3920b | ||
|
|
c62bccb470 | ||
|
|
ea351ea293 | ||
|
|
d806a07c60 | ||
|
|
c0ea096f1f | ||
|
|
011d4a1672 | ||
|
|
4836a737c6 | ||
|
|
5712b2ee17 | ||
|
|
32dd17694e | ||
|
|
3ebc932a6a | ||
|
|
8f351d9bef | ||
|
|
5ae3ea94e4 | ||
|
|
f203d453a4 | ||
|
|
0d5cba121b | ||
|
|
0cd6a48a46 | ||
|
|
4e07ce2b5c | ||
|
|
85a525e301 | ||
|
|
03e4a6d723 | ||
|
|
ab28af1abe | ||
|
|
7fceed5301 | ||
|
|
0077816afa | ||
|
|
cb01423147 | ||
|
|
61b0712d36 | ||
|
|
12d7843377 | ||
|
|
9293c0a0d4 | ||
|
|
bb9522197a | ||
|
|
450a2e0664 | ||
|
|
41e35f3ec8 | ||
|
|
a9bc98abe3 | ||
|
|
47bca03532 | ||
|
|
942021371c | ||
|
|
ea2f178730 | ||
|
|
4b5c8d9efe | ||
|
|
28ebf13c3a | ||
|
|
5d52e63dd9 | ||
|
|
1a0e024050 | ||
|
|
e627a0d970 | ||
|
|
48668d94ad | ||
|
|
e08c431dd9 | ||
|
|
5ee58ad6f0 | ||
|
|
ac0a4f0586 | ||
|
|
b6f4c153e5 | ||
|
|
4fdaf5f555 | ||
|
|
b4ee9d6c00 | ||
|
|
7c73c74730 | ||
|
|
c20aa089fa | ||
|
|
b0e15c22ea | ||
|
|
d58a2c065a | ||
|
|
53135e7ee8 | ||
|
|
5c48ca9e6c | ||
|
|
c4a280f3d8 | ||
|
|
ba2943c722 | ||
|
|
26f6ffc83a | ||
|
|
bcf075a72c | ||
|
|
02d458d192 | ||
|
|
a349d8af68 | ||
|
|
0dbaf32aac |
@@ -31,6 +31,7 @@
|
|||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
"vue/no-v-html": "off",
|
"vue/no-v-html": "off",
|
||||||
|
"vue/no-v-model-argument": "off",
|
||||||
|
|
||||||
"strict": 0,
|
"strict": 0,
|
||||||
"indent": [0, 4, {
|
"indent": [0, 4, {
|
||||||
|
|||||||
86
README.md
86
README.md
@@ -1,43 +1,43 @@
|
|||||||
# Liberama
|
# Liberama
|
||||||
|
|
||||||
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
||||||
|
|
||||||
Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## VPS
|
## VPS
|
||||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||||
|
|
||||||
## Сборка проекта
|
## Сборка проекта
|
||||||
Необходима версия node.js не ниже 14.
|
Необходима версия node.js не ниже 14.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/bookpauk/liberama
|
$ git clone https://github.com/bookpauk/liberama
|
||||||
$ cd liberama
|
$ cd liberama
|
||||||
$ npm i
|
$ npm i
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
```
|
```
|
||||||
$ npm run build:win
|
$ npm run build:win
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
```
|
```
|
||||||
$ npm run build:linux
|
$ npm run build:linux
|
||||||
```
|
```
|
||||||
|
|
||||||
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
|
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
|
||||||
|
|
||||||
### Разработка
|
### Разработка
|
||||||
```
|
```
|
||||||
$ npm run dev
|
$ npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Помочь проекту
|
## Помочь проекту
|
||||||
|
|
||||||
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85
|
* bitcoin: bc1q3tyumaj648pp2e69jalsez2lnt462ttc33nup9
|
||||||
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
|
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
|
||||||
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
|
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
@@ -23,30 +23,14 @@ async function main() {
|
|||||||
|
|
||||||
await fs.ensureDir(tempDownloadDir);
|
await fs.ensureDir(tempDownloadDir);
|
||||||
|
|
||||||
//sqlite3
|
|
||||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
|
|
||||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
|
|
||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
|
||||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
|
||||||
|
|
||||||
//распаковываем
|
|
||||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
|
||||||
console.log('files decompressed');
|
|
||||||
}
|
|
||||||
// копируем в дистрибутив
|
|
||||||
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
|
|
||||||
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
|
|
||||||
|
|
||||||
//ipfs
|
//ipfs
|
||||||
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
|
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
|
||||||
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
||||||
|
|
||||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ const { VueLoaderPlugin } = require('vue-loader');
|
|||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/*resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
vue: '@vue/compat'
|
ws: false,
|
||||||
|
//vue: '@vue/compat'
|
||||||
}
|
}
|
||||||
},*/
|
},
|
||||||
entry: [`${clientDir}/main.js`],
|
entry: [`${clientDir}/main.js`],
|
||||||
output: {
|
output: {
|
||||||
publicPath: '/app/',
|
publicPath: '/app/',
|
||||||
|
|||||||
22
build/win.js
22
build/win.js
@@ -4,7 +4,7 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
@@ -23,30 +23,14 @@ async function main() {
|
|||||||
|
|
||||||
await fs.ensureDir(tempDownloadDir);
|
await fs.ensureDir(tempDownloadDir);
|
||||||
|
|
||||||
//sqlite3
|
|
||||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
|
|
||||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
|
|
||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
|
||||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
|
||||||
|
|
||||||
//распаковываем
|
|
||||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
|
||||||
console.log('files decompressed');
|
|
||||||
}
|
|
||||||
// копируем в дистрибутив
|
|
||||||
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
|
|
||||||
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
|
|
||||||
|
|
||||||
//ipfs
|
//ipfs
|
||||||
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
|
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
|
||||||
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
if (!await fs.pathExists(ipfsDecompressedFilename)) {
|
||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
||||||
|
|
||||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Misc {
|
|||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
|
|
||||||
const query = {params: [
|
const query = {params: [
|
||||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch',
|
||||||
]};
|
]};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as utils from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
|
import * as cryptoUtils from '../share/cryptoUtils';
|
||||||
import wsc from './webSocketConnection';
|
import wsc from './webSocketConnection';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
@@ -119,32 +120,7 @@ class Reader {
|
|||||||
estSize = response.headers['content-length'];
|
estSize = response.headers['content-length'];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//восстановим при необходимости файл на сервере из удаленного облака
|
//
|
||||||
let response = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
//если с WebSocket проблема, работаем по http
|
|
||||||
response = await api.post('/restore-cached-file', {path: url});
|
|
||||||
response = response.data;
|
|
||||||
}
|
|
||||||
if (response.state == 'error') {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workerId = response.workerId;
|
|
||||||
if (!workerId)
|
|
||||||
throw new Error('Неверный ответ api');
|
|
||||||
|
|
||||||
response = await this.getWorkerStateFinish(workerId);
|
|
||||||
if (response.state == 'error') {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
if (response.size && estSize < 0) {
|
|
||||||
estSize = response.size;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return estSize;
|
return estSize;
|
||||||
@@ -174,11 +150,10 @@ class Reader {
|
|||||||
return await axios.get(url, options);
|
return await axios.get(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(file, maxUploadFileSize, callback) {
|
async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
|
||||||
if (!maxUploadFileSize)
|
|
||||||
maxUploadFileSize = 10*1024*1024;
|
|
||||||
if (file.size > maxUploadFileSize)
|
if (file.size > maxUploadFileSize)
|
||||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('file', file, file.name);
|
formData.append('file', file, file.name);
|
||||||
|
|
||||||
@@ -219,12 +194,52 @@ class Reader {
|
|||||||
const state = response.state;
|
const state = response.state;
|
||||||
if (!state)
|
if (!state)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
if (response.state == 'error') {
|
if (state == 'error') {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeUrlFromBuf(buf) {
|
||||||
|
const key = utils.toHex(cryptoUtils.sha256(buf));
|
||||||
|
return `disk://${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileBuf(buf, url) {
|
||||||
|
if (!url)
|
||||||
|
url = this.makeUrlFromBuf(buf);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
|
||||||
|
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
|
||||||
|
} catch (e) {
|
||||||
|
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadedFileBuf(url) {
|
||||||
|
url = url.replace('disk://', '/upload/');
|
||||||
|
return (await axios.get(url)).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBuc(bookUrls) {
|
||||||
|
const response = await wsc.message(await wsc.send({action: 'check-buc', bookUrls}));
|
||||||
|
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
|
||||||
|
if (!response.data)
|
||||||
|
throw new Error(`response.data is empty`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Reader();
|
export default new Reader();
|
||||||
@@ -238,7 +238,7 @@ class App {
|
|||||||
const url = s[1] || '';
|
const url = s[1] || '';
|
||||||
const q = utils.parseQuery(s[0] || '');
|
const q = utils.parseQuery(s[0] || '');
|
||||||
if (url) {
|
if (url) {
|
||||||
q.url = decodeURIComponent(url);
|
q.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.history.replaceState({}, '', '/');
|
window.history.replaceState({}, '', '/');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
Открыть выбранную закладку
|
Открыть выбранную закладку
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-input ref="search" v-model="search" class="col" rounded outlined dense bg-color="white" placeholder="Найти">
|
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
|
||||||
</template>
|
</template>
|
||||||
@@ -55,16 +55,16 @@
|
|||||||
|
|
||||||
<div class="col fit tree">
|
<div class="col fit tree">
|
||||||
<div v-show="nodes.length" class="checkbox-tick-all">
|
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||||
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @input="makeTickAll" />
|
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
|
||||||
</div>
|
</div>
|
||||||
<q-tree
|
<q-tree
|
||||||
|
v-model:selected="selected"
|
||||||
|
v-model:ticked="ticked"
|
||||||
|
v-model:expanded="expanded"
|
||||||
class="q-my-xs"
|
class="q-my-xs"
|
||||||
:nodes="nodes"
|
:nodes="nodes"
|
||||||
node-key="key"
|
node-key="key"
|
||||||
tick-strategy="leaf"
|
tick-strategy="leaf"
|
||||||
v-model:selected="selected"
|
|
||||||
v-model:ticked="ticked"
|
|
||||||
v-model:expanded="expanded"
|
|
||||||
selected-color="black"
|
selected-color="black"
|
||||||
:filter="search"
|
:filter="search"
|
||||||
no-nodes-label="Закладок пока нет"
|
no-nodes-label="Закладок пока нет"
|
||||||
@@ -97,7 +97,7 @@ const componentOptions = {
|
|||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ticked: function() {
|
ticked() {
|
||||||
this.checkAllTicked();
|
this.checkAllTicked();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
||||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
|
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||||
<q-icon name="la la-plus" size="16px" />
|
<q-icon name="la la-plus" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||||
<q-icon name="la la-minus" size="16px" />
|
<q-icon name="la la-minus" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||||
<q-icon name="la la-question-circle" size="16px" />
|
<q-icon name="la la-question-circle" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
:options="rootLinkOptions"
|
:options="rootLinkOptions"
|
||||||
style="width: 230px"
|
style="width: 230px"
|
||||||
dropdown-icon="la la-angle-down la-sm"
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
|
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
:options="selectedLinkOptions"
|
:options="selectedLinkOptions"
|
||||||
style="width: 50px"
|
style="width: 50px"
|
||||||
dropdown-icon="la la-angle-down la-sm"
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||||
>
|
>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
v-model="bookUrl"
|
v-model="bookUrl"
|
||||||
class="col q-mr-sm"
|
class="col q-mr-sm"
|
||||||
rounded outlined dense
|
outlined dense
|
||||||
bg-color="white"
|
bg-color="white"
|
||||||
placeholder="Скопируйте сюда URL книги"
|
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
|
||||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-btn :disabled="!bookUrl" rounded color="green-7" no-caps size="14px" @click="submitUrl">
|
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
|
||||||
Открыть
|
Открыть
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
Открыть в читалке
|
Открыть в читалке
|
||||||
@@ -894,14 +894,15 @@ export default vueComponent(ExternalLibs);
|
|||||||
background-color: #A0A0A0;
|
background-color: #A0A0A0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-screen-button {
|
.header-button {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-screen-button:hover {
|
.header-button:hover {
|
||||||
background-color: #69C05F;
|
color: white;
|
||||||
|
background-color: #39902F;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparent-layout {
|
.transparent-layout {
|
||||||
|
|||||||
@@ -23,15 +23,15 @@
|
|||||||
|
|
||||||
<div class="q-mb-sm" />
|
<div class="q-mb-sm" />
|
||||||
|
|
||||||
<div v-show="selectedTab == 'contents'" class="tab-panel">
|
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
<div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" />
|
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||||
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px" />
|
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
<div :style="item.indentStyle"></div>
|
<div :style="item.indentStyle"></div>
|
||||||
@@ -42,8 +42,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
<div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
|
||||||
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
|
<div
|
||||||
|
v-for="subitem in item.list"
|
||||||
|
:ref="`subitem${subitem.key}`"
|
||||||
|
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
|
||||||
|
>
|
||||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||||
<div class="no-expand-button"></div>
|
<div class="no-expand-button"></div>
|
||||||
<div :style="subitem.indentStyle"></div>
|
<div :style="subitem.indentStyle"></div>
|
||||||
@@ -61,10 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="selectedTab == 'images'" class="tab-panel">
|
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
<div class="image-thumb-box row justify-center items-center">
|
<div class="image-thumb-box row justify-center items-center">
|
||||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
||||||
@@ -124,7 +128,10 @@ const componentOptions = {
|
|||||||
watch: {
|
watch: {
|
||||||
bookPos() {
|
bookPos() {
|
||||||
this.updateBookPosSelection();
|
this.updateBookPosSelection();
|
||||||
}
|
},
|
||||||
|
selectedTab() {
|
||||||
|
this.updateBookPosScrollTop();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
class ContentsPage {
|
class ContentsPage {
|
||||||
@@ -282,31 +289,30 @@ class ContentsPage {
|
|||||||
if (!this.isVisible)
|
if (!this.isVisible)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await utils.sleep(50);
|
await this.$nextTick();
|
||||||
const bp = this.bookPos;
|
const bp = this.bookPos;
|
||||||
|
|
||||||
for (let i = 0; i < this.contents.length; i++) {
|
for (let i = 0; i < this.contents.length; i++) {
|
||||||
const item = this.contents[i];
|
const item = this.contents[i];
|
||||||
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||||
|
|
||||||
|
if (bp >= item.offset && bp < nextOffset) {
|
||||||
|
item.isBookPos = true;
|
||||||
|
} else if (item.isBookPos) {
|
||||||
|
item.isBookPos = false;
|
||||||
|
}
|
||||||
|
|
||||||
for (let j = 0; j < item.list.length; j++) {
|
for (let j = 0; j < item.list.length; j++) {
|
||||||
const subitem = item.list[j];
|
const subitem = item.list[j];
|
||||||
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||||
|
|
||||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||||
subitem.isBookPos = true;
|
subitem.isBookPos = true;
|
||||||
this.contents[i] = Object.assign(item, {list: item.list});
|
this.updateBookPosScrollTop('contents', item, subitem, j);
|
||||||
} else if (subitem.isBookPos) {
|
} else if (subitem.isBookPos) {
|
||||||
subitem.isBookPos = false;
|
subitem.isBookPos = false;
|
||||||
this.contents[i] = Object.assign(item, {list: item.list});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bp >= item.offset && bp < nextOffset) {
|
|
||||||
this.contents[i] = Object.assign(item, {isBookPos: true});
|
|
||||||
} else if (item.isBookPos) {
|
|
||||||
this.contents[i] = Object.assign(item, {isBookPos: false});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.images.length; i++) {
|
for (let i = 0; i < this.images.length; i++) {
|
||||||
@@ -314,11 +320,96 @@ class ContentsPage {
|
|||||||
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||||
|
|
||||||
if (bp >= img.offset && bp < nextOffset) {
|
if (bp >= img.offset && bp < nextOffset) {
|
||||||
this.images[i] = Object.assign(img, {isBookPos: true});
|
this.images[i].isBookPos = true;
|
||||||
} else if (img.isBookPos) {
|
} else if (img.isBookPos) {
|
||||||
this.images[i] = Object.assign(img, {isBookPos: false});
|
this.images[i].isBookPos = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateBookPosScrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*getOffsetTop(key) {
|
||||||
|
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
|
||||||
|
return (el ? el.offsetTop : 0);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
async updateBookPosScrollTop() {
|
||||||
|
try {
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
if (this.selectedTab == 'contents') {
|
||||||
|
let item;
|
||||||
|
let subitem;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
//ищем выделенные item
|
||||||
|
for(const _item of this.contents) {
|
||||||
|
if (_item.isBookPos) {
|
||||||
|
item = _item;
|
||||||
|
for (let ii = 0; ii < item.list.length; ii++) {
|
||||||
|
const _subitem = item.list[ii];
|
||||||
|
if (_subitem.isBookPos) {
|
||||||
|
subitem = _subitem;
|
||||||
|
i = ii;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//вычисляем и смещаем tabPanel.scrollTop
|
||||||
|
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
|
||||||
|
let elShift = 0;
|
||||||
|
if (subitem && item.expanded) {
|
||||||
|
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
|
||||||
|
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
|
||||||
|
} else {
|
||||||
|
elShift = el.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabPanel = this.$refs.tabPanelContents;
|
||||||
|
const halfH = tabPanel.clientHeight/2;
|
||||||
|
const newScrollTop = el.offsetTop - halfH - elShift;
|
||||||
|
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||||
|
tabPanel.scrollTop = newScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedTab == 'images') {
|
||||||
|
let item;
|
||||||
|
|
||||||
|
//ищем выделенные item
|
||||||
|
for(const _item of this.images) {
|
||||||
|
if (_item.isBookPos) {
|
||||||
|
item = _item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//вычисляем и смещаем tabPanel.scrollTop
|
||||||
|
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
|
||||||
|
|
||||||
|
const tabPanel = this.$refs.tabPanelImages;
|
||||||
|
const halfH = tabPanel.clientHeight/2;
|
||||||
|
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
|
||||||
|
|
||||||
|
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||||
|
tabPanel.scrollTop = newScrollTop;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstElem(items) {
|
||||||
|
return (Array.isArray(items) ? items[0] : items);
|
||||||
}
|
}
|
||||||
|
|
||||||
async expandClick(key) {
|
async expandClick(key) {
|
||||||
@@ -326,17 +417,17 @@ class ContentsPage {
|
|||||||
const expanded = !item.expanded;
|
const expanded = !item.expanded;
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
const subitems = this.$refs[`subitem${key}`];
|
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||||
subitems.style.height = '0';
|
subdiv.style.height = '0';
|
||||||
await utils.sleep(200);
|
await utils.sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contents[key] = Object.assign({}, item, {expanded});
|
this.contents[key].expanded = expanded;
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
const subitems = this.$refs[`subitem${key}`];
|
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||||
subitems.style.height = subitems.scrollHeight + 'px';
|
subdiv.style.height = subdiv.scrollHeight + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="box">
|
<div class="column items-center" style="width: 500px">
|
||||||
<p class="p">
|
<p class="p">
|
||||||
Вы можете пожертвовать на развитие проекта любую сумму:
|
Здесь вы можете пожертвовать на развитие проекта:
|
||||||
</p>
|
</p>
|
||||||
<div class="address">
|
|
||||||
<img class="logo" src="./assets/yoomoney.png">
|
|
||||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
|
|
||||||
Пожертвовать
|
|
||||||
</q-btn><br>
|
|
||||||
<div class="para">
|
|
||||||
{{ yooAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address">
|
<q-btn no-caps class="q-my-lg" color="green-8" size="14px" style="width: 200px" @click="makeDonation">
|
||||||
<img class="logo" src="./assets/paypal.png">
|
<q-icon class="q-mr-xs" name="la la-donate" size="24px" />
|
||||||
<div class="para">
|
Поддержать проект
|
||||||
{{ paypalAddress }}
|
</q-btn>
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address">
|
<div style="font-size: 60%">
|
||||||
<img class="logo" src="./assets/bitcoin.png">
|
* Ваш донат является подарком автору проекта
|
||||||
<div class="para">
|
|
||||||
{{ bitcoinAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address">
|
|
||||||
<img class="logo" src="./assets/litecoin.png">
|
|
||||||
<div class="para">
|
|
||||||
{{ litecoinAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address">
|
|
||||||
<img class="logo" src="./assets/monero.png">
|
|
||||||
<div class="para">
|
|
||||||
{{ moneroAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,28 +21,14 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import vueComponent from '../../../vueComponent.js';
|
import vueComponent from '../../../vueComponent.js';
|
||||||
|
|
||||||
import {copyTextToClipboard} from '../../../../share/utils';
|
import * as utils from '../../../../share/utils';
|
||||||
|
|
||||||
class DonateHelpPage {
|
class DonateHelpPage {
|
||||||
yooAddress = '410018702323056';
|
|
||||||
paypalAddress = 'bookpauk@gmail.com';
|
|
||||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
|
||||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
|
||||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
donateYooMoney() {
|
makeDonation() {
|
||||||
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
|
utils.makeDonation();
|
||||||
}
|
|
||||||
|
|
||||||
async copyAddress(address, prefix) {
|
|
||||||
const result = await copyTextToClipboard(address);
|
|
||||||
if (result)
|
|
||||||
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
|
||||||
else
|
|
||||||
this.$root.notify.error('Копирование не удалось');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,31 +49,4 @@ export default vueComponent(DonateHelpPage);
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
text-indent: 20px;
|
text-indent: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
|
||||||
max-width: 550px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
|
||||||
padding-top: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.para {
|
|
||||||
margin: 10px 10px 10px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 130px;
|
|
||||||
position: relative;
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
margin-left: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 120%;
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB |
@@ -1,17 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window @close="close">
|
<Window @close="close" style="z-index: 200">
|
||||||
<template #header>
|
<template #header>
|
||||||
Справка
|
Справка
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="col column" style="min-width: 600px">
|
<div class="col column" style="min-width: 600px">
|
||||||
<q-btn-toggle
|
<div class="bg-grey-3 row">
|
||||||
v-model="selectedTab"
|
<q-tabs
|
||||||
toggle-color="primary"
|
v-model="selectedTab"
|
||||||
no-caps unelevated
|
active-color="black"
|
||||||
:options="buttons"
|
active-bg-color="white"
|
||||||
/>
|
indicator-color="white"
|
||||||
<div class="separator"></div>
|
dense
|
||||||
|
no-caps
|
||||||
|
inline-label
|
||||||
|
class="bg-grey-4 text-grey-7"
|
||||||
|
>
|
||||||
|
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="activePage" ref="page" class="col"></component>
|
<component :is="activePage" ref="page" class="col"></component>
|
||||||
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.separator {
|
|
||||||
height: 1px;
|
|
||||||
background-color: #E0E0E0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,14 +33,15 @@ class VersionHistoryPage {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
let vh = [];
|
let vh = [];
|
||||||
for (const version of versionHistory) {
|
for (const v of versionHistory) {
|
||||||
vh.push(version.header);
|
vh.push(`${v.version} (${v.releaseDate})`);
|
||||||
}
|
}
|
||||||
this.versionHeader = vh;
|
this.versionHeader = vh;
|
||||||
|
|
||||||
let vc = [];
|
let vc = [];
|
||||||
for (const version of versionHistory) {
|
for (const v of versionHistory) {
|
||||||
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
let header = `${v.version} (${v.releaseDate})`;
|
||||||
|
vc.push({key: header, content: 'Версия ' + header + v.content});
|
||||||
}
|
}
|
||||||
this.versionContent = vc;
|
this.versionContent = vc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,21 +12,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
<q-input ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" placeholder="URL книги" @keydown="onInputKeydown">
|
<q-input
|
||||||
|
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
|
||||||
|
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
|
||||||
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<input id="file" ref="file" type="file" style="display: none;" @change="loadFile" />
|
<input
|
||||||
|
id="file" ref="file" type="file"
|
||||||
|
style="display: none;"
|
||||||
|
:accept="acceptFileExt"
|
||||||
|
@change="loadFile"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||||
|
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
|
||||||
Загрузить файл с диска
|
Загрузить файл с диска
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||||
|
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||||
Из буфера обмена
|
Из буфера обмена
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
@@ -45,6 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||||
|
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
|
||||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||||
|
|
||||||
@@ -53,6 +64,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||||
|
|
||||||
|
<Dialog ref="dialog1" v-model="findBookVisible">
|
||||||
|
<template #header>
|
||||||
|
Подсказка ;-)
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Если вы хотите найти определенную книгу, добро пожаловать в
|
||||||
|
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
|
||||||
|
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,12 +85,15 @@ import vueComponent from '../../vueComponent.js';
|
|||||||
|
|
||||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||||
|
|
||||||
|
import Dialog from '../../share/Dialog.vue';
|
||||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||||
import {versionHistory} from '../versionHistory';
|
import {versionHistory} from '../versionHistory';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
GithubCorner,
|
GithubCorner,
|
||||||
|
Dialog,
|
||||||
PasteTextPage,
|
PasteTextPage,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -77,6 +103,7 @@ class LoaderPage {
|
|||||||
bookUrl = null;
|
bookUrl = null;
|
||||||
loadPercent = 0;
|
loadPercent = 0;
|
||||||
pasteTextActive = false;
|
pasteTextActive = false;
|
||||||
|
findBookVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -109,14 +136,16 @@ class LoaderPage {
|
|||||||
return this.$store.state.config.version;
|
return this.$store.state.config.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get acceptFileExt() {
|
||||||
|
return this.$store.state.config.acceptFileExt;
|
||||||
|
}
|
||||||
|
|
||||||
get isExternalConverter() {
|
get isExternalConverter() {
|
||||||
return this.$store.state.config.useExternalBookConverter;
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
get clientVersion() {
|
get clientVersion() {
|
||||||
let v = versionHistory[0].header;
|
return versionHistory[0].version;
|
||||||
v = v.split(' ')[0];
|
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submitUrl() {
|
submitUrl() {
|
||||||
@@ -138,7 +167,7 @@ class LoaderPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadBufferClick() {
|
loadBufferClick() {
|
||||||
this.pasteTextToggle();
|
this.showPasteText();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBuffer(opts) {
|
loadBuffer(opts) {
|
||||||
@@ -148,6 +177,10 @@ class LoaderPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showPasteText() {
|
||||||
|
this.pasteTextActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
pasteTextToggle() {
|
pasteTextToggle() {
|
||||||
this.pasteTextActive = !this.pasteTextActive;
|
this.pasteTextActive = !this.pasteTextActive;
|
||||||
}
|
}
|
||||||
@@ -160,6 +193,10 @@ class LoaderPage {
|
|||||||
this.$emit('do-action', {action: 'donate'});
|
this.$emit('do-action', {action: 'donate'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findBook() {
|
||||||
|
this.findBookVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
openComments() {
|
openComments() {
|
||||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||||
}
|
}
|
||||||
@@ -168,26 +205,24 @@ class LoaderPage {
|
|||||||
window.open('http://old.omnireader.ru', '_blank');
|
window.open('http://old.omnireader.ru', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputKeydown(event) {
|
async onInputKeydown(event) {
|
||||||
if (event.key == 'Enter') {
|
if (event.key == 'Enter') {
|
||||||
|
await utils.sleep(100);
|
||||||
this.submitUrl();
|
this.submitUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
|
if (this.$refs.dialog1.active)
|
||||||
|
return true;
|
||||||
|
|
||||||
if (this.pasteTextActive) {
|
if (this.pasteTextActive) {
|
||||||
return this.$refs.pasteTextPage.keyHook(event);
|
return this.$refs.pasteTextPage.keyHook(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = this.$refs.input.getNativeElement();
|
const input = this.$refs.input.getNativeElement();
|
||||||
if (event.type == 'keydown' && document.activeElement !== input) {
|
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
|
||||||
const action = this.$root.readerActionByKeyEvent(event);
|
return true;
|
||||||
switch (action) {
|
|
||||||
case 'help':
|
|
||||||
this.openHelp(event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
|
||||||
<div class="column justify-start items-center" style="height: 250px">
|
<div class="column justify-start items-center" style="height: 250px">
|
||||||
<q-circular-progress
|
<q-circular-progress
|
||||||
show-value
|
show-value
|
||||||
|
|||||||
@@ -2,16 +2,35 @@
|
|||||||
<div class="column no-wrap">
|
<div class="column no-wrap">
|
||||||
<div v-show="toolBarActive" ref="header" class="header">
|
<div v-show="toolBarActive" ref="header" class="header">
|
||||||
<div ref="buttons" class="row justify-between no-wrap">
|
<div ref="buttons" class="row justify-between no-wrap">
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
||||||
<q-icon name="la la-arrow-left" size="32px" />
|
<q-icon name="la la-arrow-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
{{ rstore.readerActions['loader'] }}
|
{{ rstore.readerActions['loader'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
|
||||||
|
<q-icon name="la la-caret-square-up" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['loadFile'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
|
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
|
||||||
|
<q-icon name="la la-comment" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['loadBuffer'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
|
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
|
||||||
|
<q-icon name="la la-question" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['help'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
|
<div class="space"></div>
|
||||||
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
||||||
<q-icon name="la la-angle-left" size="32px" />
|
<q-icon name="la la-angle-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -81,14 +100,27 @@
|
|||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
|
<button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
|
||||||
|
<div v-show="bothBucEnabled && needBookUpdateCount > 0" style="position: absolute">
|
||||||
|
<div class="need-book-update-count">
|
||||||
|
{{ needBookUpdateCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-icon name="la la-book-open" size="32px" />
|
<q-icon name="la la-book-open" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
{{ rstore.readerActions['recentBooks'] }}
|
{{ rstore.readerActions['recentBooks'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="space"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
|
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
|
||||||
|
<q-icon name="la la-mouse" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['clickControl'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
||||||
<q-icon name="la la-unlink" size="32px" />
|
<q-icon name="la la-unlink" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -115,6 +147,7 @@
|
|||||||
@load-file="loadFile"
|
@load-file="loadFile"
|
||||||
@book-pos-changed="bookPosChanged"
|
@book-pos-changed="bookPosChanged"
|
||||||
@do-action="doAction"
|
@do-action="doAction"
|
||||||
|
@hide-tool-bar="hideToolBar"
|
||||||
></component>
|
></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
|
|
||||||
@@ -129,14 +162,14 @@
|
|||||||
></SearchPage>
|
></SearchPage>
|
||||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
||||||
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
||||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose" @update-count-changed="updateCountChanged"></RecentBooksPage>
|
||||||
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
||||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></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>
|
||||||
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||||
|
|
||||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -167,6 +200,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
|||||||
|
|
||||||
import bookManager from './share/bookManager';
|
import bookManager from './share/bookManager';
|
||||||
import wallpaperStorage from './share/wallpaperStorage';
|
import wallpaperStorage from './share/wallpaperStorage';
|
||||||
|
import coversStorage from './share/coversStorage';
|
||||||
import dynamicCss from '../../share/dynamicCss';
|
import dynamicCss from '../../share/dynamicCss';
|
||||||
|
|
||||||
import rstore from '../../store/modules/reader';
|
import rstore from '../../store/modules/reader';
|
||||||
@@ -175,6 +209,7 @@ import miscApi from '../../api/misc';
|
|||||||
|
|
||||||
import {versionHistory} from './versionHistory';
|
import {versionHistory} from './versionHistory';
|
||||||
import * as utils from '../../share/utils';
|
import * as utils from '../../share/utils';
|
||||||
|
import LockQueue from '../../share/LockQueue';
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
@@ -245,6 +280,8 @@ class Reader {
|
|||||||
rstore = {};
|
rstore = {};
|
||||||
|
|
||||||
loaderActive = false;
|
loaderActive = false;
|
||||||
|
loadFileActive = false;
|
||||||
|
loadBufferActive = false;
|
||||||
fullScreenActive = false;
|
fullScreenActive = false;
|
||||||
setPositionActive = false;
|
setPositionActive = false;
|
||||||
searchActive = false;
|
searchActive = false;
|
||||||
@@ -254,7 +291,7 @@ class Reader {
|
|||||||
contentsActive = false;
|
contentsActive = false;
|
||||||
libsActive = false;
|
libsActive = false;
|
||||||
recentBooksActive = false;
|
recentBooksActive = false;
|
||||||
offlineModeActive = false;
|
clickControlActive = false;
|
||||||
settingsActive = false;
|
settingsActive = false;
|
||||||
|
|
||||||
clickMapActive = false;
|
clickMapActive = false;
|
||||||
@@ -277,6 +314,10 @@ class Reader {
|
|||||||
donationVisible = false;
|
donationVisible = false;
|
||||||
dualPageMode = false;
|
dualPageMode = false;
|
||||||
|
|
||||||
|
bucEnabled = false;
|
||||||
|
bucSetOnNew = false;
|
||||||
|
needBookUpdateCount = 0;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.rstore = rstore;
|
this.rstore = rstore;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -284,6 +325,8 @@ class Reader {
|
|||||||
this.reader = this.$store.state.reader;
|
this.reader = this.$store.state.reader;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
|
|
||||||
|
this.lock = new LockQueue(100);
|
||||||
|
|
||||||
this.$root.addEventHook('key', this.keyHook);
|
this.$root.addEventHook('key', this.keyHook);
|
||||||
|
|
||||||
this.lastActivePage = false;
|
this.lastActivePage = false;
|
||||||
@@ -316,6 +359,39 @@ class Reader {
|
|||||||
this.debouncedSetRecentBook(newValue);
|
this.debouncedSetRecentBook(newValue);
|
||||||
}, 15000, {maxWait: 20000});
|
}, 15000, {maxWait: 20000});
|
||||||
|
|
||||||
|
this.debouncedHideToolBar = _.debounce((event) => {
|
||||||
|
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
|
||||||
|
this.commit('reader/setToolBarActive', !!event.show);
|
||||||
|
this.$root.eventHook('resize');
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
this.debouncedRecentBooksPageUpdate = _.debounce(async() => {
|
||||||
|
if (this.recentBooksActive) {
|
||||||
|
await this.$refs.recentBooksPage.updateTableData();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
this.recentItemKeys = [];
|
||||||
|
this.debouncedSaveRecent = _.debounce(async() => {
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
if (!this.offlineModeActive)
|
||||||
|
this.$root.notify.error('Таймаут соединения');
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const itemKeys = this.recentItemKeys;
|
||||||
|
this.recentItemKeys = [];
|
||||||
|
//сохранение в удаленном хранилище
|
||||||
|
await this.$refs.serverStorage.saveRecent(itemKeys);
|
||||||
|
} catch (e) {
|
||||||
|
if (!this.offlineModeActive)
|
||||||
|
this.$root.notify.error(e.message);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, 500, {maxWait: 1000});
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
});
|
});
|
||||||
@@ -324,10 +400,10 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateHeaderMinWidth();
|
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
await wallpaperStorage.init();
|
await wallpaperStorage.init();
|
||||||
|
await coversStorage.init();
|
||||||
|
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
bookManager.addEventListener(this.bookManagerEvent);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
|
|
||||||
@@ -353,16 +429,30 @@ class Reader {
|
|||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
|
|
||||||
await this.$refs.dialogs.init();
|
await this.$refs.dialogs.init();
|
||||||
|
|
||||||
|
this.$refs.recentBooksPage.init();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
//проверки обновлений читалки
|
||||||
(async() => {
|
(async() => {
|
||||||
this.isFirstNeedUpdateNotify = true;
|
this.isFirstNeedUpdateNotify = true;
|
||||||
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
|
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
|
||||||
while (true) {// eslint-disable-line no-constant-condition
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
await this.checkNewVersionAvailable();
|
await this.checkNewVersionAvailable();
|
||||||
await utils.sleep(3600*1000); //каждый час
|
await utils.sleep(60*60*1000); //каждый час
|
||||||
}
|
}
|
||||||
//дальше кода нет
|
//дальше хода нет
|
||||||
|
})();
|
||||||
|
|
||||||
|
//проверки обновлений книг
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(15*1000); //подождем неск. секунд перед первым запросом
|
||||||
|
//вечный цикл, запрашиваем периодически обновления
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
await this.checkBuc();
|
||||||
|
await utils.sleep(70*60*1000); //каждые 70 минут
|
||||||
|
}
|
||||||
|
//дальше хода нет
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +462,10 @@ class Reader {
|
|||||||
this.copyFullText = settings.copyFullText;
|
this.copyFullText = settings.copyFullText;
|
||||||
this.showClickMapPage = settings.showClickMapPage;
|
this.showClickMapPage = settings.showClickMapPage;
|
||||||
this.clickControl = settings.clickControl;
|
this.clickControl = settings.clickControl;
|
||||||
|
this.clickControlActive = this.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
|
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
|
||||||
this.enableSitesFilter = settings.enableSitesFilter;
|
this.enableSitesFilter = settings.enableSitesFilter;
|
||||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||||
this.splitToPara = settings.splitToPara;
|
this.splitToPara = settings.splitToPara;
|
||||||
@@ -382,35 +474,80 @@ class Reader {
|
|||||||
this.pdfQuality = settings.pdfQuality;
|
this.pdfQuality = settings.pdfQuality;
|
||||||
this.dualPageMode = settings.dualPageMode;
|
this.dualPageMode = settings.dualPageMode;
|
||||||
this.userWallpapers = settings.userWallpapers;
|
this.userWallpapers = settings.userWallpapers;
|
||||||
|
this.bucEnabled = settings.bucEnabled;
|
||||||
|
this.bucSizeDiff = settings.bucSizeDiff;
|
||||||
|
this.bucSetOnNew = settings.bucSetOnNew;
|
||||||
|
this.bucCancelEnabled = settings.bucCancelEnabled;
|
||||||
|
this.bucCancelDays = settings.bucCancelDays;
|
||||||
|
|
||||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||||
this.$root.readerActionByKeyEvent = (event) => {
|
this.$root.readerActionByKeyEvent = (event) => {
|
||||||
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateHeaderMinWidth();
|
|
||||||
|
|
||||||
this.loadWallpapers();//no await
|
this.loadWallpapers();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showHelpOnErrorIfNeeded(errorMessage) {
|
||||||
|
//небольшая эвристика
|
||||||
|
let i = errorMessage.indexOf('http://');
|
||||||
|
if (i < 0)
|
||||||
|
i = errorMessage.indexOf('https://');
|
||||||
|
|
||||||
|
errorMessage = errorMessage.substring(i + 7);
|
||||||
|
const perCount = errorMessage.split('%').length - 1;
|
||||||
|
|
||||||
|
if (perCount > errorMessage.length/3.2) {
|
||||||
|
this.$refs.dialogs.showUrlHelp();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
//wallpaper css
|
//wallpaper css
|
||||||
async loadWallpapers() {
|
async loadWallpapers() {
|
||||||
const wallpaperDataLength = await wallpaperStorage.getLength();
|
if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
|
||||||
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
|
this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
|
||||||
this.wallpaperDataLength = wallpaperDataLength;
|
|
||||||
|
|
||||||
let newCss = '';
|
let newCss = '';
|
||||||
|
let updated = false;
|
||||||
|
const wallpaperExists = new Set();
|
||||||
for (const wp of this.userWallpapers) {
|
for (const wp of this.userWallpapers) {
|
||||||
const data = await wallpaperStorage.getData(wp.cssClass);
|
wallpaperExists.add(wp.cssClass);
|
||||||
|
|
||||||
|
let data = await wallpaperStorage.getData(wp.cssClass);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
//здесь будем восстанавливать данные с сервера
|
//здесь будем восстанавливать данные с сервера
|
||||||
|
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
|
||||||
|
try {
|
||||||
|
data = await readerApi.getUploadedFileBuf(url);
|
||||||
|
await wallpaperStorage.setData(wp.cssClass, data);
|
||||||
|
updated = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//почистим wallpaperStorage
|
||||||
|
for (const key of await wallpaperStorage.getKeys()) {
|
||||||
|
if (!wallpaperExists.has(key)) {
|
||||||
|
await wallpaperStorage.removeData(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//обновим settings, если загружали обои из /upload/
|
||||||
|
if (updated) {
|
||||||
|
const newSettings = _.cloneDeep(this.settings);
|
||||||
|
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
dynamicCss.replace('wallpapers', newCss);
|
dynamicCss.replace('wallpapers', newCss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,15 +576,90 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderMinWidth() {
|
async checkBuc() {
|
||||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
if (!this.bothBucEnabled)
|
||||||
if (this.$refs.buttons)
|
return;
|
||||||
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
|
|
||||||
(async() => {
|
try {
|
||||||
await utils.sleep(1000);
|
const sorted = bookManager.getSortedRecent();
|
||||||
if (this.$refs.header)
|
|
||||||
this.$refs.header.style.overflowX = 'auto';
|
//выберем все кандидиаты на обновление
|
||||||
})();
|
const updateUrls = new Set();
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (!book.deleted && book.checkBuc && book.url && book.url.indexOf('disk://') !== 0)
|
||||||
|
updateUrls.add(book.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
//теперь по кусочкам запросим сервер
|
||||||
|
const arr = Array.from(updateUrls);
|
||||||
|
const bucSize = {};
|
||||||
|
const chunkSize = 100;
|
||||||
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
|
const chunk = arr.slice(i, i + chunkSize);
|
||||||
|
|
||||||
|
const data = await readerApi.checkBuc(chunk);
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
bucSize[item.id] = item.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(1000);//чтобы не ддосить сервер
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSetTime = {};
|
||||||
|
//проставим новые размеры у книг
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (book.deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//размер 0 считаем отсутствующим
|
||||||
|
if (book.url && bucSize[book.url] && bucSize[book.url] !== book.bucSize) {
|
||||||
|
book.bucSize = bucSize[book.url];
|
||||||
|
await bookManager.recentSetItem(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
//подготовка к следующему шагу, ищем книгу по url с максимальной датой установки checkBucTime/loadTime
|
||||||
|
//от этой даты будем потом отсчитывать bucCancelDays
|
||||||
|
if (updateUrls.has(book.url)) {
|
||||||
|
let rec = checkSetTime[book.url] || {time: 0, loadTime: 0};
|
||||||
|
|
||||||
|
const time = (book.checkBucTime ? book.checkBucTime : (rec.loadTime || 0));
|
||||||
|
if (time > rec.time || (time == rec.time && (book.loadTime > rec.loadTime)))
|
||||||
|
rec = {time, loadTime: book.loadTime, key: book.key};
|
||||||
|
|
||||||
|
checkSetTime[book.url] = rec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//bucCancelEnabled и bucCancelDays
|
||||||
|
//снимем флаг checkBuc у необновлявшихся bucCancelDays
|
||||||
|
if (this.bucCancelEnabled) {
|
||||||
|
for (const rec of Object.values(checkSetTime)) {
|
||||||
|
if (rec.time && Date.now() - rec.time > this.bucCancelDays*24*3600*1000) {
|
||||||
|
const book = await bookManager.getRecentBook({key: rec.key});
|
||||||
|
const needBookUpdate =
|
||||||
|
book.checkBuc
|
||||||
|
&& book.bucSize
|
||||||
|
&& utils.hasProp(book, 'downloadSize')
|
||||||
|
&& book.bucSize !== book.downloadSize
|
||||||
|
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff)
|
||||||
|
;
|
||||||
|
|
||||||
|
if (book && !needBookUpdate) {
|
||||||
|
await bookManager.setCheckBuc(book, undefined);//!!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$refs.recentBooksPage.updateTableData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCountChanged(event) {
|
||||||
|
this.needBookUpdateCount = event.needBookUpdateCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSetStorageAccessKey() {
|
checkSetStorageAccessKey() {
|
||||||
@@ -508,7 +720,7 @@ class Reader {
|
|||||||
return;
|
return;
|
||||||
const recent = this.mostRecentBook();
|
const recent = this.mostRecentBook();
|
||||||
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
||||||
const url = (recent ? `url=${recent.url}` : '');
|
const url = (recent ? `url=${encodeURIComponent(recent.url)}` : '');
|
||||||
if (isNewRoute)
|
if (isNewRoute)
|
||||||
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
|
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
|
||||||
else
|
else
|
||||||
@@ -525,9 +737,11 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get clientVersion() {
|
get clientVersion() {
|
||||||
let v = versionHistory[0].header;
|
return versionHistory[0].version;
|
||||||
v = v.split(' ')[0];
|
}
|
||||||
return v;
|
|
||||||
|
get bothBucEnabled() {
|
||||||
|
return this.$store.state.config.bucEnabled && this.bucEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
@@ -578,27 +792,12 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventName == 'recent-changed') {
|
if (eventName == 'recent-changed') {
|
||||||
if (this.recentBooksActive) {
|
this.debouncedRecentBooksPageUpdate();
|
||||||
await this.$refs.recentBooksPage.updateTableData();
|
|
||||||
}
|
|
||||||
|
|
||||||
//сохранение в serverStorage
|
//сохранение в serverStorage
|
||||||
if (value) {
|
if (value && this.recentItemKeys.indexOf(value) < 0) {
|
||||||
await utils.sleep(500);
|
this.recentItemKeys.push(value);
|
||||||
|
this.debouncedSaveRecent();
|
||||||
let timer = setTimeout(() => {
|
|
||||||
if (!this.offlineModeActive)
|
|
||||||
this.$root.notify.error('Таймаут соединения');
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$refs.serverStorage.saveRecent(value);
|
|
||||||
} catch (e) {
|
|
||||||
if (!this.offlineModeActive)
|
|
||||||
this.$root.notify.error(e.message);
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,6 +806,10 @@ class Reader {
|
|||||||
return this.reader.toolBarActive;
|
return this.reader.toolBarActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get offlineModeActive() {
|
||||||
|
return this.reader.offlineModeActive;
|
||||||
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
const result = bookManager.mostRecentBook();
|
const result = bookManager.mostRecentBook();
|
||||||
this.mostRecentBookReactive = result;
|
this.mostRecentBookReactive = result;
|
||||||
@@ -632,6 +835,10 @@ class Reader {
|
|||||||
this.$root.eventHook('resize');
|
this.$root.eventHook('resize');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideToolBar(event) {
|
||||||
|
this.debouncedHideToolBar(event);
|
||||||
|
}
|
||||||
|
|
||||||
fullScreenToggle() {
|
fullScreenToggle() {
|
||||||
this.fullScreenActive = !this.fullScreenActive;
|
this.fullScreenActive = !this.fullScreenActive;
|
||||||
if (this.fullScreenActive) {
|
if (this.fullScreenActive) {
|
||||||
@@ -659,6 +866,28 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadFileToggle() {
|
||||||
|
if (!this.loaderActive)
|
||||||
|
this.loaderToggle();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const page = this.$refs.page;
|
||||||
|
if (this.activePage == 'LoaderPage' && page.loadFileClick) {
|
||||||
|
page.loadFileClick();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBufferToggle() {
|
||||||
|
if (!this.loaderActive)
|
||||||
|
this.loaderToggle();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const page = this.$refs.page;
|
||||||
|
if (this.activePage == 'LoaderPage' && page.showPasteText) {
|
||||||
|
page.showPasteText();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setPositionToggle() {
|
setPositionToggle() {
|
||||||
this.setPositionActive = !this.setPositionActive;
|
this.setPositionActive = !this.setPositionActive;
|
||||||
const page = this.$refs.page;
|
const page = this.$refs.page;
|
||||||
@@ -786,8 +1015,14 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickControlToggle() {
|
||||||
|
const newSettings = _.cloneDeep(this.settings);
|
||||||
|
newSettings.clickControl = !this.clickControl;
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
offlineModeToggle() {
|
offlineModeToggle() {
|
||||||
this.offlineModeActive = !this.offlineModeActive;
|
this.commit('reader/setOfflineModeActive', !this.offlineModeActive);
|
||||||
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,7 +1074,7 @@ class Reader {
|
|||||||
|
|
||||||
refreshBook() {
|
refreshBook() {
|
||||||
const mrb = this.mostRecentBook();
|
const mrb = this.mostRecentBook();
|
||||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
this.loadBook(Object.assign({}, mrb, {force: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
undoAction() {
|
undoAction() {
|
||||||
@@ -874,6 +1109,9 @@ class Reader {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'loader':
|
case 'loader':
|
||||||
|
case 'loadFile':
|
||||||
|
case 'loadBuffer':
|
||||||
|
case 'help':
|
||||||
case 'fullScreen':
|
case 'fullScreen':
|
||||||
case 'setPosition':
|
case 'setPosition':
|
||||||
case 'search':
|
case 'search':
|
||||||
@@ -883,6 +1121,7 @@ class Reader {
|
|||||||
case 'contents':
|
case 'contents':
|
||||||
case 'libs':
|
case 'libs':
|
||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
|
case 'clickControl':
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (this.progressActive) {
|
if (this.progressActive) {
|
||||||
@@ -920,7 +1159,6 @@ class Reader {
|
|||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
case 'recentBooks':
|
|
||||||
if (!this.mostRecentBookReactive)
|
if (!this.mostRecentBookReactive)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
@@ -989,7 +1227,7 @@ class Reader {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBook(opts) {
|
async _loadBook(opts) {
|
||||||
if (!opts || !opts.url) {
|
if (!opts || !opts.url) {
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
return;
|
return;
|
||||||
@@ -999,10 +1237,6 @@ class Reader {
|
|||||||
|
|
||||||
let url = encodeURI(decodeURI(opts.url));
|
let url = encodeURI(decodeURI(opts.url));
|
||||||
|
|
||||||
//TODO: убрать конвертирование 'file://' после 06.2021
|
|
||||||
if (url.length == 71 && url.indexOf('file://') == 0)
|
|
||||||
url = url.replace(/^file/, 'disk');
|
|
||||||
|
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||||
(url.indexOf('disk://') != 0))
|
(url.indexOf('disk://') != 0))
|
||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
@@ -1029,33 +1263,38 @@ class Reader {
|
|||||||
progress.show();
|
progress.show();
|
||||||
progress.setState({state: 'parse'});
|
progress.setState({state: 'parse'});
|
||||||
|
|
||||||
// есть ли среди недавних
|
// есть ли среди загруженных
|
||||||
const key = bookManager.keyFromUrl(url);
|
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
|
||||||
let wasOpened = await bookManager.getRecentBook({key});
|
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
|
||||||
wasOpened = (wasOpened ? wasOpened : {});
|
|
||||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
wasOpened = Object.assign(wasOpened, {
|
||||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
url: (opts.url !== undefined ? opts.url : wasOpened.url),
|
||||||
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
path: (opts.path !== undefined ? opts.path : wasOpened.path),
|
||||||
|
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
|
||||||
|
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
|
||||||
|
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
|
||||||
|
});
|
||||||
|
|
||||||
let book = null;
|
let book = null;
|
||||||
|
|
||||||
if (!opts.force) {
|
if (!opts.force) {
|
||||||
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
||||||
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
|
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
|
||||||
progress.setState({progress: prog});
|
progress.setState({progress: prog});
|
||||||
});
|
});
|
||||||
|
|
||||||
// если есть в локальном кэше
|
// если есть в локальном кэше
|
||||||
if (bookParsed) {
|
if (bookParsed) {
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
|
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(wasOpened.bookPos);
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.blinkCachedLoadMessage();
|
this.blinkCachedLoadMessage();
|
||||||
|
|
||||||
this.checkBookPosPercent();
|
this.checkBookPosPercent();
|
||||||
await this.activateClickMapPage();
|
this.activateClickMapPage();//no await
|
||||||
|
this.$refs.recentBooksPage.updateTableData();//no await
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,7 +1308,7 @@ class Reader {
|
|||||||
});
|
});
|
||||||
book = Object.assign({}, wasOpened, {data: resp.data});
|
book = Object.assign({}, wasOpened, {data: resp.data});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//молчим
|
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1080,7 +1319,7 @@ class Reader {
|
|||||||
if (!book) {
|
if (!book) {
|
||||||
book = await readerApi.loadBook({
|
book = await readerApi.loadBook({
|
||||||
url,
|
url,
|
||||||
uploadFileName,
|
uploadFileName: wasOpened.uploadFileName,
|
||||||
enableSitesFilter: this.enableSitesFilter,
|
enableSitesFilter: this.enableSitesFilter,
|
||||||
skipHtmlCheck: (this.splitToPara ? true : false),
|
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||||
isText: (this.splitToPara ? true : false),
|
isText: (this.splitToPara ? true : false),
|
||||||
@@ -1097,14 +1336,48 @@ class Reader {
|
|||||||
|
|
||||||
// добавляем в bookManager
|
// добавляем в bookManager
|
||||||
progress.setState({state: 'parse', step: 5});
|
progress.setState({state: 'parse', step: 5});
|
||||||
|
|
||||||
const addedBook = await bookManager.addBook(book, (prog) => {
|
const addedBook = await bookManager.addBook(book, (prog) => {
|
||||||
progress.setState({progress: prog});
|
progress.setState({progress: prog});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// sameBookKey
|
||||||
|
if (url.indexOf('disk://') == 0) {
|
||||||
|
//ищем такой файл в загруженных
|
||||||
|
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
|
||||||
|
found = (found ? _.cloneDeep(found) : found);
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
if (wasOpened.sameBookKey != found.sameBookKey) {
|
||||||
|
//спрашиваем, надо ли объединить файлы
|
||||||
|
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
|
||||||
|
await this.$root.stdDialog.askYesNo(`
|
||||||
|
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
|
||||||
|
<br>Объединить позицию?`, 'Найдена похожая книга');
|
||||||
|
if (askResult) {
|
||||||
|
wasOpened.bookPos = found.bookPos;
|
||||||
|
wasOpened.bookPosSeen = found.bookPosSeen;
|
||||||
|
wasOpened.sameBookKey = found.sameBookKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wasOpened.sameBookKey = wasOpened.uploadFileName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wasOpened.sameBookKey = addedBook.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
|
||||||
|
delete wasOpened.loadTime;
|
||||||
|
|
||||||
// добавляем в историю
|
// добавляем в историю
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
|
const recentBook = await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
|
||||||
|
if (this.bucSetOnNew) {
|
||||||
|
await bookManager.setCheckBuc(recentBook, true);
|
||||||
|
}
|
||||||
|
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(recentBook.bookPos);
|
||||||
this.updateRoute(true);
|
this.updateRoute(true);
|
||||||
|
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
@@ -1115,17 +1388,29 @@ class Reader {
|
|||||||
this.stopBlink = true;
|
this.stopBlink = true;
|
||||||
|
|
||||||
this.checkBookPosPercent();
|
this.checkBookPosPercent();
|
||||||
await this.activateClickMapPage();
|
this.activateClickMapPage();//no await
|
||||||
|
this.$refs.recentBooksPage.updateTableData();//no await
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
if (!this.showHelpOnErrorIfNeeded(url)) {
|
||||||
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.checkNewVersionAvailable();
|
this.checkNewVersionAvailable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadFile(opts) {
|
async loadBook(opts) {
|
||||||
|
await this.lock.get();
|
||||||
|
try {
|
||||||
|
await this._loadBook(opts);
|
||||||
|
} finally {
|
||||||
|
this.lock.ret();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadFile(opts) {
|
||||||
this.progressActive = true;
|
this.progressActive = true;
|
||||||
|
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
@@ -1141,7 +1426,7 @@ class Reader {
|
|||||||
|
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
|
|
||||||
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
await this._loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
@@ -1149,6 +1434,15 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadFile(opts) {
|
||||||
|
await this.lock.get();
|
||||||
|
try {
|
||||||
|
await this._loadFile(opts);
|
||||||
|
} finally {
|
||||||
|
this.lock.ret();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blinkCachedLoadMessage() {
|
blinkCachedLoadMessage() {
|
||||||
if (!this.blinkCachedLoad)
|
if (!this.blinkCachedLoad)
|
||||||
return;
|
return;
|
||||||
@@ -1185,6 +1479,12 @@ class Reader {
|
|||||||
case 'loader':
|
case 'loader':
|
||||||
this.loaderToggle();
|
this.loaderToggle();
|
||||||
break;
|
break;
|
||||||
|
case 'loadFile':
|
||||||
|
this.loadFileToggle();
|
||||||
|
break;
|
||||||
|
case 'loadBuffer':
|
||||||
|
this.loadBufferToggle();
|
||||||
|
break;
|
||||||
case 'help':
|
case 'help':
|
||||||
this.helpToggle();
|
this.helpToggle();
|
||||||
break;
|
break;
|
||||||
@@ -1227,6 +1527,9 @@ class Reader {
|
|||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
this.recentBooksToggle();
|
this.recentBooksToggle();
|
||||||
break;
|
break;
|
||||||
|
case 'clickControl':
|
||||||
|
this.clickControlToggle();
|
||||||
|
break;
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
this.offlineModeToggle();
|
this.offlineModeToggle();
|
||||||
break;
|
break;
|
||||||
@@ -1329,13 +1632,14 @@ class Reader {
|
|||||||
if (!result && event.type == 'keydown') {
|
if (!result && event.type == 'keydown') {
|
||||||
const action = this.$root.readerActionByKeyEvent(event);
|
const action = this.$root.readerActionByKeyEvent(event);
|
||||||
|
|
||||||
if (action == 'loader') {
|
/*if (action == 'loader') {
|
||||||
result = this.doAction({action, event});
|
result = this.doAction({action, event});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result && this.activePage == 'TextPage') {
|
if (!result && this.activePage == 'TextPage') {
|
||||||
result = this.doAction({action, event});
|
result = this.doAction({action, event});
|
||||||
}
|
}*/
|
||||||
|
result = this.doAction({action, event});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -1348,12 +1652,33 @@ export default vueComponent(Reader);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
|
height: 50px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
background-color: #1B695F;
|
background-color: #1B695F;
|
||||||
color: #000;
|
color: #000;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
height: 50px;
|
overflow-y: hidden;
|
||||||
|
scrollbar-color: #c49a60 #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-track {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #c49a60;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #b48a50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
@@ -1415,4 +1740,16 @@ export default vueComponent(Reader);
|
|||||||
.clear {
|
.clear {
|
||||||
color: rgba(0,0,0,0);
|
color: rgba(0,0,0,0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.need-book-update-count {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 6px 2px 6px;
|
||||||
|
left: 27px;
|
||||||
|
top: 22px;
|
||||||
|
background-color: blue;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,61 +5,84 @@
|
|||||||
Что нового:
|
Что нового:
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
<div style="line-height: 20px; min-width: 300px">
|
||||||
|
<div v-html="whatsNewContent"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||||
<span slot="footer">
|
|
||||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
<template #footer>
|
||||||
</span>
|
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
|
||||||
|
Больше не показывать
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog ref="dialog2" v-model="donationVisible">
|
<q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
|
||||||
|
<div class="column bg-white no-wrap q-pa-md">
|
||||||
|
<div class="row justify-center q-mb-md" style="font-size: 110%">
|
||||||
|
Здравствуйте, дорогие читатели!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md column" style="word-break: normal">
|
||||||
|
<div>
|
||||||
|
Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
|
||||||
|
|
||||||
|
Напоминаем вам, что проект является некоммерческим и обладает такими
|
||||||
|
достоинствами, как:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>все функции читалки открыты и доступны совершенно бесплатно</li>
|
||||||
|
<li>в проекте отсутствует какая-либо реклама или баннеры</li>
|
||||||
|
<li>нет никакой регистрации и монетизации</li>
|
||||||
|
<li>нет сбора персональных данных</li>
|
||||||
|
<li>открытый исходный код</li>
|
||||||
|
<li>проект постепенно улучшается, по мере возможности</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Однако на оплату хостинга читалки и сервера обновлений автор тратит свои
|
||||||
|
собственные средства, а также тратит свое время и силы на улучшение проекта.
|
||||||
|
<br><br>
|
||||||
|
Поддержим же материально наш ресурс, чтобы и дальше спокойно существовать и развиваться:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn style="margin: 10px 50px 10px 50px" color="green-8" size="14px" no-caps @click="makeDonation">
|
||||||
|
<q-icon class="q-mr-xs" name="la la-donate" size="24px" />
|
||||||
|
Поддержать проект
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn style="margin: 0 50px 20px 50px" size="14px" no-caps @click="donationDialogRemind">
|
||||||
|
Напомнить в следующем месяце
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<div class="row justify-center">
|
||||||
|
<div class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
|
||||||
|
Помочь проекту можно в любое время
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
||||||
<template #header>
|
<template #header>
|
||||||
Здравствуйте, уважаемые читатели!
|
Обнаружена невалидная ссылка в поле "URL книги".
|
||||||
|
<br>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div style="word-break: normal">
|
<div style="word-break: normal">
|
||||||
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
|
||||||
|
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
|
||||||
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||||
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
|
||||||
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
|
||||||
|
|
||||||
<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>
|
||||||
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
|
Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
|
||||||
<br><br>
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||||
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||||
|
Из буфера обмена
|
||||||
<br><br>
|
</q-btn>
|
||||||
<div class="row justify-center">
|
на странице загрузки.
|
||||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
|
|
||||||
Помочь проекту
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span slot="footer">
|
|
||||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
|
||||||
<br>
|
|
||||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
|
||||||
</span>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,6 +111,7 @@ class ReaderDialogs {
|
|||||||
whatsNewVisible = false;
|
whatsNewVisible = false;
|
||||||
whatsNewContent = '';
|
whatsNewContent = '';
|
||||||
donationVisible = false;
|
donationVisible = false;
|
||||||
|
urlHelpVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -105,43 +129,49 @@ class ReaderDialogs {
|
|||||||
loadSettings() {
|
loadSettings() {
|
||||||
const settings = this.settings;
|
const settings = this.settings;
|
||||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
this.showDonationDialog = settings.showDonationDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
async showWhatsNew() {
|
async showWhatsNew() {
|
||||||
const whatsNew = versionHistory[0];
|
const whatsNew = versionHistory[0];
|
||||||
if (this.showWhatsNewDialog &&
|
if (this.showWhatsNewDialog &&
|
||||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||||
whatsNew.header != this.whatsNewContentHash) {
|
this.whatsNewHeader != this.whatsNewContentHash) {
|
||||||
await utils.sleep(2000);
|
await utils.sleep(2000);
|
||||||
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
|
||||||
this.whatsNewVisible = true;
|
this.whatsNewVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async showDonation() {
|
async showDonation() {
|
||||||
const today = utils.formatDate(new Date(), 'coDate');
|
const today = utils.formatDate(new Date(), 'coMonth');
|
||||||
|
|
||||||
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && this.showDonationDialog && this.donationRemindDate != today) {
|
||||||
await utils.sleep(3000);
|
await utils.sleep(3000);
|
||||||
this.donationVisible = true;
|
this.donationVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
donationDialogDisable() {
|
async showUrlHelp() {
|
||||||
this.donationVisible = false;
|
this.urlHelpVisible = true;
|
||||||
if (this.showDonationDialog2020) {
|
}
|
||||||
this.commit('reader/setSettings', { showDonationDialog2020: false });
|
|
||||||
}
|
loadBufferClick() {
|
||||||
|
this.$emit('load-buffer-toggle');
|
||||||
|
this.urlHelpVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
donationDialogRemind() {
|
donationDialogRemind() {
|
||||||
this.donationVisible = false;
|
this.donationVisible = false;
|
||||||
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coMonth'));
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDonation() {
|
||||||
|
utils.makeDonation();
|
||||||
|
this.donationDialogRemind();
|
||||||
}
|
}
|
||||||
|
|
||||||
openDonate() {
|
openDonate() {
|
||||||
this.donationVisible = false;
|
|
||||||
this.$emit('donate-toggle');
|
this.$emit('donate-toggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +190,11 @@ class ReaderDialogs {
|
|||||||
|
|
||||||
whatsNewDisable() {
|
whatsNewDisable() {
|
||||||
this.whatsNewVisible = false;
|
this.whatsNewVisible = false;
|
||||||
const whatsNew = versionHistory[0];
|
this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
|
||||||
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
}
|
||||||
|
|
||||||
|
get whatsNewHeader() {
|
||||||
|
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mode() {
|
get mode() {
|
||||||
@@ -181,7 +214,7 @@ class ReaderDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyHook() {
|
keyHook() {
|
||||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
|
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,10 @@
|
|||||||
<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"
|
<q-input
|
||||||
placeholder="что ищем"
|
ref="input" v-model="needle"
|
||||||
:value="needle" @input="needle = $event.target.value"/-->
|
|
||||||
<q-input ref="input" v-model="needle"
|
|
||||||
class="col" outlined dense
|
class="col" outlined dense
|
||||||
placeholder="что ищем"
|
placeholder="Найти"
|
||||||
@keydown="inputKeyDown"
|
@keydown="inputKeyDown"
|
||||||
/>
|
/>
|
||||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
|
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
|
||||||
@@ -108,7 +106,7 @@ class SearchPage {
|
|||||||
this.parsed = parsed;
|
this.parsed = parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.header = 'Найти';
|
this.header = 'Поиск в тексте';
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.$refs.input.focus();
|
this.$refs.input.focus();
|
||||||
this.$refs.input.select();
|
this.$refs.input.select();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import bookManager from '../share/bookManager';
|
|||||||
import readerApi from '../../../api/reader';
|
import readerApi from '../../../api/reader';
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||||
|
import LockQueue from '../../../share/LockQueue';
|
||||||
|
|
||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
const ssCacheStore = localForage.createInstance({
|
const ssCacheStore = localForage.createInstance({
|
||||||
@@ -48,6 +49,8 @@ class ServerStorage {
|
|||||||
this.keyInited = false;
|
this.keyInited = false;
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.prevServerStorageKey = null;
|
this.prevServerStorageKey = null;
|
||||||
|
this.lock = new LockQueue(100);
|
||||||
|
|
||||||
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
|
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
|
||||||
|
|
||||||
this.debouncedSaveSettings = _.debounce(() => {
|
this.debouncedSaveSettings = _.debounce(() => {
|
||||||
@@ -542,14 +545,16 @@ class ServerStorage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRecent(itemKey, recurse) {
|
async saveRecent(itemKeys, recurse) {
|
||||||
while (!this.inited || this.savingRecent)
|
while (!this.inited)
|
||||||
await utils.sleep(100);
|
await utils.sleep(100);
|
||||||
|
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.savingRecent = true;
|
let needRecurseCall = false;
|
||||||
|
|
||||||
|
await this.lock.get();
|
||||||
try {
|
try {
|
||||||
const bm = bookManager;
|
const bm = bookManager;
|
||||||
|
|
||||||
@@ -559,22 +564,29 @@ class ServerStorage {
|
|||||||
|
|
||||||
//newRecentMod
|
//newRecentMod
|
||||||
let newRecentMod = {};
|
let newRecentMod = {};
|
||||||
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
|
let oneItemKey = null;
|
||||||
|
if (itemKeys && itemKeys.length == 1)
|
||||||
|
oneItemKey = itemKeys[0];
|
||||||
|
|
||||||
|
if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
|
||||||
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||||
newRecentMod.rev++;
|
newRecentMod.rev++;
|
||||||
|
|
||||||
newRecentMod.data.key = itemKey;
|
newRecentMod.data.key = oneItemKey;
|
||||||
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
|
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
|
||||||
needSaveRecentMod = true;
|
needSaveRecentMod = true;
|
||||||
}
|
}
|
||||||
this.prevItemKey = itemKey;
|
this.prevItemKey = oneItemKey;
|
||||||
|
|
||||||
//newRecentPatch
|
//newRecentPatch
|
||||||
let newRecentPatch = {};
|
let newRecentPatch = {};
|
||||||
if (itemKey && !needSaveRecentMod) {
|
if (itemKeys && !needSaveRecentMod) {
|
||||||
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||||
newRecentPatch.rev++;
|
newRecentPatch.rev++;
|
||||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
|
||||||
|
for (const key of itemKeys) {
|
||||||
|
newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
|
||||||
|
}
|
||||||
|
|
||||||
const applyMod = this.cachedRecentMod.data;
|
const applyMod = this.cachedRecentMod.data;
|
||||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||||
@@ -587,11 +599,7 @@ class ServerStorage {
|
|||||||
|
|
||||||
//newRecent
|
//newRecent
|
||||||
let newRecent = {};
|
let newRecent = {};
|
||||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
//ждем весь bm.recent
|
|
||||||
/*while (!bookManager.loaded)
|
|
||||||
await utils.sleep(100);*/
|
|
||||||
|
|
||||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
@@ -625,10 +633,8 @@ class ServerStorage {
|
|||||||
|
|
||||||
if (res)
|
if (res)
|
||||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
if (!recurse && itemKey) {
|
if (!recurse && itemKeys) {
|
||||||
this.savingRecent = false;
|
needRecurseCall = true;
|
||||||
await this.saveRecent(itemKey, true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else if (result.state == 'success') {
|
} else if (result.state == 'success') {
|
||||||
if (needSaveRecent && newRecent.rev)
|
if (needSaveRecent && newRecent.rev)
|
||||||
@@ -639,8 +645,11 @@ class ServerStorage {
|
|||||||
await this.setCachedRecentMod(newRecentMod);
|
await this.setCachedRecentMod(newRecentMod);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.savingRecent = false;
|
this.lock.ret();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needRecurseCall)
|
||||||
|
await this.saveRecent(itemKeys, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageCheck(items) {
|
async storageCheck(items) {
|
||||||
@@ -728,10 +737,10 @@ class ServerStorage {
|
|||||||
const ids = id.split('.');
|
const ids = id.split('.');
|
||||||
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||||
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||||
items[utils.fromBase58(ids[1])] = decoded;
|
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.items = items;
|
result.items = items;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<template #header>
|
<template #header>
|
||||||
Установить позицию
|
Установить позицию
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div id="set-position-slider" class="slider q-px-md">
|
<div class="col column justify-center">
|
||||||
<q-slider
|
<div id="set-position-slider" class="slider q-px-md column justify-center">
|
||||||
v-model="sliderValue"
|
<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"
|
||||||
|
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||||
:max="sliderMax"
|
|
||||||
label
|
:max="sliderMax"
|
||||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
label
|
||||||
color="primary"
|
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||||
/>
|
color="primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
@@ -76,7 +78,8 @@ export default vueComponent(SetPositionPage);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.slider {
|
.slider {
|
||||||
margin: 20px;
|
margin: 0 20px 0 20px;
|
||||||
|
height: 35px;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<div class="part-header">Показывать кнопки панели</div>
|
|
||||||
|
|
||||||
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
|
||||||
<div class="label-3"></div>
|
|
||||||
<div class="col row">
|
|
||||||
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -43,21 +43,10 @@
|
|||||||
|
|
||||||
<div class="item row">
|
<div class="item row">
|
||||||
<div class="label-6">Уведомление</div>
|
<div class="label-6">Уведомление</div>
|
||||||
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
|
<q-checkbox size="xs" v-model="showDonationDialog">
|
||||||
Показывать уведомление о новой версии
|
Показывать форму доната
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
<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-tooltip>
|
||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window ref="window" height="95%" width="600px" @close="close">
|
<Window ref="window" width="600px" @close="close">
|
||||||
<template #header>
|
<template #header>
|
||||||
Настройки
|
Настройки
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
|
<a ref="download" style="display: none;" target="_blank"></a>
|
||||||
|
|
||||||
<div class="full-height">
|
<div class="full-height">
|
||||||
<q-tabs
|
<q-tabs
|
||||||
ref="tabs"
|
ref="tabs"
|
||||||
@@ -24,10 +26,11 @@
|
|||||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
<div v-show="tabsScrollable" class="q-pt-lg" />
|
||||||
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
|
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
|
||||||
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
|
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
|
||||||
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
|
||||||
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
||||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
||||||
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||||
|
<q-tab class="tab" name="update" icon="la la-sync" label="Обновление" />
|
||||||
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
||||||
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
||||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
<div v-show="tabsScrollable" class="q-pt-lg" />
|
||||||
@@ -82,8 +85,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Кнопки ---------------------------------------------------------------------->
|
<!-- Кнопки ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
|
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
|
||||||
@@include('./ButtonsTab.inc');
|
@@include('./ToolBarTab.inc');
|
||||||
</div>
|
</div>
|
||||||
<!-- Управление ------------------------------------------------------------------>
|
<!-- Управление ------------------------------------------------------------------>
|
||||||
<div v-if="selectedTab == 'keys'" class="fit column">
|
<div v-if="selectedTab == 'keys'" class="fit column">
|
||||||
@@ -97,6 +100,10 @@
|
|||||||
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
||||||
@@include('./ConvertTab.inc');
|
@@include('./ConvertTab.inc');
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Обновление ------------------------------------------------------------------>
|
||||||
|
<div v-if="selectedTab == 'update'" class="fit tab-panel">
|
||||||
|
@@include('./UpdateTab.inc');
|
||||||
|
</div>
|
||||||
<!-- Прочее ---------------------------------------------------------------------->
|
<!-- Прочее ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
||||||
@@include('./OthersTab.inc');
|
@@include('./OthersTab.inc');
|
||||||
@@ -124,6 +131,7 @@ import NumInput from '../../share/NumInput.vue';
|
|||||||
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
||||||
import wallpaperStorage from '../share/wallpaperStorage';
|
import wallpaperStorage from '../share/wallpaperStorage';
|
||||||
|
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
import rstore from '../../../store/modules/reader';
|
import rstore from '../../../store/modules/reader';
|
||||||
import defPalette from './defPalette';
|
import defPalette from './defPalette';
|
||||||
|
|
||||||
@@ -310,6 +318,10 @@ class SettingsPage {
|
|||||||
return this.$store.state.reader.profiles;
|
return this.$store.state.reader.profiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get configBucEnabled() {
|
||||||
|
return this.$store.state.config.bucEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
get currentProfileOptions() {
|
get currentProfileOptions() {
|
||||||
const profNames = Object.keys(this.profiles)
|
const profNames = Object.keys(this.profiles)
|
||||||
profNames.sort();
|
profNames.sort();
|
||||||
@@ -636,8 +648,17 @@ class SettingsPage {
|
|||||||
|
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
newUserWallpapers.push({label, cssClass});
|
newUserWallpapers.push({label, cssClass});
|
||||||
if (!wallpaperStorage.keyExists(cssClass))
|
if (!wallpaperStorage.keyExists(cssClass)) {
|
||||||
await wallpaperStorage.setData(cssClass, data);
|
await wallpaperStorage.setData(cssClass, data);
|
||||||
|
//отправим data на сервер в файл `/upload/${key}`
|
||||||
|
try {
|
||||||
|
//const res =
|
||||||
|
await readerApi.uploadFileBuf(data);
|
||||||
|
//console.log(res);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.userWallpapers = newUserWallpapers;
|
this.userWallpapers = newUserWallpapers;
|
||||||
this.wallpaper = cssClass;
|
this.wallpaper = cssClass;
|
||||||
@@ -664,6 +685,27 @@ class SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadWallpaper() {
|
||||||
|
if (this.wallpaper.indexOf('user-paper') != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const d = this.$refs.download;
|
||||||
|
|
||||||
|
const dataUrl = await wallpaperStorage.getData(this.wallpaper);
|
||||||
|
|
||||||
|
if (!dataUrl)
|
||||||
|
throw new Error('Файл обоев не найден');
|
||||||
|
|
||||||
|
d.href = dataUrl;
|
||||||
|
d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
|
||||||
|
|
||||||
|
d.click();
|
||||||
|
} catch (e) {
|
||||||
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
@@ -702,11 +744,11 @@ export default vueComponent(SettingsPage);
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-1, .label-7 {
|
.label-1, .label-3, .label-7 {
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-2, .label-3, .label-4, .label-5 {
|
.label-2, .label-4, .label-5 {
|
||||||
width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="part-header">Отображение</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-3"></div>
|
||||||
|
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Скрывать/показывть панель при прокрутке текста вперед/назад
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="part-header">Показывать кнопки</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||||
|
<div class="label-3"></div>
|
||||||
|
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
76
client/components/Reader/SettingsPage/UpdateTab.inc
Normal file
76
client/components/Reader/SettingsPage/UpdateTab.inc
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Обновление читалки</div>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
|
||||||
|
Проверять наличие новой версии
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Напоминать о необходимости обновления страницы<br>
|
||||||
|
при появлении новой версии читалки
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">Обновление книг</div>
|
||||||
|
<div v-show="!configBucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<div>Сервер обновлений временно не работает</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<q-checkbox size="xs" v-model="bucEnabled">
|
||||||
|
Проверять обновления книг
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled && bucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<div class="col-5 column justify-center items-end q-pr-xs">Разница размеров</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="bucSizeDiff" />
|
||||||
|
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Уведомлять о наличии обновления книги в списке загруженных<br>
|
||||||
|
при указанной разнице в размерах старого и нового файлов.<br>
|
||||||
|
Разница указывается в байтах и может быть отрицательной.
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled && bucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<q-checkbox size="xs" v-model="bucSetOnNew">
|
||||||
|
Автопроверка для вновь загружаемых
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Автоматически устанавливать флаг проверки<br>
|
||||||
|
обновлений для всех вновь загружаемых книг
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled && bucEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<q-checkbox size="xs" v-model="bucCancelEnabled">
|
||||||
|
Отменять проверку через {{ bucCancelDays }} дней{{ (bucCancelEnabled ? ':' : '') }}
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Снимать флаг проверки с книги, если не было<br>
|
||||||
|
обновлений в течение {{ bucCancelDays }} дней
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="configBucEnabled && bucEnabled && bucCancelEnabled" class="item row">
|
||||||
|
<div class="label-6"></div>
|
||||||
|
<div class="col-5"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="bucCancelDays" :min="1" :max="10000"/>
|
||||||
|
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Снимать флаг проверки с книги, если не было<br>
|
||||||
|
обновлений в течение {{ bucCancelDays }} дней
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
class="q-ml-sm col"
|
class="q-ml-sm col"
|
||||||
outlined dense rounded
|
outlined dense
|
||||||
bg-color="grey-4"
|
bg-color="grey-4"
|
||||||
placeholder="Найти"
|
placeholder="Найти"
|
||||||
@click.stop
|
@click.stop
|
||||||
|
|||||||
@@ -102,6 +102,11 @@
|
|||||||
Удалить выбранные обои
|
Удалить выбранные обои
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
Скачать выбранные обои
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,29 +6,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||||
<div v-html="page1"></div>
|
<div @copy.prevent="copyText" v-html="page1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
||||||
<div v-html="page2"></div>
|
<div @copy.prevent="copyText" v-html="page2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showStatusBar" ref="statusBar" class="layout">
|
<div v-show="showStatusBar" ref="statusBar" class="layout">
|
||||||
<div v-html="statusBar"></div>
|
<div v-html="statusBar"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="clickControl" ref="layoutEvents" class="layout events"
|
<div
|
||||||
|
v-show="clickControl" ref="layoutEvents" class="layout events"
|
||||||
oncontextmenu="return false;"
|
oncontextmenu="return false;"
|
||||||
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||||
@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"
|
||||||
>
|
>
|
||||||
<div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
<div
|
||||||
|
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"
|
@click.prevent.stop="onStatusBarClick"
|
||||||
v-html="statusBarClickable"
|
v-html="statusBarClickable"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
<div
|
||||||
|
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
||||||
@mousedown.prevent.stop @touchstart.stop
|
@mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"
|
@click.prevent.stop="onStatusBarClick"
|
||||||
v-html="statusBarClickable"
|
v-html="statusBarClickable"
|
||||||
@@ -46,6 +49,7 @@ import vueComponent from '../../vueComponent.js';
|
|||||||
|
|
||||||
import {loadCSS} from 'fg-loadcss';
|
import {loadCSS} from 'fg-loadcss';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
import './TextPage.css';
|
import './TextPage.css';
|
||||||
|
|
||||||
@@ -62,7 +66,14 @@ const componentOptions = {
|
|||||||
watch: {
|
watch: {
|
||||||
bookPos: function() {
|
bookPos: function() {
|
||||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||||
|
|
||||||
this.draw();
|
this.draw();
|
||||||
|
|
||||||
|
if (this.userBookPosChange) {
|
||||||
|
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
|
||||||
|
this.prevBookPos = this.bookPos;
|
||||||
|
this.userBookPosChange = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bookPosSeen: function() {
|
bookPosSeen: function() {
|
||||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||||
@@ -95,6 +106,8 @@ class TextPage {
|
|||||||
lastBook = null;
|
lastBook = null;
|
||||||
bookPos = 0;
|
bookPos = 0;
|
||||||
bookPosSeen = null;
|
bookPosSeen = null;
|
||||||
|
prevBookPos = 0;
|
||||||
|
userBookPosChange = false;
|
||||||
|
|
||||||
fontStyle = null;
|
fontStyle = null;
|
||||||
fontSize = null;
|
fontSize = null;
|
||||||
@@ -151,7 +164,7 @@ class TextPage {
|
|||||||
|
|
||||||
this.$root.addEventHook('resize', async() => {
|
this.$root.addEventHook('resize', async() => {
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
await utils.sleep(500);
|
await utils.sleep(200);
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -495,12 +508,25 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onResize() {
|
async onResize() {
|
||||||
|
if (this.resizing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.resizing = true;
|
||||||
try {
|
try {
|
||||||
|
const scrolled = this.doingScrolling;
|
||||||
|
if (scrolled)
|
||||||
|
await this.stopTextScrolling();
|
||||||
|
|
||||||
this.calcDrawProps();
|
this.calcDrawProps();
|
||||||
this.setBackground();
|
this.setBackground();
|
||||||
this.draw();
|
this.draw();
|
||||||
|
|
||||||
|
if (scrolled)
|
||||||
|
this.startTextScrolling();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
|
} finally {
|
||||||
|
this.resizing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,7 +674,7 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
|
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
|
||||||
this.doEnd(true);
|
this.doEnd(true, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,7 +697,7 @@ class TextPage {
|
|||||||
this.debouncedDrawPageDividerAndOrnament();
|
this.debouncedDrawPageDividerAndOrnament();
|
||||||
|
|
||||||
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
||||||
this.doEnd(true);
|
this.doEnd(true, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,12 +933,14 @@ class TextPage {
|
|||||||
|
|
||||||
doDown() {
|
doDown() {
|
||||||
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
|
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesDown[1].begin;
|
this.bookPos = this.linesDown[1].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doUp() {
|
doUp() {
|
||||||
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
|
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesUp[1].begin;
|
this.bookPos = this.linesUp[1].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,6 +953,7 @@ class TextPage {
|
|||||||
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = true;
|
this.pageChangeDirectionDown = true;
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesDown[i].begin;
|
this.bookPos = this.linesDown[i].begin;
|
||||||
} else
|
} else
|
||||||
this.doEnd();
|
this.doEnd();
|
||||||
@@ -940,6 +969,7 @@ class TextPage {
|
|||||||
if (i >= 0 && this.linesUp.length > i) {
|
if (i >= 0 && this.linesUp.length > i) {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = false;
|
this.pageChangeDirectionDown = false;
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesUp[i].begin;
|
this.bookPos = this.linesUp[i].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -948,10 +978,11 @@ class TextPage {
|
|||||||
doHome() {
|
doHome() {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = false;
|
this.pageChangeDirectionDown = false;
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = 0;
|
this.bookPos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
doEnd(noAni) {
|
doEnd(noAni, isUser = true) {
|
||||||
if (this.parsed.para.length && this.pageLineCount > 0) {
|
if (this.parsed.para.length && this.pageLineCount > 0) {
|
||||||
let i = this.parsed.para.length - 1;
|
let i = this.parsed.para.length - 1;
|
||||||
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
|
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
|
||||||
@@ -962,6 +993,7 @@ class TextPage {
|
|||||||
if (!noAni)
|
if (!noAni)
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = true;
|
this.pageChangeDirectionDown = true;
|
||||||
|
this.userBookPosChange = isUser;
|
||||||
this.bookPos = lines[i].begin;
|
this.bookPos = lines[i].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1201,8 +1233,54 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyText(event) {
|
||||||
|
//все это для того, чтобы правильно расставить переносы \n при копировании текста
|
||||||
|
//прямо с текущей страницы
|
||||||
|
|
||||||
|
//подготовка, вытаскиваем весь текст страницы
|
||||||
|
const lines = this.getLines(this.bookPos);
|
||||||
|
const decodedLines = [];
|
||||||
|
for (const line of lines.linesDown) {
|
||||||
|
let lineText = '';
|
||||||
|
for (const part of line.parts) {
|
||||||
|
lineText += part.text;
|
||||||
|
}
|
||||||
|
decodedLines.push({text: he.decode(lineText), first: line.first});
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const findDecoded = (line) => {
|
||||||
|
for (let j = i; j < decodedLines.length; j++) {
|
||||||
|
const decoded = decodedLines[j];
|
||||||
|
if (decoded.text.indexOf(line) >= 0) {
|
||||||
|
i = j;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const splitted = selection.toString().split(/[\n\r]/);
|
||||||
|
|
||||||
|
let filtered = '';
|
||||||
|
//формируем filtered, учитывая переносы из decodedLines
|
||||||
|
for (const line of splitted) {
|
||||||
|
const found = findDecoded(line);
|
||||||
|
if (found && found.first) {
|
||||||
|
filtered += (filtered ? '\n' : '') + line;
|
||||||
|
} else {
|
||||||
|
filtered += (filtered ? '\r ' : '') + line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//маленькие хитрости, убираем переносы по слогам
|
||||||
|
filtered = filtered.replace(/-\r /g, '').replace(/\r /g, ' ');
|
||||||
|
|
||||||
|
event.clipboardData.setData('text/plain', filtered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vueComponent(TextPage);
|
export default vueComponent(TextPage);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
|
|||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxImageLineCount = 100;
|
const maxImageLineCount = 100;
|
||||||
|
const maxParaLength = 10000;
|
||||||
|
const maxParaTextLength = 10000;
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
@@ -83,6 +85,7 @@ export default class BookParser {
|
|||||||
let binaryId = '';
|
let binaryId = '';
|
||||||
let binaryType = '';
|
let binaryType = '';
|
||||||
let dimPromises = [];
|
let dimPromises = [];
|
||||||
|
this.coverPageId = '';
|
||||||
|
|
||||||
//оглавление
|
//оглавление
|
||||||
this.contents = [];
|
this.contents = [];
|
||||||
@@ -226,13 +229,26 @@ export default class BookParser {
|
|||||||
paraOffset += len;
|
paraOffset += len;
|
||||||
};
|
};
|
||||||
|
|
||||||
const growParagraph = (text, len) => {
|
const growParagraph = (text, len, textRaw) => {
|
||||||
|
//начальный параграф
|
||||||
if (paraIndex < 0) {
|
if (paraIndex < 0) {
|
||||||
newParagraph();
|
newParagraph();
|
||||||
growParagraph(text, len);
|
growParagraph(text, len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//ограничение на размер куска текста в параграфе
|
||||||
|
if (textRaw && textRaw.length > maxParaTextLength) {
|
||||||
|
while (textRaw.length > 0) {
|
||||||
|
const textPart = textRaw.substring(0, maxParaTextLength);
|
||||||
|
textRaw = textRaw.substring(maxParaTextLength);
|
||||||
|
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(textPart, textPart.length);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (inSubtitle) {
|
if (inSubtitle) {
|
||||||
curSubtitle.title += text;
|
curSubtitle.title += text;
|
||||||
} else if (inTitle) {
|
} else if (inTitle) {
|
||||||
@@ -240,6 +256,14 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const p = para[paraIndex];
|
const p = para[paraIndex];
|
||||||
|
|
||||||
|
//ограничение на размер параграфа
|
||||||
|
if (p.length > maxParaLength) {
|
||||||
|
newParagraph();
|
||||||
|
growParagraph(text, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
p.length += len;
|
p.length += len;
|
||||||
p.text += text;
|
p.text += text;
|
||||||
paraOffset += len;
|
paraOffset += len;
|
||||||
@@ -266,7 +290,7 @@ export default class BookParser {
|
|||||||
const href = attrs.href.value;
|
const href = attrs.href.value;
|
||||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||||
const {id, local} = this.imageHrefToId(href);
|
const {id, local} = this.imageHrefToId(href);
|
||||||
if (href[0] == '#') {//local
|
if (local) {//local
|
||||||
imageNum++;
|
imageNum++;
|
||||||
|
|
||||||
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||||
@@ -278,6 +302,11 @@ export default class BookParser {
|
|||||||
|
|
||||||
if (inPara && this.sets.showInlineImagesInCenter)
|
if (inPara && this.sets.showInlineImagesInCenter)
|
||||||
newParagraph();
|
newParagraph();
|
||||||
|
|
||||||
|
//coverpage
|
||||||
|
if (path == '/fictionbook/description/title-info/coverpage/image') {
|
||||||
|
this.coverPageId = id;
|
||||||
|
}
|
||||||
} else {//external
|
} else {//external
|
||||||
imageNum++;
|
imageNum++;
|
||||||
|
|
||||||
@@ -536,7 +565,7 @@ export default class BookParser {
|
|||||||
tClose += (center ? '</center>' : '');
|
tClose += (center ? '</center>' : '');
|
||||||
|
|
||||||
if (text != ' ')
|
if (text != ' ')
|
||||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
|
||||||
else
|
else
|
||||||
growParagraph(' ', 1);
|
growParagraph(' ', 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
|
import path from 'path-browserify';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
|
import coversStorage from './coversStorage';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxDataSize = 500*1024*1024;//compressed bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
|
const maxRecentLength = 5000;
|
||||||
|
|
||||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
@@ -17,9 +21,6 @@ const bmDataStore = localForage.createInstance({
|
|||||||
});
|
});
|
||||||
|
|
||||||
//список недавно открытых книг
|
//список недавно открытых книг
|
||||||
const bmRecentStoreOld = localForage.createInstance({
|
|
||||||
name: 'bmRecentStore'
|
|
||||||
});
|
|
||||||
const bmRecentStoreNew = localForage.createInstance({
|
const bmRecentStoreNew = localForage.createInstance({
|
||||||
name: 'bmRecentStoreNew'
|
name: 'bmRecentStoreNew'
|
||||||
});
|
});
|
||||||
@@ -39,7 +40,7 @@ class BookManager {
|
|||||||
|
|
||||||
this.saveRecentItem = _.debounce(() => {
|
this.saveRecentItem = _.debounce(() => {
|
||||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||||
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
|
||||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||||
}, 200, {maxWait: 300});
|
}, 200, {maxWait: 300});
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ class BookManager {
|
|||||||
if (this.recentItem)
|
if (this.recentItem)
|
||||||
this.recent[this.recentItem.key] = this.recentItem;
|
this.recent[this.recentItem.key] = this.recentItem;
|
||||||
|
|
||||||
|
//конвертируем в новые ключи
|
||||||
|
await this.convertRecent();
|
||||||
|
|
||||||
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||||
if (this.recentLastKey) {
|
if (this.recentLastKey) {
|
||||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||||
@@ -70,6 +74,40 @@ class BookManager {
|
|||||||
this.loadStored();//no await
|
this.loadStored();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: убрать в 2025г
|
||||||
|
async convertRecent() {
|
||||||
|
const converted = await bmRecentStoreNew.getItem('recent-converted');
|
||||||
|
|
||||||
|
if (converted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newRecent = {};
|
||||||
|
for (const book of Object.values(this.recent)) {
|
||||||
|
|
||||||
|
if (!book.path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKey = this.keyFromPath(book.path);
|
||||||
|
|
||||||
|
newRecent[newKey] = _.cloneDeep(book);
|
||||||
|
newRecent[newKey].key = newKey;
|
||||||
|
if (!newRecent[newKey].loadTime)
|
||||||
|
newRecent[newKey].loadTime = newRecent[newKey].addTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recent = newRecent;
|
||||||
|
|
||||||
|
//console.log(converted);
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
this.saveRecent();
|
||||||
|
this.emit('recent-changed');
|
||||||
|
this.emit('set-recent');
|
||||||
|
await bmRecentStoreNew.setItem('recent-converted', true);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
//Ленивая асинхронная загрузка bmMetaStore
|
//Ленивая асинхронная загрузка bmMetaStore
|
||||||
async loadStored() {
|
async loadStored() {
|
||||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||||
@@ -196,8 +234,12 @@ class BookManager {
|
|||||||
|
|
||||||
async addBook(newBook, callback) {
|
async addBook(newBook, callback) {
|
||||||
let meta = {url: newBook.url, path: newBook.path};
|
let meta = {url: newBook.url, path: newBook.path};
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
|
||||||
meta.addTime = Date.now();
|
if (newBook.downloadSize !== undefined && newBook.downloadSize >= 0)
|
||||||
|
meta.downloadSize = newBook.downloadSize;
|
||||||
|
|
||||||
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
meta.addTime = Date.now();//время добавления в кеш
|
||||||
|
|
||||||
const cb = (perc) => {
|
const cb = (perc) => {
|
||||||
const p = Math.round(30*perc/100);
|
const p = Math.round(30*perc/100);
|
||||||
@@ -232,10 +274,10 @@ class BookManager {
|
|||||||
async hasBookParsed(meta) {
|
async hasBookParsed(meta) {
|
||||||
if (!this.books)
|
if (!this.books)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.url)
|
if (!meta.path)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
|
||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
@@ -250,8 +292,12 @@ class BookManager {
|
|||||||
|
|
||||||
async getBook(meta, callback) {
|
async getBook(meta, callback) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
|
|
||||||
|
if (!meta.path)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
|
||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
@@ -261,11 +307,6 @@ class BookManager {
|
|||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
|
||||||
if (meta.path && result && meta.path != result.path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result && !result.parsed) {
|
if (result && !result.parsed) {
|
||||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||||
callback(5);
|
callback(5);
|
||||||
@@ -310,9 +351,38 @@ class BookManager {
|
|||||||
const parsed = new BookParser(this.settings);
|
const parsed = new BookParser(this.settings);
|
||||||
|
|
||||||
const parsedMeta = await parsed.parse(data, callback);
|
const parsedMeta = await parsed.parse(data, callback);
|
||||||
|
|
||||||
|
//cover page
|
||||||
|
let coverPageUrl = '';
|
||||||
|
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
|
||||||
|
const bin = parsed.binary[parsed.coverPageId];
|
||||||
|
let dataUrl = `data:${bin.type};base64,${bin.data}`;
|
||||||
|
try {
|
||||||
|
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
|
||||||
|
|
||||||
|
//далее асинхронно
|
||||||
|
(async() => {
|
||||||
|
//отправим dataUrl на сервер в /upload
|
||||||
|
try {
|
||||||
|
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохраним в storage
|
||||||
|
await coversStorage.setData(coverPageUrl, dataUrl);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
const result = Object.assign({}, meta, parsedMeta, {
|
const result = Object.assign({}, meta, parsedMeta, {
|
||||||
length: data.length,
|
length: data.length,
|
||||||
textLength: parsed.textLength,
|
textLength: parsed.textLength,
|
||||||
|
coverPageUrl,
|
||||||
parsed
|
parsed
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -325,10 +395,20 @@ class BookManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFromUrl(url) {
|
/*keyFromUrl(url) {
|
||||||
return utils.stringToHex(url);
|
return utils.stringToHex(url);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
keyFromPath(bookPath) {
|
||||||
|
return path.basename(bookPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keysEqual(bookPath1, bookPath2) {
|
||||||
|
if (bookPath1 === undefined || bookPath2 === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
|
||||||
|
}
|
||||||
//-- recent --------------------------------------------------------------
|
//-- recent --------------------------------------------------------------
|
||||||
async recentSetItem(item = null, skipCheck = false) {
|
async recentSetItem(item = null, skipCheck = false) {
|
||||||
const rev = await bmRecentStoreNew.getItem('rev');
|
const rev = await bmRecentStoreNew.getItem('rev');
|
||||||
@@ -369,7 +449,10 @@ class BookManager {
|
|||||||
|
|
||||||
async setRecentBook(value) {
|
async setRecentBook(value) {
|
||||||
let result = this.metaOnly(value);
|
let result = this.metaOnly(value);
|
||||||
result.touchTime = Date.now();
|
result.touchTime = Date.now();//время последнего чтения
|
||||||
|
if (!result.loadTime)
|
||||||
|
result.loadTime = Date.now();//время загрузки файла
|
||||||
|
|
||||||
result.deleted = 0;
|
result.deleted = 0;
|
||||||
|
|
||||||
if (this.recent[result.key]) {
|
if (this.recent[result.key]) {
|
||||||
@@ -385,9 +468,9 @@ class BookManager {
|
|||||||
return this.recent[value.key];
|
return this.recent[value.key];
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value, delFlag = 1) {
|
||||||
const item = this.recent[value.key];
|
const item = this.recent[value.key];
|
||||||
item.deleted = 1;
|
item.deleted = delFlag;
|
||||||
|
|
||||||
if (this.recentLastKey == value.key) {
|
if (this.recentLastKey == value.key) {
|
||||||
await this.recentSetLastKey(null);
|
await this.recentSetLastKey(null);
|
||||||
@@ -397,11 +480,43 @@ class BookManager {
|
|||||||
this.emit('recent-deleted', value.key);
|
this.emit('recent-deleted', value.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreRecentBook(value) {
|
||||||
|
const item = this.recent[value.key];
|
||||||
|
item.deleted = 0;
|
||||||
|
|
||||||
|
await this.recentSetItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCheckBuc(value, checkBuc) {
|
||||||
|
const item = this.recent[value.key];
|
||||||
|
|
||||||
|
const updateItems = [];
|
||||||
|
if (item) {
|
||||||
|
if (item.sameBookKey !== undefined) {
|
||||||
|
const sorted = this.getSortedRecent();
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (!book.deleted && book.sameBookKey === item.sameBookKey)
|
||||||
|
updateItems.push(book);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
for (const book of updateItems) {
|
||||||
|
book.checkBuc = checkBuc;
|
||||||
|
if (checkBuc)
|
||||||
|
book.checkBucTime = now;
|
||||||
|
await this.recentSetItem(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
const sorted = this.getSortedRecent();
|
const sorted = this.getSortedRecent();
|
||||||
|
|
||||||
let isDel = false;
|
let isDel = false;
|
||||||
for (let i = 1000; i < sorted.length; i++) {
|
for (let i = maxRecentLength; i < sorted.length; i++) {
|
||||||
delete this.recent[sorted[i].key];
|
delete this.recent[sorted[i].key];
|
||||||
isDel = true;
|
isDel = true;
|
||||||
}
|
}
|
||||||
@@ -421,7 +536,7 @@ class BookManager {
|
|||||||
|
|
||||||
let max = 0;
|
let max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
for (let key in this.recent) {
|
for (const key in this.recent) {
|
||||||
const book = this.recent[key];
|
const book = this.recent[key];
|
||||||
if (!book.deleted && book.touchTime > max) {
|
if (!book.deleted && book.touchTime > max) {
|
||||||
max = book.touchTime;
|
max = book.touchTime;
|
||||||
@@ -452,6 +567,43 @@ class BookManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findRecentByUrlAndPath(url, bookPath) {
|
||||||
|
if (bookPath) {
|
||||||
|
const key = this.keyFromPath(bookPath);
|
||||||
|
const book = this.recent[key];
|
||||||
|
if (book && !book.deleted)
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max = 0;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
for (const key in this.recent) {
|
||||||
|
const book = this.recent[key];
|
||||||
|
if (!book.deleted && book.url == url && book.loadTime > max) {
|
||||||
|
max = book.loadTime;
|
||||||
|
result = book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
findRecentBySameBookKey(sameKey) {
|
||||||
|
let max = 0;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
for (const key in this.recent) {
|
||||||
|
const book = this.recent[key];
|
||||||
|
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
|
||||||
|
max = book.loadTime;
|
||||||
|
result = book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async setRecent(value) {
|
async setRecent(value) {
|
||||||
const mergedRecent = _.cloneDeep(this.recent);
|
const mergedRecent = _.cloneDeep(this.recent);
|
||||||
|
|
||||||
|
|||||||
61
client/components/Reader/share/coversStorage.js
Normal file
61
client/components/Reader/share/coversStorage.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import localForage from 'localforage';
|
||||||
|
//import _ from 'lodash';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
|
const maxDataSize = 100*1024*1024;
|
||||||
|
|
||||||
|
const coversStore = localForage.createInstance({
|
||||||
|
name: 'coversStorage'
|
||||||
|
});
|
||||||
|
|
||||||
|
class CoversStorage {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.cleanCovers(); //no await
|
||||||
|
}
|
||||||
|
|
||||||
|
async setData(key, data) {
|
||||||
|
await coversStore.setItem(key, {addTime: Date.now(), data});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData(key) {
|
||||||
|
const item = await coversStore.getItem(key);
|
||||||
|
return (item ? item.data : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeData(key) {
|
||||||
|
await coversStore.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanCovers() {
|
||||||
|
await utils.sleep(10000);
|
||||||
|
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
let size = 0;
|
||||||
|
let min = Date.now();
|
||||||
|
let toDel = null;
|
||||||
|
for (const key of (await coversStore.keys())) {
|
||||||
|
const item = await coversStore.getItem(key);
|
||||||
|
|
||||||
|
size += item.data.length;
|
||||||
|
|
||||||
|
if (item.addTime < min) {
|
||||||
|
toDel = key;
|
||||||
|
min = item.addTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (size > maxDataSize && toDel) {
|
||||||
|
await this.removeData(toDel);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CoversStorage();
|
||||||
@@ -32,6 +32,10 @@ class WallpaperStorage {
|
|||||||
this.cachedKeys = await wpStore.keys();
|
this.cachedKeys = await wpStore.keys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getKeys() {
|
||||||
|
return await wpStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
keyExists(key) {//не асинхронная
|
keyExists(key) {//не асинхронная
|
||||||
return this.cachedKeys.includes(key);
|
return this.cachedKeys.includes(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,183 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
{
|
{
|
||||||
showUntil: '2022-01-10',
|
version: '0.12.2',
|
||||||
header: '0.11.2 (2022-01-11)',
|
releaseDate: '2022-09-04',
|
||||||
|
showUntil: '2022-09-11',
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>переход на JembaDb вместо SQLite</li>
|
<li>исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц</li>
|
||||||
|
<li>автор приносит извинения за доставленные неудобства</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.12.1',
|
||||||
|
releaseDate: '2022-09-01',
|
||||||
|
showUntil: '2022-08-30',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена форма для доната</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.12.0',
|
||||||
|
releaseDate: '2022-07-27',
|
||||||
|
showUntil: '2022-08-03',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>запущен сервер проверки обновлений книг:</li>
|
||||||
|
<ul>
|
||||||
|
<li>проверка обновления той или иной книги настраивается в списке загруженных (чекбокс)</li>
|
||||||
|
<li>для того, чтобы чекбокс появился у ранее загруженной, необходимо принудительно обновить книгу</li>
|
||||||
|
<li>в настройках можно указать разницу размеров, при которой требуется делать уведомление</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.8',
|
||||||
|
releaseDate: '2022-07-14',
|
||||||
|
showUntil: '2022-07-13',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
|
||||||
|
<li>добавлена синхронизация обоев</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.7',
|
||||||
|
releaseDate: '2022-07-12',
|
||||||
|
showUntil: '2022-07-19',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
|
||||||
|
<li>изменения в окне загруженных книг:</li>
|
||||||
|
<ul>
|
||||||
|
<li>добавлена группировка по версиям файла одной и той же книги</li>
|
||||||
|
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
|
||||||
|
<li>добавлены различные методы сортировки списка загруженных книг</li>
|
||||||
|
<li>нумерация всегда осуществляется по времени загрузки</li>
|
||||||
|
</ul>
|
||||||
|
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.6',
|
||||||
|
releaseDate: '2022-07-02',
|
||||||
|
showUntil: '2022-07-01',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
|
||||||
|
<li>актуализация используемых пакетов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.5',
|
||||||
|
releaseDate: '2022-04-15',
|
||||||
|
showUntil: '2022-04-14',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>небольшие дополнения интерфейса</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.1',
|
||||||
|
releaseDate: '2021-12-03',
|
||||||
showUntil: '2021-12-02',
|
showUntil: '2021-12-02',
|
||||||
header: '0.11.1 (2021-12-03)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>переход на JembaDb вместо SQLite</li>
|
<li>переход на JembaDb вместо SQLite</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.11.0',
|
||||||
|
releaseDate: '2021-11-18',
|
||||||
showUntil: '2021-11-17',
|
showUntil: '2021-11-17',
|
||||||
header: '0.11.0 (2021-11-18)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>переход на Vue 3</li>
|
<li>переход на Vue 3</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.3',
|
||||||
|
releaseDate: '2021-10-24',
|
||||||
showUntil: '2021-10-23',
|
showUntil: '2021-10-23',
|
||||||
header: '0.10.3 (2021-10-24)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.2',
|
||||||
|
releaseDate: '2021-10-19',
|
||||||
showUntil: '2021-10-18',
|
showUntil: '2021-10-18',
|
||||||
header: '0.10.2 (2021-10-19)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>актуализация версий пакетов и стека используемых технологий</li>
|
<li>актуализация версий пакетов и стека используемых технологий</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.1',
|
||||||
|
releaseDate: '2021-10-10',
|
||||||
showUntil: '2021-10-09',
|
showUntil: '2021-10-09',
|
||||||
header: '0.10.1 (2021-10-10)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.0',
|
||||||
|
releaseDate: '2021-02-09',
|
||||||
showUntil: '2021-02-16',
|
showUntil: '2021-02-16',
|
||||||
header: '0.10.0 (2021-02-09)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -76,12 +186,14 @@ export const versionHistory = [
|
|||||||
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||||
<li>немного улучшен парсинг fb2</li>
|
<li>немного улучшен парсинг fb2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.12',
|
||||||
|
releaseDate: '2020-12-18',
|
||||||
showUntil: '2020-12-17',
|
showUntil: '2020-12-17',
|
||||||
header: '0.9.12 (2020-12-18)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -90,23 +202,27 @@ export const versionHistory = [
|
|||||||
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||||
<li>улучшения работы конвертеров</li>
|
<li>улучшения работы конвертеров</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.11',
|
||||||
|
releaseDate: '2020-12-09',
|
||||||
showUntil: '2020-12-08',
|
showUntil: '2020-12-08',
|
||||||
header: '0.9.11 (2020-12-09)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>оптимизации, улучшения работы конвертеров</li>
|
<li>оптимизации, улучшения работы конвертеров</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.10',
|
||||||
|
releaseDate: '2020-12-03',
|
||||||
showUntil: '2020-12-10',
|
showUntil: '2020-12-10',
|
||||||
header: '0.9.10 (2020-12-03)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -114,69 +230,81 @@ export const versionHistory = [
|
|||||||
<li>добавлена поддержка Rar-архивов</li>
|
<li>добавлена поддержка Rar-архивов</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.9',
|
||||||
|
releaseDate: '2020-11-21',
|
||||||
showUntil: '2020-11-20',
|
showUntil: '2020-11-20',
|
||||||
header: '0.9.9 (2020-11-21)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>оптимизации, исправления багов</li>
|
<li>оптимизации, исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.8',
|
||||||
|
releaseDate: '2020-11-13',
|
||||||
showUntil: '2020-11-12',
|
showUntil: '2020-11-12',
|
||||||
header: '0.9.8 (2020-11-13)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлено окно "Оглавление/закладки"</li>
|
<li>добавлено окно "Оглавление/закладки"</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.7',
|
||||||
|
releaseDate: '2020-11-12',
|
||||||
showUntil: '2020-11-11',
|
showUntil: '2020-11-11',
|
||||||
header: '0.9.7 (2020-11-12)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.6',
|
||||||
|
releaseDate: '2020-11-06',
|
||||||
showUntil: '2020-11-05',
|
showUntil: '2020-11-05',
|
||||||
header: '0.9.6 (2020-11-06)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>завершена работа над новым окном "Библиотека"</li>
|
<li>завершена работа над новым окном "Библиотека"</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.5',
|
||||||
|
releaseDate: '2020-11-01',
|
||||||
showUntil: '2020-10-31',
|
showUntil: '2020-10-31',
|
||||||
header: '0.9.5 (2020-11-01)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.4',
|
||||||
|
releaseDate: '2020-10-29',
|
||||||
showUntil: '2020-10-28',
|
showUntil: '2020-10-28',
|
||||||
header: '0.9.4 (2020-10-29)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -184,23 +312,27 @@ export const versionHistory = [
|
|||||||
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.3',
|
||||||
|
releaseDate: '2020-05-21',
|
||||||
showUntil: '2020-05-20',
|
showUntil: '2020-05-20',
|
||||||
header: '0.9.3 (2020-05-21)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.2',
|
||||||
|
releaseDate: '2020-03-15',
|
||||||
showUntil: '2020-04-25',
|
showUntil: '2020-04-25',
|
||||||
header: '0.9.2 (2020-03-15)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -208,119 +340,139 @@ export const versionHistory = [
|
|||||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.1',
|
||||||
|
releaseDate: '2020-03-03',
|
||||||
showUntil: '2020-03-02',
|
showUntil: '2020-03-02',
|
||||||
header: '0.9.1 (2020-03-03)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>улучшение работы серверной части</li>
|
<li>улучшение работы серверной части</li>
|
||||||
<li>незначительные изменения интерфейса</li>
|
<li>незначительные изменения интерфейса</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.0',
|
||||||
|
releaseDate: '2020-02-26',
|
||||||
showUntil: '2020-02-25',
|
showUntil: '2020-02-25',
|
||||||
header: '0.9.0 (2020-02-26)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>переход на UI-фреймфорк Quasar</li>
|
<li>переход на UI-фреймфорк Quasar</li>
|
||||||
<li>незначительные изменения интерфейса</li>
|
<li>незначительные изменения интерфейса</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.4',
|
||||||
|
releaseDate: '2020-02-06',
|
||||||
showUntil: '2020-02-05',
|
showUntil: '2020-02-05',
|
||||||
header: '0.8.4 (2020-02-06)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлен paypal-адрес для пожертвований</li>
|
<li>добавлен paypal-адрес для пожертвований</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.3',
|
||||||
|
releaseDate: '2020-01-28',
|
||||||
showUntil: '2020-01-27',
|
showUntil: '2020-01-27',
|
||||||
header: '0.8.3 (2020-01-28)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||||
<li>внутренние оптимизации</li>
|
<li>внутренние оптимизации</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.2',
|
||||||
|
releaseDate: '2020-01-20',
|
||||||
showUntil: '2020-01-19',
|
showUntil: '2020-01-19',
|
||||||
header: '0.8.2 (2020-01-20)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>внутренние оптимизации</li>
|
<li>внутренние оптимизации</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.1',
|
||||||
|
releaseDate: '2020-01-07',
|
||||||
showUntil: '2020-01-06',
|
showUntil: '2020-01-06',
|
||||||
header: '0.8.1 (2020-01-07)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлена частичная поддержка формата FB3</li>
|
<li>добавлена частичная поддержка формата FB3</li>
|
||||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.0',
|
||||||
|
releaseDate: '2020-01-02',
|
||||||
showUntil: '2020-01-05',
|
showUntil: '2020-01-05',
|
||||||
header: '0.8.0 (2020-01-02)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>окончательный переход на https</li>
|
<li>окончательный переход на https</li>
|
||||||
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.9',
|
||||||
|
releaseDate: '2019-11-27',
|
||||||
showUntil: '2019-11-26',
|
showUntil: '2019-11-26',
|
||||||
header: '0.7.9 (2019-11-27)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.8',
|
||||||
|
releaseDate: '2019-11-25',
|
||||||
showUntil: '2019-11-24',
|
showUntil: '2019-11-24',
|
||||||
header: '0.7.8 (2019-11-25)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>улучшение html-фильтров для сайтов</li>
|
<li>улучшение html-фильтров для сайтов</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.7',
|
||||||
|
releaseDate: '2019-11-06',
|
||||||
showUntil: '2019-11-10',
|
showUntil: '2019-11-10',
|
||||||
header: '0.7.7 (2019-11-06)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -332,34 +484,40 @@ export const versionHistory = [
|
|||||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.6',
|
||||||
|
releaseDate: '2019-10-30',
|
||||||
showUntil: '2019-10-29',
|
showUntil: '2019-10-29',
|
||||||
header: '0.7.6 (2019-10-30)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.5',
|
||||||
|
releaseDate: '2019-10-22',
|
||||||
showUntil: '2019-10-21',
|
showUntil: '2019-10-21',
|
||||||
header: '0.7.5 (2019-10-22)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.3',
|
||||||
|
releaseDate: '2019-10-18',
|
||||||
showUntil: '2019-10-17',
|
showUntil: '2019-10-17',
|
||||||
header: '0.7.3 (2019-10-18)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -368,12 +526,14 @@ export const versionHistory = [
|
|||||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.1',
|
||||||
|
releaseDate: '2019-09-20',
|
||||||
showUntil: '2019-09-19',
|
showUntil: '2019-09-19',
|
||||||
header: '0.7.1 (2019-09-20)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -381,12 +541,14 @@ export const versionHistory = [
|
|||||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||||
<li>актуализирована справка</li>
|
<li>актуализирована справка</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.0',
|
||||||
|
releaseDate: '2019-09-07',
|
||||||
showUntil: '2019-10-01',
|
showUntil: '2019-10-01',
|
||||||
header: '0.7.0 (2019-09-07)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -397,23 +559,27 @@ export const versionHistory = [
|
|||||||
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||||
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.10',
|
||||||
|
releaseDate: '2019-07-21',
|
||||||
showUntil: '2019-07-20',
|
showUntil: '2019-07-20',
|
||||||
header: '0.6.10 (2019-07-21)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.9',
|
||||||
|
releaseDate: '2019-06-23',
|
||||||
showUntil: '2019-06-22',
|
showUntil: '2019-06-22',
|
||||||
header: '0.6.9 (2019-06-23)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -424,12 +590,14 @@ export const versionHistory = [
|
|||||||
<li>улучшены прогрессбары</li>
|
<li>улучшены прогрессбары</li>
|
||||||
<li>исправления недочетов, небольшие оптимизации</li>
|
<li>исправления недочетов, небольшие оптимизации</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.7',
|
||||||
|
releaseDate: '2019-05-30',
|
||||||
showUntil: '2019-06-05',
|
showUntil: '2019-06-05',
|
||||||
header: '0.6.7 (2019-05-30)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -442,36 +610,42 @@ export const versionHistory = [
|
|||||||
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||||
<li>исправления багов и недочетов</li>
|
<li>исправления багов и недочетов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.6',
|
||||||
|
releaseDate: '2019-03-29',
|
||||||
showUntil: '2019-03-29',
|
showUntil: '2019-03-29',
|
||||||
header: '0.6.6 (2019-03-29)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||||
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.4',
|
||||||
|
releaseDate: '2019-03-24',
|
||||||
showUntil: '2019-03-24',
|
showUntil: '2019-03-24',
|
||||||
header: '0.6.4 (2019-03-24)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов, оптимизации</li>
|
<li>исправления багов, оптимизации</li>
|
||||||
<li>добавлена возможность синхронизации данных между устройствами</li>
|
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.5.4',
|
||||||
|
releaseDate: '2019-03-04',
|
||||||
showUntil: '2019-03-04',
|
showUntil: '2019-03-04',
|
||||||
header: '0.5.4 (2019-03-04)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -480,12 +654,14 @@ export const versionHistory = [
|
|||||||
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||||
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.3.0',
|
||||||
|
releaseDate: '2019-02-17',
|
||||||
showUntil: '2019-02-17',
|
showUntil: '2019-02-17',
|
||||||
header: '0.3.0 (2019-02-17)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -493,12 +669,14 @@ export const versionHistory = [
|
|||||||
<li>улучшено распознавание текста</li>
|
<li>улучшено распознавание текста</li>
|
||||||
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.1.7',
|
||||||
|
releaseDate: '2019-02-14',
|
||||||
showUntil: '2019-02-14',
|
showUntil: '2019-02-14',
|
||||||
header: '0.1.7 (2019-02-14)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -508,17 +686,20 @@ export const versionHistory = [
|
|||||||
<li>добавлена возможность сброса настроек</li>
|
<li>добавлена возможность сброса настроек</li>
|
||||||
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.1.0',
|
||||||
|
releaseDate: '2019-02-12',
|
||||||
showUntil: '2019-02-12',
|
showUntil: '2019-02-12',
|
||||||
header: '0.1.0 (2019-02-12)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--------------------------------------------------->
|
||||||
|
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn v-close-popup flat round dense>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md">
|
||||||
|
<div v-html="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons row justify-end q-pa-md">
|
||||||
|
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||||
|
Нет
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||||
|
Да
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--------------------------------------------------->
|
<!--------------------------------------------------->
|
||||||
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
@@ -262,6 +290,23 @@ class StdDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
askYesNo(message, caption, opts) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.init(message, caption, opts);
|
||||||
|
|
||||||
|
this.hideTrigger = () => {
|
||||||
|
if (this.ok) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'askYesNo';
|
||||||
|
this.active = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
prompt(message, caption, opts) {
|
prompt(message, caption, opts) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.enableValidator = false;
|
this.enableValidator = false;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default vueComponent(Window);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(to bottom right, green, #59B04F);
|
background: linear-gradient(to bottom right, #007000, #59B04F);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
@@ -161,8 +161,8 @@ export default vueComponent(Window);
|
|||||||
.header-text {
|
.header-text {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: yellow;
|
color: #FFFFA0;
|
||||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,8 @@ export default vueComponent(Window);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.close-button:hover {
|
.close-button:hover {
|
||||||
background-color: #69C05F;
|
color: white;
|
||||||
|
background-color: #FF3030;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
|||||||
import {QDialog} from 'quasar/src/components/dialog';
|
import {QDialog} from 'quasar/src/components/dialog';
|
||||||
import {QChip} from 'quasar/src/components/chip';
|
import {QChip} from 'quasar/src/components/chip';
|
||||||
import {QTree} from 'quasar/src/components/tree';
|
import {QTree} from 'quasar/src/components/tree';
|
||||||
|
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
|
||||||
|
|
||||||
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
@@ -62,6 +64,7 @@ const components = {
|
|||||||
QChip,
|
QChip,
|
||||||
QTree,
|
QTree,
|
||||||
//QExpansionItem,
|
//QExpansionItem,
|
||||||
|
QVirtualScroll,
|
||||||
};
|
};
|
||||||
|
|
||||||
//directives
|
//directives
|
||||||
|
|||||||
53
client/share/LockQueue.js
Normal file
53
client/share/LockQueue.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
class LockQueue {
|
||||||
|
constructor(queueSize) {
|
||||||
|
this.queueSize = queueSize;
|
||||||
|
this.freed = true;
|
||||||
|
this.waitingQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//async
|
||||||
|
get(take = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.freed) {
|
||||||
|
if (take)
|
||||||
|
this.freed = false;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.waitingQueue.length < this.queueSize) {
|
||||||
|
this.waitingQueue.push({resolve, reject});
|
||||||
|
} else {
|
||||||
|
reject(new Error('Lock queue is too long'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ret() {
|
||||||
|
if (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().resolve();
|
||||||
|
} else {
|
||||||
|
this.freed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//async
|
||||||
|
wait() {
|
||||||
|
return this.get(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
retAll() {
|
||||||
|
while (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errAll(error = 'rejected') {
|
||||||
|
while (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().reject(new Error(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LockQueue;
|
||||||
@@ -45,6 +45,8 @@ export function formatDate(d, format) {
|
|||||||
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
||||||
case 'coDate':
|
case 'coDate':
|
||||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||||
|
case 'coMonth':
|
||||||
|
return `${(d.getMonth() + 1).toString().padStart(2, '0')}`;
|
||||||
case 'noDate':
|
case 'noDate':
|
||||||
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
|
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
|
||||||
}
|
}
|
||||||
@@ -90,7 +92,7 @@ export function toBase58(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fromBase58(data) {
|
export function fromBase58(data) {
|
||||||
return bs58.decode(data);
|
return Buffer.from(bs58.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
//base-x слишком тормозит, используем sjcl
|
//base-x слишком тормозит, используем sjcl
|
||||||
@@ -107,6 +109,10 @@ export function fromBase64(data) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasProp(obj, prop) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjDiff(oldObj, newObj, opts = {}) {
|
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||||
const {
|
const {
|
||||||
exclude = [],
|
exclude = [],
|
||||||
@@ -126,7 +132,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
|||||||
for (const key of Object.keys(oldObj)) {
|
for (const key of Object.keys(oldObj)) {
|
||||||
const kp = `${keyPath}${key}`;
|
const kp = `${keyPath}${key}`;
|
||||||
|
|
||||||
if (newObj.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||||
if (ex.has(kp))
|
if (ex.has(kp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -149,7 +155,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
|||||||
if (exAdd.has(kp))
|
if (exAdd.has(kp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!oldObj.hasOwnProperty(key)) {
|
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
|
||||||
result.add[key] = _.cloneDeep(newObj[key]);
|
result.add[key] = _.cloneDeep(newObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +219,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
|
|||||||
|
|
||||||
const change = diff.change;
|
const change = diff.change;
|
||||||
for (const key of Object.keys(change)) {
|
for (const key of Object.keys(change)) {
|
||||||
if (result.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||||
if (_.isObject(change[key])) {
|
if (_.isObject(change[key])) {
|
||||||
result[key] = applyObjDiff(result[key], change[key], opts);
|
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||||
} else {
|
} else {
|
||||||
@@ -359,4 +365,54 @@ export function getBookTitle(fb2) {
|
|||||||
]).join(' - ');
|
]).join(' - ');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
|
||||||
|
return new Promise ((resolve, reject) => { (async() => {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
let width = img.width;
|
||||||
|
let height = img.height;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
if (width > toWidth) {
|
||||||
|
height = height * (toWidth / width);
|
||||||
|
width = toWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > toHeight) {
|
||||||
|
width = width * (toHeight / height);
|
||||||
|
height = toHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
const result = canvas.toDataURL('image/jpeg', quality);
|
||||||
|
resolved = true;
|
||||||
|
resolve(result);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = reject;
|
||||||
|
|
||||||
|
img.src = dataUrl;
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
if (!resolved)
|
||||||
|
reject('Не удалось изменить размер');
|
||||||
|
})().catch(reject); });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeDonation() {
|
||||||
|
window.open('https://donatty.com/liberama', '_blank');
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
import createPersistedState from 'vuex-persistedstate';
|
//import createPersistedState from 'vuex-persistedstate';
|
||||||
|
import VuexPersistence from 'vuex-persist';
|
||||||
|
|
||||||
import root from './root.js';
|
import root from './root.js';
|
||||||
import uistate from './modules/uistate';
|
import uistate from './modules/uistate';
|
||||||
@@ -8,6 +9,8 @@ import reader from './modules/reader';
|
|||||||
|
|
||||||
const debug = process.env.NODE_ENV !== 'production';
|
const debug = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
const vuexLocal = new VuexPersistence();
|
||||||
|
|
||||||
export default createStore(Object.assign({}, root, {
|
export default createStore(Object.assign({}, root, {
|
||||||
modules: {
|
modules: {
|
||||||
uistate,
|
uistate,
|
||||||
@@ -15,5 +18,5 @@ export default createStore(Object.assign({}, root, {
|
|||||||
reader,
|
reader,
|
||||||
},
|
},
|
||||||
strict: debug,
|
strict: debug,
|
||||||
plugins: [createPersistedState()]
|
plugins: [vuexLocal.plugin]
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import * as utils from '../../share/utils';
|
|||||||
import googleFonts from './fonts/fonts.json';
|
import googleFonts from './fonts/fonts.json';
|
||||||
|
|
||||||
const readerActions = {
|
const readerActions = {
|
||||||
'help': 'Вызвать cправку',
|
|
||||||
'loader': 'На страницу загрузки',
|
'loader': 'На страницу загрузки',
|
||||||
|
'loadFile': 'Загрузить файл с диска',
|
||||||
|
'loadBuffer': 'Загрузить из буфера обмена',
|
||||||
|
'help': 'Вызвать cправку',
|
||||||
'settings': 'Настроить',
|
'settings': 'Настроить',
|
||||||
'undoAction': 'Действие назад',
|
'undoAction': 'Действие назад',
|
||||||
'redoAction': 'Действие вперед',
|
'redoAction': 'Действие вперед',
|
||||||
@@ -15,10 +17,11 @@ const readerActions = {
|
|||||||
'copyText': 'Скопировать текст со страницы',
|
'copyText': 'Скопировать текст со страницы',
|
||||||
'convOptions': 'Настроить конвертирование',
|
'convOptions': 'Настроить конвертирование',
|
||||||
'refresh': 'Принудительно обновить книгу',
|
'refresh': 'Принудительно обновить книгу',
|
||||||
|
'clickControl': 'Управление кликом',
|
||||||
'offlineMode': 'Автономный режим (без интернета)',
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
'contents': 'Оглавление/закладки',
|
'contents': 'Оглавление/закладки',
|
||||||
'libs': 'Сетевая библиотека',
|
'libs': 'Сетевая библиотека',
|
||||||
'recentBooks': 'Открыть недавние',
|
'recentBooks': 'Показать загруженные',
|
||||||
'switchToolbar': 'Показать/скрыть панель управления',
|
'switchToolbar': 'Показать/скрыть панель управления',
|
||||||
'donate': '',
|
'donate': '',
|
||||||
'bookBegin': 'В начало книги',
|
'bookBegin': 'В начало книги',
|
||||||
@@ -35,6 +38,9 @@ const readerActions = {
|
|||||||
|
|
||||||
//readerActions[name]
|
//readerActions[name]
|
||||||
const toolButtons = [
|
const toolButtons = [
|
||||||
|
{name: 'loadFile', show: true},
|
||||||
|
{name: 'loadBuffer', show: true},
|
||||||
|
{name: 'help', show: true},
|
||||||
{name: 'undoAction', show: true},
|
{name: 'undoAction', show: true},
|
||||||
{name: 'redoAction', show: true},
|
{name: 'redoAction', show: true},
|
||||||
{name: 'fullScreen', show: true},
|
{name: 'fullScreen', show: true},
|
||||||
@@ -47,13 +53,16 @@ const toolButtons = [
|
|||||||
{name: 'contents', show: true},
|
{name: 'contents', show: true},
|
||||||
{name: 'libs', show: true},
|
{name: 'libs', show: true},
|
||||||
{name: 'recentBooks', show: true},
|
{name: 'recentBooks', show: true},
|
||||||
|
{name: 'clickControl', show: false},
|
||||||
{name: 'offlineMode', show: false},
|
{name: 'offlineMode', show: false},
|
||||||
];
|
];
|
||||||
|
|
||||||
//readerActions[name]
|
//readerActions[name]
|
||||||
const hotKeys = [
|
const hotKeys = [
|
||||||
{name: 'help', codes: ['F1', 'H']},
|
|
||||||
{name: 'loader', codes: ['Escape']},
|
{name: 'loader', codes: ['Escape']},
|
||||||
|
{name: 'loadFile', codes: ['F3']},
|
||||||
|
{name: 'loadBuffer', codes: ['F4']},
|
||||||
|
{name: 'help', codes: ['F1', 'H']},
|
||||||
{name: 'settings', codes: ['S']},
|
{name: 'settings', codes: ['S']},
|
||||||
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||||
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||||
@@ -61,12 +70,13 @@ const hotKeys = [
|
|||||||
{name: 'scrolling', codes: ['Z']},
|
{name: 'scrolling', codes: ['Z']},
|
||||||
{name: 'setPosition', codes: ['P']},
|
{name: 'setPosition', codes: ['P']},
|
||||||
{name: 'search', codes: ['Ctrl+F']},
|
{name: 'search', codes: ['Ctrl+F']},
|
||||||
{name: 'copyText', codes: ['Ctrl+C']},
|
{name: 'copyText', codes: ['Ctrl+Space']},
|
||||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||||
{name: 'refresh', codes: ['R']},
|
{name: 'refresh', codes: ['R']},
|
||||||
{name: 'contents', codes: ['C']},
|
{name: 'contents', codes: ['C']},
|
||||||
{name: 'libs', codes: ['L']},
|
{name: 'libs', codes: ['L']},
|
||||||
{name: 'recentBooks', codes: ['X']},
|
{name: 'recentBooks', codes: ['X']},
|
||||||
|
{name: 'clickControl', codes: ['Ctrl+B']},
|
||||||
{name: 'offlineMode', codes: ['O']},
|
{name: 'offlineMode', codes: ['O']},
|
||||||
|
|
||||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||||
@@ -170,13 +180,28 @@ const settingDefaults = {
|
|||||||
|
|
||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
showDonationDialog2020: true,
|
showDonationDialog: true,
|
||||||
showNeedUpdateNotify: true,
|
showNeedUpdateNotify: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
|
toolBarHideOnScroll: false,
|
||||||
userHotKeys: {},
|
userHotKeys: {},
|
||||||
userWallpapers: [],
|
userWallpapers: [],
|
||||||
|
|
||||||
|
recentShowSameBook: false,
|
||||||
|
recentSortMethod: '',
|
||||||
|
|
||||||
|
//Book Update Checker
|
||||||
|
bucEnabled: true, // общее включение/выключение проверки обновлений
|
||||||
|
bucSizeDiff: 1, // разница в размерах файла, при которой показывать наличие обновления
|
||||||
|
bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
|
||||||
|
bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
|
||||||
|
bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
|
||||||
|
|
||||||
|
//для SettingsPage
|
||||||
|
needUpdateSettingsView: 0,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
@@ -212,9 +237,6 @@ const libsDefaults = {
|
|||||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||||
]},
|
]},
|
||||||
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
|
||||||
{l: 'https://flibs.in', c: 'Flibs'},
|
|
||||||
]},
|
|
||||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||||
]},
|
]},
|
||||||
@@ -233,6 +255,7 @@ const libsDefaults = {
|
|||||||
// initial state
|
// initial state
|
||||||
const state = {
|
const state = {
|
||||||
toolBarActive: true,
|
toolBarActive: true,
|
||||||
|
offlineModeActive: false,
|
||||||
serverSyncEnabled: false,
|
serverSyncEnabled: false,
|
||||||
serverStorageKey: '',
|
serverStorageKey: '',
|
||||||
profiles: {},
|
profiles: {},
|
||||||
@@ -258,6 +281,9 @@ const mutations = {
|
|||||||
setToolBarActive(state, value) {
|
setToolBarActive(state, value) {
|
||||||
state.toolBarActive = value;
|
state.toolBarActive = value;
|
||||||
},
|
},
|
||||||
|
setOfflineModeActive(state, value) {
|
||||||
|
state.offlineModeActive = value;
|
||||||
|
},
|
||||||
setServerSyncEnabled(state, value) {
|
setServerSyncEnabled(state, value) {
|
||||||
state.serverSyncEnabled = value;
|
state.serverSyncEnabled = value;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name beta.liberama.top;
|
server_name beta.liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:34082;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -15,15 +16,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -32,6 +38,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
@@ -50,6 +61,7 @@ server {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name b.beta.liberama.top;
|
server_name b.beta.liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:34082;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -59,15 +71,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34082;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -76,6 +93,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name beta.omnireader.ru;
|
server_name beta.omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:34081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -15,15 +16,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -32,6 +38,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name beta.omnireader.ru;
|
server_name beta.omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:34081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -10,15 +11,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:34081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -27,6 +33,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name liberama.top;
|
server_name liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:55081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -26,12 +27,16 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -44,6 +49,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
@@ -62,8 +72,9 @@ server {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name b.liberama.top;
|
server_name b.liberama.top;
|
||||||
|
set $liberama http://127.0.0.1:55081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -71,15 +82,20 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:55081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -88,6 +104,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
@@ -140,5 +161,6 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://fantasy-worlds.org;
|
proxy_pass http://fantasy-worlds.org;
|
||||||
|
proxy_hide_header x-frame-options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ git clone https://github.com/bookpauk/liberama
|
|||||||
### node.js
|
### node.js
|
||||||
```
|
```
|
||||||
sudo apt install -y curl
|
sudo apt install -y curl
|
||||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||||
sudo apt install -y nodejs
|
sudo apt install -y nodejs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if ! pgrep -x "liberama" > /dev/null ; then
|
if ! pgrep -x "liberama" > /dev/null ; then
|
||||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null"
|
||||||
else
|
else
|
||||||
echo "Process 'liberama' already running"
|
echo "Process 'liberama' already running"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ server {
|
|||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:44081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -15,12 +16,16 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -33,6 +38,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
@@ -52,7 +62,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name old.omnireader.ru;
|
server_name old.omnireader.ru;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
set $liberama http://127.0.0.1:44081;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
@@ -10,12 +11,16 @@ server {
|
|||||||
gzip_proxied expired no-cache no-store private auth;
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
gzip_types *;
|
gzip_types *;
|
||||||
|
|
||||||
|
location @liberama {
|
||||||
|
proxy_pass $liberama;
|
||||||
|
}
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://127.0.0.1:44081;
|
proxy_pass $liberama;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -27,6 +32,11 @@ server {
|
|||||||
location /tmp {
|
location /tmp {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
types { } default_type "application/xml; charset=utf-8";
|
||||||
add_header Content-Encoding gzip;
|
add_header Content-Encoding gzip;
|
||||||
|
try_files $uri @liberama;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /upload {
|
||||||
|
try_files $uri @liberama;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null & disown"
|
||||||
sudo service cron start
|
sudo service cron start
|
||||||
|
|||||||
12984
package-lock.json
generated
12984
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
86
package.json
86
package.json
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.11.2",
|
"version": "0.12.2",
|
||||||
"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",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.4.0"
|
"node": ">=16.16.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||||
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -C GZip -o dist/linux/liberama .",
|
"build:linux": "npm run build:client && node build/linux && pkg -t node16-linux-x64 -C GZip -o dist/linux/liberama .",
|
||||||
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -C GZip -o dist/win/liberama .",
|
"build:win": "npm run build:client && node build/win && pkg -t node16-win-x64 -C GZip -o dist/win/liberama .",
|
||||||
"lint": "eslint --ext=.js,.vue client server",
|
"lint": "eslint --ext=.js,.vue client server",
|
||||||
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
||||||
"postinstall": "npm run build:client-dev && node build/linux"
|
"postinstall": "npm run build:client-dev && node build/linux"
|
||||||
@@ -21,68 +21,64 @@
|
|||||||
"scripts": "server/config/*.js"
|
"scripts": "server/config/*.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.18.13",
|
||||||
"@babel/eslint-parser": "^7.16.3",
|
"@babel/eslint-parser": "^7.18.9",
|
||||||
"@babel/eslint-plugin": "^7.14.5",
|
"@babel/eslint-plugin": "^7.18.10",
|
||||||
"@babel/plugin-proposal-decorators": "^7.16.0",
|
"@babel/plugin-proposal-decorators": "^7.18.10",
|
||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.18.10",
|
||||||
"@vue/compiler-sfc": "^3.2.22",
|
"@vue/compiler-sfc": "^3.2.22",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.5",
|
||||||
"copy-webpack-plugin": "^9.1.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.5.1",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.1.3",
|
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.23.0",
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^9.4.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"mini-css-extract-plugin": "^2.4.4",
|
"mini-css-extract-plugin": "^2.6.1",
|
||||||
"pkg": "^5.5.1",
|
"pkg": "^5.8.0",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"vue-eslint-parser": "^8.0.1",
|
"vue-eslint-parser": "^9.0.3",
|
||||||
"vue-loader": "^16.8.3",
|
"vue-loader": "^17.0.0",
|
||||||
"vue-style-loader": "^4.1.3",
|
"vue-style-loader": "^4.1.3",
|
||||||
"webpack": "^5.64.1",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack-cli": "^4.10.0",
|
||||||
"webpack-dev-middleware": "^5.2.1",
|
"webpack-dev-middleware": "^5.3.3",
|
||||||
"webpack-hot-middleware": "^2.25.1",
|
"webpack-hot-middleware": "^2.25.2",
|
||||||
"webpack-merge": "^5.8.0",
|
"webpack-merge": "^5.8.0",
|
||||||
"workbox-webpack-plugin": "^6.4.1"
|
"workbox-webpack-plugin": "^6.5.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.12.0",
|
"@quasar/extras": "^1.15.2",
|
||||||
"@vue/compat": "^3.2.21",
|
"@vue/compat": "^3.2.38",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.27.2",
|
||||||
"base-x": "^3.0.9",
|
"base-x": "^4.0.0",
|
||||||
"chardet": "^1.4.0",
|
"chardet": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.18.1",
|
||||||
"fg-loadcss": "^3.1.0",
|
"fg-loadcss": "^3.1.0",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"got": "^11.8.2",
|
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jembadb": "^1.3.0",
|
"jembadb": "^4.2.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.6",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.5-lts.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pidusage": "^3.0.0",
|
"pidusage": "^3.0.0",
|
||||||
"quasar": "^2.3.2",
|
"quasar": "^2.7.7",
|
||||||
"safe-buffer": "^5.2.1",
|
"safe-buffer": "^5.2.1",
|
||||||
"sanitize-html": "^2.5.3",
|
"sanitize-html": "^2.7.1",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"sql-template-strings": "^2.2.2",
|
|
||||||
"sqlite": "^4.0.23",
|
|
||||||
"sqlite3": "^5.0.2",
|
|
||||||
"tar-fs": "^2.1.1",
|
"tar-fs": "^2.1.1",
|
||||||
"unbzip2-stream": "^1.4.3",
|
"unbzip2-stream": "^1.4.3",
|
||||||
"vue": "^3.2.22",
|
"vue": "^3.2.37",
|
||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.1.5",
|
||||||
"vuex": "^4.0.2",
|
"vuex": "^4.0.2",
|
||||||
"vuex-persistedstate": "^4.1.0",
|
"vuex-persist": "^3.1.3",
|
||||||
"webdav": "^4.7.0",
|
"webdav": "^4.11.0",
|
||||||
"ws": "^8.2.3",
|
"ws": "^8.8.1",
|
||||||
"zip-stream": "^4.1.0"
|
"zip-stream": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,46 +22,62 @@ module.exports = {
|
|||||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||||
|
|
||||||
useExternalBookConverter: false,
|
useExternalBookConverter: false,
|
||||||
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
|
acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png',
|
||||||
|
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch'],
|
||||||
db: [
|
|
||||||
{
|
|
||||||
poolName: 'app',
|
|
||||||
connCount: 20,
|
|
||||||
fileName: 'app.sqlite',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
poolName: 'readerStorage',
|
|
||||||
connCount: 20,
|
|
||||||
fileName: 'reader-storage.sqlite',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
jembaDb: [
|
jembaDb: [
|
||||||
{
|
{
|
||||||
|
serverMode: ['reader', 'omnireader', 'liberama.top'],
|
||||||
|
dbName: 'app',
|
||||||
|
thread: true,
|
||||||
|
openAll: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serverMode: ['reader', 'omnireader', 'liberama.top'],
|
||||||
dbName: 'reader-storage',
|
dbName: 'reader-storage',
|
||||||
thread: true,
|
thread: true,
|
||||||
openAll: true,
|
openAll: true,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
serverMode: 'book_update_checker',
|
||||||
|
dbName: 'book-update-server',
|
||||||
|
thread: true,
|
||||||
|
openAll: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
serverName: '1',
|
serverName: '1',
|
||||||
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader'
|
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: '33080',
|
port: '33080',
|
||||||
},
|
},
|
||||||
|
/*{
|
||||||
|
serverName: '2',
|
||||||
|
mode: 'book_update_checker', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
|
||||||
|
isHttps: true,
|
||||||
|
keysFile: 'server',
|
||||||
|
ip: '0.0.0.0',
|
||||||
|
port: '33443',
|
||||||
|
accessToken: '',
|
||||||
|
}*/
|
||||||
],
|
],
|
||||||
|
|
||||||
remoteWebDavStorage: false,
|
remoteStorage: false,
|
||||||
/*
|
/*
|
||||||
remoteWebDavStorage: {
|
remoteStorage: {
|
||||||
url: '127.0.0.1:1900',
|
url: 'wss://127.0.0.1:11900',
|
||||||
username: '',
|
accessToken: '',
|
||||||
password: '',
|
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
bucEnabled: false,
|
||||||
|
bucServer: false,
|
||||||
|
/*
|
||||||
|
bucServer: {
|
||||||
|
url: 'wss://127.0.0.1:33443',
|
||||||
|
accessToken: '',
|
||||||
|
}
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const propsToSave = [
|
|||||||
'useExternalBookConverter',
|
'useExternalBookConverter',
|
||||||
|
|
||||||
'servers',
|
'servers',
|
||||||
'remoteWebDavStorage',
|
'remoteStorage',
|
||||||
|
'bucEnabled',
|
||||||
|
'bucServer',
|
||||||
];
|
];
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|||||||
126
server/controllers/BookUpdateCheckerController.js
Normal file
126
server/controllers/BookUpdateCheckerController.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
//const _ = require('lodash');
|
||||||
|
|
||||||
|
const BUCServer = require('../core/BookUpdateChecker/BUCServer');
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
|
//const utils = require('../core/utils');
|
||||||
|
|
||||||
|
const cleanPeriod = 1*60*1000;//1 минута
|
||||||
|
const closeSocketOnIdle = 5*60*1000;//5 минут
|
||||||
|
|
||||||
|
class BookUpdateCheckerController {
|
||||||
|
constructor(wss, config) {
|
||||||
|
this.config = config;
|
||||||
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
|
this.accessToken = config.accessToken;
|
||||||
|
this.bucServer = new BUCServer(config);
|
||||||
|
|
||||||
|
this.wss = wss;
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
this.onMessage(ws, message.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
log(LM_ERR, err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
periodicClean() {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.wss.clients.forEach((ws) => {
|
||||||
|
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
|
||||||
|
ws.terminate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(ws, message) {
|
||||||
|
let req = {};
|
||||||
|
try {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`BUC-WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
req = JSON.parse(message);
|
||||||
|
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
|
||||||
|
//pong for WebSocketConnection
|
||||||
|
this.send({_rok: 1}, req, ws);
|
||||||
|
|
||||||
|
if (req.accessToken !== this.accessToken)
|
||||||
|
throw new Error('Access denied');
|
||||||
|
|
||||||
|
switch (req.action) {
|
||||||
|
case 'test':
|
||||||
|
await this.test(req, ws); break;
|
||||||
|
case 'get-buc':
|
||||||
|
await this.getBuc(req, ws); break;
|
||||||
|
case 'update-buc':
|
||||||
|
await this.updateBuc(req, ws); break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.send({error: e.message}, req, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(res, req, ws) {
|
||||||
|
if (ws.readyState == WebSocket.OPEN) {
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
let r = res;
|
||||||
|
if (req.requestId)
|
||||||
|
r = Object.assign({requestId: req.requestId}, r);
|
||||||
|
|
||||||
|
const message = JSON.stringify(r);
|
||||||
|
ws.send(message);
|
||||||
|
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
log(`BUC-WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Actions ------------------------------------------------------------------
|
||||||
|
async test(req, ws) {
|
||||||
|
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBuc(req, ws) {
|
||||||
|
if (!req.fromCheckTime)
|
||||||
|
throw new Error(`key 'fromCheckTime' is empty`);
|
||||||
|
|
||||||
|
await this.bucServer.getBuc(req.fromCheckTime, (rows) => {
|
||||||
|
this.send({state: 'get', rows}, req, ws);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.send({state: 'finish'}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBuc(req, ws) {
|
||||||
|
if (!req.bookUrls)
|
||||||
|
throw new Error(`key 'bookUrls' is empty`);
|
||||||
|
|
||||||
|
if (!Array.isArray(req.bookUrls))
|
||||||
|
throw new Error(`key 'bookUrls' must be array`);
|
||||||
|
|
||||||
|
await this.bucServer.updateBuc(req.bookUrls);
|
||||||
|
|
||||||
|
this.send({state: 'success'}, req, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BookUpdateCheckerController;
|
||||||
@@ -68,24 +68,6 @@ class ReaderController extends BaseController {
|
|||||||
res.status(400).send({error});
|
res.status(400).send({error});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreCachedFile(req, res) {
|
|
||||||
const request = req.body;
|
|
||||||
let error = '';
|
|
||||||
try {
|
|
||||||
if (!request.path)
|
|
||||||
throw new Error(`key 'path' is empty`);
|
|
||||||
|
|
||||||
const workerId = this.readerWorker.restoreCachedFile(request.path);
|
|
||||||
const state = this.workerState.getState(workerId);
|
|
||||||
return (state ? state : {});
|
|
||||||
} catch (e) {
|
|
||||||
error = e.message;
|
|
||||||
}
|
|
||||||
//bad request
|
|
||||||
res.status(400).send({error});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ReaderController;
|
module.exports = ReaderController;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const _ = require('lodash');
|
|||||||
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
||||||
const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
|
const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
|
||||||
const WorkerState = require('../core/WorkerState');//singleton
|
const WorkerState = require('../core/WorkerState');//singleton
|
||||||
|
const BUCClient = require('../core/BookUpdateChecker/BUCClient');//singleton
|
||||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
const utils = require('../core/utils');
|
const utils = require('../core/utils');
|
||||||
|
|
||||||
@@ -19,12 +20,20 @@ class WebSocketController {
|
|||||||
this.readerWorker = new ReaderWorker(config);
|
this.readerWorker = new ReaderWorker(config);
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
|
|
||||||
|
if (config.bucEnabled) {
|
||||||
|
this.bucClient = new BUCClient(config);
|
||||||
|
}
|
||||||
|
|
||||||
this.wss = wss;
|
this.wss = wss;
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
ws.on('message', (message) => {
|
ws.on('message', (message) => {
|
||||||
this.onMessage(ws, message.toString());
|
this.onMessage(ws, message.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ws.on('error', (err) => {
|
||||||
|
log(LM_ERR, err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||||
@@ -55,8 +64,7 @@ class WebSocketController {
|
|||||||
ws.lastActivity = Date.now();
|
ws.lastActivity = Date.now();
|
||||||
|
|
||||||
//pong for WebSocketConnection
|
//pong for WebSocketConnection
|
||||||
if (req._rpo === 1)
|
this.send({_rok: 1}, req, ws);
|
||||||
this.send({_rok: 1}, req, ws);
|
|
||||||
|
|
||||||
switch (req.action) {
|
switch (req.action) {
|
||||||
case 'test':
|
case 'test':
|
||||||
@@ -67,10 +75,14 @@ class WebSocketController {
|
|||||||
await this.workerGetState(req, ws); break;
|
await this.workerGetState(req, ws); break;
|
||||||
case 'worker-get-state-finish':
|
case 'worker-get-state-finish':
|
||||||
await 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':
|
case 'reader-storage':
|
||||||
await this.readerStorageDo(req, ws); break;
|
await this.readerStorageDo(req, ws); break;
|
||||||
|
case 'upload-file-buf':
|
||||||
|
await this.uploadFileBuf(req, ws); break;
|
||||||
|
case 'upload-file-touch':
|
||||||
|
await this.uploadFileTouch(req, ws); break;
|
||||||
|
case 'check-buc':
|
||||||
|
await this.checkBuc(req, ws); break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Action not found: ${req.action}`);
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
@@ -150,15 +162,6 @@ 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) {
|
async readerStorageDo(req, ws) {
|
||||||
if (!req.body)
|
if (!req.body)
|
||||||
throw new Error(`key 'body' is empty`);
|
throw new Error(`key 'body' is empty`);
|
||||||
@@ -169,6 +172,35 @@ class WebSocketController {
|
|||||||
|
|
||||||
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadFileBuf(req, ws) {
|
||||||
|
if (!req.buf)
|
||||||
|
throw new Error(`key 'buf' is empty`);
|
||||||
|
|
||||||
|
this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileTouch(req, ws) {
|
||||||
|
if (!req.url)
|
||||||
|
throw new Error(`key 'url' is empty`);
|
||||||
|
|
||||||
|
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBuc(req, ws) {
|
||||||
|
if (!this.config.bucEnabled)
|
||||||
|
throw new Error('BookUpdateChecker disabled');
|
||||||
|
|
||||||
|
if (!req.bookUrls)
|
||||||
|
throw new Error(`key 'bookUrls' is empty`);
|
||||||
|
|
||||||
|
if (!Array.isArray(req.bookUrls))
|
||||||
|
throw new Error(`key 'bookUrls' must be array`);
|
||||||
|
|
||||||
|
const data = await this.bucClient.checkBuc(req.bookUrls);
|
||||||
|
|
||||||
|
this.send({state: 'success', data}, req, ws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WebSocketController;
|
module.exports = WebSocketController;
|
||||||
|
|||||||
@@ -25,60 +25,7 @@ class WorkerController extends BaseController {
|
|||||||
res.status(400).send({error});
|
res.status(400).send({error});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: удалить бесполезную getStateFinish
|
|
||||||
async getStateFinish(req, res) {
|
|
||||||
const request = req.body;
|
|
||||||
let error = '';
|
|
||||||
try {
|
|
||||||
if (!request.workerId)
|
|
||||||
throw new Error(`key 'workerId' is wrong`);
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'text/json; charset=utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
const splitter = '-- aod2t5hDXU32bUFyqlFE next status --';
|
|
||||||
const refreshPause = 200;
|
|
||||||
let i = 0;
|
|
||||||
let prevProgress = -1;
|
|
||||||
let prevState = '';
|
|
||||||
let state;
|
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
|
||||||
state = this.workerState.getState(request.workerId);
|
|
||||||
if (!state) break;
|
|
||||||
|
|
||||||
res.write(splitter + JSON.stringify(state));
|
|
||||||
res.flush();
|
|
||||||
|
|
||||||
if (state.state != 'finish' && state.state != 'error')
|
|
||||||
await utils.sleep(refreshPause);
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
|
|
||||||
i++;
|
|
||||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
|
||||||
res.write(splitter + JSON.stringify({state: 'error', error: 'Слишком долгое время ожидания'}));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
|
||||||
prevProgress = state.progress;
|
|
||||||
prevState = state.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
res.write(splitter + JSON.stringify({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
error = e.message;
|
|
||||||
}
|
|
||||||
//bad request
|
|
||||||
res.status(400).send({error});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WorkerController;
|
module.exports = WorkerController;
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ module.exports = {
|
|||||||
ReaderController: require('./ReaderController'),
|
ReaderController: require('./ReaderController'),
|
||||||
WorkerController: require('./WorkerController'),
|
WorkerController: require('./WorkerController'),
|
||||||
WebSocketController: require('./WebSocketController'),
|
WebSocketController: require('./WebSocketController'),
|
||||||
|
BookUpdateCheckerController: require('./BookUpdateCheckerController'),
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,30 @@
|
|||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
const defaultTimeout = 15*1000;//15 sec
|
const defaultTimeout = 15*1000;//15 sec
|
||||||
const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException', 'SIGUSR2'];
|
const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException'];
|
||||||
|
|
||||||
//singleton
|
//singleton
|
||||||
class AsyncExit {
|
class AsyncExit {
|
||||||
constructor() {
|
constructor(signals = exitSignals, codeOnSignal = 2) {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
this.onSignalCallbacks = new Map();
|
this.onSignalCallbacks = new Map();
|
||||||
this.callbacks = new Map();
|
this.callbacks = new Map();
|
||||||
this.afterCallbacks = new Map();
|
this.afterCallbacks = new Map();
|
||||||
this.exitTimeout = defaultTimeout;
|
this.exitTimeout = defaultTimeout;
|
||||||
this.inited = false;
|
|
||||||
|
this._init(signals, codeOnSignal);
|
||||||
|
|
||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(signals = exitSignals, codeOnSignal = 2) {
|
_init(signals, codeOnSignal) {
|
||||||
if (this.inited)
|
const runSingalCallbacks = async(signal, err, origin) => {
|
||||||
throw new Error('AsyncExit: initialized already');
|
|
||||||
|
|
||||||
const runSingalCallbacks = async(signal) => {
|
|
||||||
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
||||||
try {
|
try {
|
||||||
await signalCallback(signal);
|
await signalCallback(signal, err, origin);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@@ -33,13 +32,11 @@ class AsyncExit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const signal of signals) {
|
for (const signal of signals) {
|
||||||
process.once(signal, async() => {
|
process.once(signal, async(err, origin) => {
|
||||||
await runSingalCallbacks(signal);
|
await runSingalCallbacks(signal, err, origin);
|
||||||
this.exit(codeOnSignal);
|
this.exit(codeOnSignal);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.inited = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSignal(signalCallback) {
|
onSignal(signalCallback) {
|
||||||
|
|||||||
262
server/core/BookUpdateChecker/BUCClient.js
Normal file
262
server/core/BookUpdateChecker/BUCClient.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
const WebSocketConnection = require('../WebSocketConnection');
|
||||||
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
|
||||||
|
const ayncExit = new (require('../AsyncExit'))();
|
||||||
|
const utils = require('../utils');
|
||||||
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
const minuteMs = 60*1000;
|
||||||
|
const hourMs = 60*minuteMs;
|
||||||
|
const dayMs = 24*hourMs;
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class BUCClient {
|
||||||
|
constructor(config) {
|
||||||
|
if (!instance) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this.connManager = new JembaConnManager();
|
||||||
|
this.appDb = this.connManager.db['app'];
|
||||||
|
|
||||||
|
this.wsc = new WebSocketConnection(config.bucServer.url, 10, 30, {rejectUnauthorized: false});
|
||||||
|
this.accessToken = config.bucServer.accessToken;
|
||||||
|
|
||||||
|
//константы
|
||||||
|
if (this.config.branch !== 'development') {
|
||||||
|
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
|
||||||
|
this.syncPeriod = 1*hourMs;//период синхронизации с сервером BUC
|
||||||
|
this.sendBookUrlsPeriod = 1*minuteMs;//период отправки BookUrls на сервер BUC
|
||||||
|
} else {
|
||||||
|
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
|
||||||
|
this.syncPeriod = 1*minuteMs;//период синхронизации с сервером BUC
|
||||||
|
this.sendBookUrlsPeriod = 1*1000;//период отправки BookUrls на сервер BUC
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fromCheckTime = 1;
|
||||||
|
this.bookUrls = new Set();
|
||||||
|
|
||||||
|
this.main();//no await
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsRequest(query) {
|
||||||
|
const response = await this.wsc.message(
|
||||||
|
await this.wsc.send(Object.assign({accessToken: this.accessToken}, query), 60),
|
||||||
|
60
|
||||||
|
);
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsGetBuc(fromCheckTime, callback) {
|
||||||
|
const requestId = await this.wsc.send({accessToken: this.accessToken, action: 'get-buc', fromCheckTime}, 60);
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
const res = await this.wsc.message(requestId, 60);
|
||||||
|
|
||||||
|
if (res.state == 'get') {
|
||||||
|
await callback(res.rows);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsUpdateBuc(bookUrls) {
|
||||||
|
return await this.wsRequest({action: 'update-buc', bookUrls});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkBuc(bookUrls) {
|
||||||
|
const db = this.appDb;
|
||||||
|
|
||||||
|
for (const url of bookUrls)
|
||||||
|
this.bookUrls.add(url);
|
||||||
|
|
||||||
|
const rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
map: `(r) => ({id: r.id, size: r.size})`,
|
||||||
|
where: `@@id(${db.esc(bookUrls)})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findMaxCheckTime() {
|
||||||
|
const db = this.appDb;
|
||||||
|
|
||||||
|
let result = 1;
|
||||||
|
|
||||||
|
//одним куском, возможно будет жрать память
|
||||||
|
const rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `
|
||||||
|
const result = new Set();
|
||||||
|
let max = 0;
|
||||||
|
let maxId = null;
|
||||||
|
|
||||||
|
@iter(@all(), (row) => {
|
||||||
|
if (row.checkTime > max) {
|
||||||
|
max = row.checkTime;
|
||||||
|
maxId = row.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (maxId)
|
||||||
|
result.add(maxId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length)
|
||||||
|
result = rows[0].checkTime;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicSendBookUrls() {
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
try {
|
||||||
|
//отправим this.bookUrls
|
||||||
|
if (this.bookUrls.size) {
|
||||||
|
log(`client: remote update buc begin`);
|
||||||
|
|
||||||
|
const arr = Array.from(this.bookUrls);
|
||||||
|
this.bookUrls = new Set();
|
||||||
|
|
||||||
|
const chunkSize = 100;
|
||||||
|
let updated = 0;
|
||||||
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
|
const chunk = arr.slice(i, i + chunkSize);
|
||||||
|
|
||||||
|
const res = await this.wsUpdateBuc(chunk);
|
||||||
|
if (!res.error && res.state == 'success') {
|
||||||
|
//update success
|
||||||
|
updated += chunk.length;
|
||||||
|
} else {
|
||||||
|
for (const url of chunk) {
|
||||||
|
this.bookUrls.add(url);
|
||||||
|
}
|
||||||
|
log(LM_ERR, `update-buc error: ${(res.error ? res.error : `wrong state "${res.state}"`)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(`client: remote update buc end, updated ${updated} urls`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(this.sendBookUrlsPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicSync() {
|
||||||
|
const db = this.appDb;
|
||||||
|
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
try {
|
||||||
|
//почистим нашу таблицу 'buc'
|
||||||
|
log(`client: clean 'buc' table begin`);
|
||||||
|
const cleanTime = Date.now() - this.cleanQueryInterval;
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
//выборка всех по кусочкам
|
||||||
|
const rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `
|
||||||
|
let iter = @getItem('clean');
|
||||||
|
if (!iter) {
|
||||||
|
iter = @all();
|
||||||
|
@setItem('clean', iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = new Set();
|
||||||
|
let id = iter.next();
|
||||||
|
while (!id.done) {
|
||||||
|
ids.add(id.value);
|
||||||
|
if (ids.size >= 1000)
|
||||||
|
break;
|
||||||
|
id = iter.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length) {
|
||||||
|
const toDelIds = [];
|
||||||
|
for (const row of rows)
|
||||||
|
if (row.queryTime <= cleanTime)
|
||||||
|
toDelIds.push(row.id);
|
||||||
|
|
||||||
|
//удаление
|
||||||
|
const res = await db.delete({
|
||||||
|
table: 'buc',
|
||||||
|
where: `@@id(${db.esc(toDelIds)})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`client: clean 'buc' deleted ${res.deleted}`);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `
|
||||||
|
@delItem('clean');
|
||||||
|
return new Set();
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`client: clean 'buc' table end`);
|
||||||
|
|
||||||
|
//синхронизация с сервером BUC
|
||||||
|
log(`client: sync 'buc' table begin`);
|
||||||
|
this.fromCheckTime -= 30*minuteMs;//минус полчаса на всякий случай
|
||||||
|
await this.wsGetBuc(this.fromCheckTime, async(rows) => {
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.checkTime > this.fromCheckTime)
|
||||||
|
this.fromCheckTime = row.checkTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await db.insert({
|
||||||
|
table: 'buc',
|
||||||
|
replace: true,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`client: sync 'buc' table, inserted ${res.inserted} rows, replaced ${res.replaced}`);
|
||||||
|
});
|
||||||
|
log(`client: sync 'buc' table end`);
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(this.syncPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async main() {
|
||||||
|
try {
|
||||||
|
if (!this.config.bucEnabled)
|
||||||
|
throw new Error('BookUpdateChecker disabled');
|
||||||
|
|
||||||
|
this.fromCheckTime = await this.findMaxCheckTime();
|
||||||
|
|
||||||
|
this.periodicSendBookUrls();//no await
|
||||||
|
this.periodicSync();//no await
|
||||||
|
|
||||||
|
log(`BUC Client Worker started`);
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_FATAL, e.stack);
|
||||||
|
ayncExit.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BUCClient;
|
||||||
355
server/core/BookUpdateChecker/BUCServer.js
Normal file
355
server/core/BookUpdateChecker/BUCServer.js
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
const FileDownloader = require('../FileDownloader');
|
||||||
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
|
||||||
|
const ayncExit = new (require('../AsyncExit'))();
|
||||||
|
const utils = require('../utils');
|
||||||
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
const minuteMs = 60*1000;
|
||||||
|
const hourMs = 60*minuteMs;
|
||||||
|
const dayMs = 24*hourMs;
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class BUCServer {
|
||||||
|
constructor(config) {
|
||||||
|
if (!instance) {
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
//константы
|
||||||
|
if (this.config.branch !== 'development') {
|
||||||
|
this.maxCheckQueueLength = 10000;//максимальная длина checkQueue
|
||||||
|
this.fillCheckQueuePeriod = 1*minuteMs;//период пополнения очереди
|
||||||
|
this.periodicCheckWait = 500;//пауза, если нечего делать
|
||||||
|
|
||||||
|
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
|
||||||
|
this.oldQueryInterval = 14*dayMs;//интервал устаревания запроса на обновление
|
||||||
|
this.checkingInterval = 5*hourMs;//интервал проверки обновления одного и того же файла
|
||||||
|
this.sameHostCheckInterval = 1000;//интервал проверки файла на том же сайте, не менее
|
||||||
|
} else {
|
||||||
|
this.maxCheckQueueLength = 10;//максимальная длина checkQueue
|
||||||
|
this.fillCheckQueuePeriod = 10*1000;//период пополнения очереди
|
||||||
|
this.periodicCheckWait = 500;//пауза, если нечего делать
|
||||||
|
|
||||||
|
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
|
||||||
|
this.oldQueryInterval = 30*dayMs;//интервал устаревания запроса на обновление
|
||||||
|
this.checkingInterval = 30*1000;//интервал проверки обновления одного и того же файла
|
||||||
|
this.sameHostCheckInterval = 1000;//интервал проверки файла на том же сайте, не менее
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.config.tempDownloadDir = `${config.tempDir}/download`;
|
||||||
|
fs.ensureDirSync(this.config.tempDownloadDir);
|
||||||
|
|
||||||
|
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||||
|
|
||||||
|
this.connManager = new JembaConnManager();
|
||||||
|
this.db = this.connManager.db['book-update-server'];
|
||||||
|
|
||||||
|
this.checkQueue = [];
|
||||||
|
this.hostChecking = {};
|
||||||
|
|
||||||
|
this.main(); //no await
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBuc(fromCheckTime, callback) {
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
const iterName = utils.randomHexString(30);
|
||||||
|
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
const rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `
|
||||||
|
let iter = @getItem(${db.esc(iterName)});
|
||||||
|
if (!iter) {
|
||||||
|
iter = @dirtyIndexLR('checkTime', ${db.esc(fromCheckTime)});
|
||||||
|
iter = iter.values();
|
||||||
|
@setItem(${db.esc(iterName)}, iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = new Set();
|
||||||
|
let id = iter.next();
|
||||||
|
while (!id.done) {
|
||||||
|
ids.add(id.value);
|
||||||
|
if (ids.size >= 100)
|
||||||
|
break;
|
||||||
|
id = iter.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length)
|
||||||
|
callback(rows);
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `
|
||||||
|
@delItem(${db.esc(iterName)});
|
||||||
|
return new Set();
|
||||||
|
`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBuc(bookUrls) {
|
||||||
|
const db = this.db;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
map: `(r) => ({id: r.id})`,
|
||||||
|
where: `@@id(${db.esc(bookUrls)})`
|
||||||
|
});
|
||||||
|
|
||||||
|
const exists = new Set();
|
||||||
|
for (const row of rows) {
|
||||||
|
exists.add(row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toUpdateIds = [];
|
||||||
|
const toInsertRows = [];
|
||||||
|
for (let id of bookUrls) {
|
||||||
|
if (!id)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (id.length > 1000) {
|
||||||
|
id = id.substring(0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists.has(id)) {
|
||||||
|
toUpdateIds.push(id);
|
||||||
|
} else {
|
||||||
|
toInsertRows.push({
|
||||||
|
id,
|
||||||
|
queryTime: now,
|
||||||
|
checkTime: 0, // 0 - never checked
|
||||||
|
etag: '',
|
||||||
|
modTime: '',
|
||||||
|
size: 0,
|
||||||
|
checkSum: '', //sha256
|
||||||
|
state: 0, // 0 - not processing, 1 - processing
|
||||||
|
error: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toUpdateIds.length) {
|
||||||
|
await db.update({
|
||||||
|
table: 'buc',
|
||||||
|
mod: `(r) => r.queryTime = ${db.esc(now)}`,
|
||||||
|
where: `@@id(${db.esc(toUpdateIds)})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toInsertRows.length) {
|
||||||
|
await db.insert({
|
||||||
|
table: 'buc',
|
||||||
|
ignore: true,
|
||||||
|
rows: toInsertRows,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fillCheckQueue() {
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
try {
|
||||||
|
let now = Date.now();
|
||||||
|
|
||||||
|
//чистка совсем устаревших
|
||||||
|
let rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `@@dirtyIndexLR('queryTime', undefined, ${db.esc(now - this.cleanQueryInterval)})`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length) {
|
||||||
|
const ids = rows.map((r) => r.id);
|
||||||
|
const res = await db.delete({
|
||||||
|
table: 'buc',
|
||||||
|
where: `@@id(${db.esc(ids)})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
log(LM_WARN, `clean 'buc' table: deleted ${res.deleted}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = await db.select({table: 'buc', count: true});
|
||||||
|
log(LM_WARN, `'buc' table size: ${rows[0].count}`);
|
||||||
|
|
||||||
|
now = Date.now();
|
||||||
|
//выборка кандидатов
|
||||||
|
rows = await db.select({
|
||||||
|
table: 'buc',
|
||||||
|
where: `
|
||||||
|
@@and(
|
||||||
|
@dirtyIndexLR('queryTime', ${db.esc(now - this.oldQueryInterval)}),
|
||||||
|
@dirtyIndexLR('checkTime', undefined, ${db.esc(now - this.checkingInterval)}),
|
||||||
|
@flag('notProcessing')
|
||||||
|
);
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
//формирование checkQueue
|
||||||
|
if (rows.length) {
|
||||||
|
const ids = [];
|
||||||
|
const rowsToPush = [];
|
||||||
|
|
||||||
|
//сначала выберем сколько надо
|
||||||
|
for (const row of rows) {
|
||||||
|
if (this.checkQueue.length + rowsToPush.length >= this.maxCheckQueueLength)
|
||||||
|
break;
|
||||||
|
|
||||||
|
rowsToPush.push(row);
|
||||||
|
ids.push(row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//установим у них флаг "в обработке"
|
||||||
|
await db.update({
|
||||||
|
table: 'buc',
|
||||||
|
mod: `(r) => r.state = 1`,
|
||||||
|
where: `@@id(${db.esc(ids)})`
|
||||||
|
});
|
||||||
|
|
||||||
|
//пушим в очередь, после этого их обработает periodicCheck
|
||||||
|
for (const row of rowsToPush) {
|
||||||
|
this.checkQueue.push(row);
|
||||||
|
log(LM_INFO, ` add ${row.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LM_WARN, `checkQueue: added ${ids.length} recs, total ${this.checkQueue.length}`);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(this.fillCheckQueuePeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicCheck() {
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
while (1) {//eslint-disable-line
|
||||||
|
try {
|
||||||
|
if (!this.checkQueue.length)
|
||||||
|
await utils.sleep(this.periodicCheckWait);
|
||||||
|
|
||||||
|
if (!this.checkQueue.length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const row = this.checkQueue.shift();
|
||||||
|
|
||||||
|
const url = new URL(row.id);
|
||||||
|
|
||||||
|
//только если обращались к тому же хосту не ранее sameHostCheckInterval миллисекунд назад
|
||||||
|
if (!this.hostChecking[url.hostname]) {
|
||||||
|
this.hostChecking[url.hostname] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let unchanged = true;
|
||||||
|
let hash = '';
|
||||||
|
|
||||||
|
const headers = await this.down.head(row.id);
|
||||||
|
|
||||||
|
const etag = headers['etag'] || '';
|
||||||
|
const modTime = headers['last-modified'] || '';
|
||||||
|
let size = parseInt(headers['content-length'], 10) || 0;
|
||||||
|
|
||||||
|
//log(row.id);
|
||||||
|
//log(`etag: ${etag}, modTime: ${modTime}, size: ${size}`)
|
||||||
|
|
||||||
|
if ((!etag || !row.etag || (etag !== row.etag))
|
||||||
|
&& (!modTime || !row.modTime || (modTime !== row.modTime))
|
||||||
|
&& (!size || !row.size || (size !== row.size))
|
||||||
|
) {
|
||||||
|
|
||||||
|
const downdata = await this.down.load(row.id);
|
||||||
|
|
||||||
|
size = downdata.length;
|
||||||
|
hash = await utils.getBufHash(downdata, 'sha256', 'hex');
|
||||||
|
unchanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.update({
|
||||||
|
table: 'buc',
|
||||||
|
mod: `(r) => {
|
||||||
|
r.checkTime = ${db.esc(Date.now())};
|
||||||
|
r.etag = ${(unchanged ? 'r.etag' : db.esc(etag))};
|
||||||
|
r.modTime = ${(unchanged ? 'r.modTime' : db.esc(modTime))};
|
||||||
|
r.size = ${(unchanged ? 'r.size' : db.esc(size))};
|
||||||
|
r.checkSum = ${(unchanged ? 'r.checkSum' : db.esc(hash))};
|
||||||
|
r.state = 0;
|
||||||
|
r.error = '';
|
||||||
|
}`,
|
||||||
|
where: `@@id(${db.esc(row.id)})`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unchanged) {
|
||||||
|
log(`checked ${row.id} > unchanged`);
|
||||||
|
} else {
|
||||||
|
log(`checked ${row.id} > size ${size}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await db.update({
|
||||||
|
table: 'buc',
|
||||||
|
mod: `(r) => {
|
||||||
|
r.checkTime = ${db.esc(Date.now())};
|
||||||
|
r.state = 0;
|
||||||
|
r.error = ${db.esc(e.message)};
|
||||||
|
}`,
|
||||||
|
where: `@@id(${db.esc(row.id)})`
|
||||||
|
});
|
||||||
|
|
||||||
|
log(LM_ERR, `error ${row.id} > ${e.stack ? e.stack : e.message}`);
|
||||||
|
} finally {
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(this.sameHostCheckInterval);
|
||||||
|
this.hostChecking[url.hostname] = false;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.checkQueue.push(row);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async main() {
|
||||||
|
try {
|
||||||
|
//обнуляем все статусы
|
||||||
|
await this.db.update({table: 'buc', mod: `(r) => r.state = 0`});
|
||||||
|
|
||||||
|
this.fillCheckQueue();//no await
|
||||||
|
|
||||||
|
//10 потоков
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
this.periodicCheck();//no await
|
||||||
|
|
||||||
|
log(`-------------------------`);
|
||||||
|
log(`BUC Server Worker started`);
|
||||||
|
log(`-------------------------`);
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_FATAL, e.stack);
|
||||||
|
ayncExit.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BUCServer;
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
|
const userAgent = 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0';
|
||||||
|
|
||||||
class FileDownloader {
|
class FileDownloader {
|
||||||
constructor(limitDownloadSize = 0) {
|
constructor(limitDownloadSize = 0) {
|
||||||
@@ -7,54 +10,112 @@ class FileDownloader {
|
|||||||
|
|
||||||
async load(url, callback, abort) {
|
async load(url, callback, abort) {
|
||||||
let errMes = '';
|
let errMes = '';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'
|
'user-agent': userAgent,
|
||||||
|
timeout: 300*1000,
|
||||||
},
|
},
|
||||||
responseType: 'buffer',
|
responseType: 'stream',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await got(url, Object.assign({}, options, {method: 'HEAD'}));
|
|
||||||
|
|
||||||
let estSize = 0;
|
|
||||||
if (response.headers['content-length']) {
|
|
||||||
estSize = response.headers['content-length'];
|
|
||||||
}
|
|
||||||
|
|
||||||
let prevProg = 0;
|
|
||||||
const request = got(url, options);
|
|
||||||
|
|
||||||
request.on('downloadProgress', progress => {
|
|
||||||
if (this.limitDownloadSize) {
|
|
||||||
if (progress.transferred > this.limitDownloadSize) {
|
|
||||||
errMes = 'Файл слишком большой';
|
|
||||||
request.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prog = 0;
|
|
||||||
if (estSize)
|
|
||||||
prog = Math.round(progress.transferred/estSize*100);
|
|
||||||
else if (progress.transferred)
|
|
||||||
prog = Math.round(progress.transferred/(progress.transferred + 200000)*100);
|
|
||||||
|
|
||||||
if (prog != prevProg && callback)
|
|
||||||
callback(prog);
|
|
||||||
prevProg = prog;
|
|
||||||
|
|
||||||
if (abort && abort()) {
|
|
||||||
errMes = 'abort';
|
|
||||||
request.cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (await request).body;
|
const res = await axios.get(url, options);
|
||||||
|
|
||||||
|
let estSize = 0;
|
||||||
|
if (res.headers['content-length']) {
|
||||||
|
estSize = res.headers['content-length'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.limitDownloadSize && estSize > this.limitDownloadSize) {
|
||||||
|
throw new Error('Файл слишком большой');
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevProg = 0;
|
||||||
|
let transferred = 0;
|
||||||
|
|
||||||
|
const download = this.streamToBuffer(res.data, (chunk) => {
|
||||||
|
transferred += chunk.length;
|
||||||
|
if (this.limitDownloadSize) {
|
||||||
|
if (transferred > this.limitDownloadSize) {
|
||||||
|
errMes = 'Файл слишком большой';
|
||||||
|
res.request.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prog = 0;
|
||||||
|
if (estSize)
|
||||||
|
prog = Math.round(transferred/estSize*100);
|
||||||
|
else
|
||||||
|
prog = Math.round(transferred/(transferred + 200000)*100);
|
||||||
|
|
||||||
|
if (prog != prevProg && callback)
|
||||||
|
callback(prog);
|
||||||
|
prevProg = prog;
|
||||||
|
|
||||||
|
if (abort && abort()) {
|
||||||
|
errMes = 'abort';
|
||||||
|
res.request.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await download;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errMes = (errMes ? errMes : error.message);
|
errMes = (errMes ? errMes : error.message);
|
||||||
throw new Error(errMes);
|
throw new Error(errMes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async head(url) {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
'user-agent': userAgent,
|
||||||
|
timeout: 10*1000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await axios.head(url, options);
|
||||||
|
return res.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamToBuffer(stream, progress, timeout = 30*1000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
if (!progress)
|
||||||
|
progress = () => {};
|
||||||
|
|
||||||
|
const _buf = [];
|
||||||
|
let resolved = false;
|
||||||
|
let timer = 0;
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
timer = 0;
|
||||||
|
_buf.push(chunk);
|
||||||
|
progress(chunk);
|
||||||
|
});
|
||||||
|
stream.on('end', () => {
|
||||||
|
resolved = true;
|
||||||
|
timer = timeout;
|
||||||
|
resolve(Buffer.concat(_buf));
|
||||||
|
});
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
stream.on('aborted', () => {
|
||||||
|
reject(new Error('aborted'));
|
||||||
|
});
|
||||||
|
|
||||||
|
//бодяга с timer и timeout, чтобы гарантировать отсутствие зависания по каким-либо причинам
|
||||||
|
(async() => {
|
||||||
|
while (timer < timeout) {
|
||||||
|
await utils.sleep(1000);
|
||||||
|
timer += 1000;
|
||||||
|
}
|
||||||
|
if (!resolved)
|
||||||
|
reject(new Error('FileDownloader: timed out'))
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FileDownloader;
|
module.exports = FileDownloader;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class BaseLog {
|
|||||||
this.outputBuffer = [];
|
this.outputBuffer = [];
|
||||||
|
|
||||||
await this.flushImpl(this.data)
|
await this.flushImpl(this.data)
|
||||||
.catch(e => { console.log(e); ayncExit.exit(1); } );
|
.catch(e => { console.error(`Logger error: ${e}`); ayncExit.exit(1); } );
|
||||||
this.flushing = false;
|
this.flushing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +188,8 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
ayncExit.onSignal((signal) => {
|
ayncExit.onSignal((signal, err) => {
|
||||||
this.log(LM_FATAL, `Signal ${signal} received, exiting...`);
|
this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", exiting...`);
|
||||||
});
|
});
|
||||||
ayncExit.addAfter(this.close.bind(this));
|
ayncExit.addAfter(this.close.bind(this));
|
||||||
}
|
}
|
||||||
@@ -218,6 +218,8 @@ class Logger {
|
|||||||
} else {
|
} else {
|
||||||
console.log(mes);
|
console.log(mes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const chardet = require('chardet');
|
|||||||
function getEncoding(buf) {
|
function getEncoding(buf) {
|
||||||
let selected = getEncodingLite(buf);
|
let selected = getEncodingLite(buf);
|
||||||
|
|
||||||
if (selected == 'ISO-8859-5') {
|
if (selected == 'ISO-8859-5' && buf.length > 10) {
|
||||||
const charsetAll = chardet.analyse(buf.slice(0, 20000));
|
const charsetAll = chardet.analyse(buf.slice(0, 20000));
|
||||||
for (const charset of charsetAll) {
|
for (const charset of charsetAll) {
|
||||||
if (charset.name.indexOf('ISO-8859') < 0) {
|
if (charset.name.indexOf('ISO-8859') < 0) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
@@ -20,25 +21,30 @@ class JembaReaderStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doAction(act) {
|
async doAction(act) {
|
||||||
if (!_.isObject(act.items))
|
try {
|
||||||
throw new Error('items is not an object');
|
if (!_.isObject(act.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
switch (act.action) {
|
switch (act.action) {
|
||||||
case 'check':
|
case 'check':
|
||||||
result = await this.checkItems(act.items);
|
result = await this.checkItems(act.items);
|
||||||
break;
|
break;
|
||||||
case 'get':
|
case 'get':
|
||||||
result = await this.getItems(act.items);
|
result = await this.getItems(act.items);
|
||||||
break;
|
break;
|
||||||
case 'set':
|
case 'set':
|
||||||
result = await this.setItems(act.items, act.force);
|
result = await this.setItems(act.items, act.force);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown action');
|
throw new Error('Unknown action');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, `JembaReaderStorage: ${e.message}`);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkItems(items) {
|
async checkItems(items) {
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ const WorkerState = require('../WorkerState');//singleton
|
|||||||
const FileDownloader = require('../FileDownloader');
|
const FileDownloader = require('../FileDownloader');
|
||||||
const FileDecompressor = require('../FileDecompressor');
|
const FileDecompressor = require('../FileDecompressor');
|
||||||
const BookConverter = require('./BookConverter');
|
const BookConverter = require('./BookConverter');
|
||||||
const RemoteWebDavStorage = require('../RemoteWebDavStorage');
|
const RemoteStorage = require('../RemoteStorage');
|
||||||
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
const ayncExit = new (require('../AsyncExit'))();
|
||||||
|
|
||||||
const utils = require('../utils');
|
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;//каждый час
|
||||||
|
const remoteSendPeriod = 119*1000;//примерно раз 2 минуты
|
||||||
|
|
||||||
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
@@ -33,15 +37,37 @@ class ReaderWorker {
|
|||||||
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
||||||
this.bookConverter = new BookConverter(this.config);
|
this.bookConverter = new BookConverter(this.config);
|
||||||
|
|
||||||
this.remoteWebDavStorage = false;
|
this.connManager = new JembaConnManager();
|
||||||
if (config.remoteWebDavStorage) {
|
this.appDb = this.connManager.db['app'];
|
||||||
this.remoteWebDavStorage = new RemoteWebDavStorage(
|
|
||||||
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteWebDavStorage)
|
this.remoteStorage = false;
|
||||||
|
if (config.remoteStorage) {
|
||||||
|
this.remoteStorage = new RemoteStorage(
|
||||||
|
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteStorage)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, cleanDirPeriod);
|
this.dirConfigArr = [
|
||||||
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod);
|
{
|
||||||
|
dir: this.config.tempPublicDir,
|
||||||
|
remoteDir: '/tmp',
|
||||||
|
maxSize: this.config.maxTempPublicDirSize,
|
||||||
|
moveToRemote: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dir: this.config.uploadDir,
|
||||||
|
remoteDir: '/upload',
|
||||||
|
maxSize: this.config.maxUploadPublicDirSize,
|
||||||
|
moveToRemote: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
//преобразуем в объект для большего удобства
|
||||||
|
this.dirConfig = {};
|
||||||
|
for (const configRec of this.dirConfigArr)
|
||||||
|
this.dirConfig[configRec.remoteDir] = configRec;
|
||||||
|
|
||||||
|
this.remoteFilesToSend = [];
|
||||||
|
this.periodicCleanDir();//no await
|
||||||
|
|
||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
@@ -54,7 +80,6 @@ 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 overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
|
||||||
@@ -80,6 +105,7 @@ class ReaderWorker {
|
|||||||
const tempFilename2 = utils.randomHexString(30);
|
const tempFilename2 = utils.randomHexString(30);
|
||||||
const decompDirname = utils.randomHexString(30);
|
const decompDirname = utils.randomHexString(30);
|
||||||
|
|
||||||
|
let downloadSize = -1;
|
||||||
//download or use uploaded
|
//download or use uploaded
|
||||||
if (url.indexOf('disk://') != 0) {//download
|
if (url.indexOf('disk://') != 0) {//download
|
||||||
const downdata = await this.down.load(url, (progress) => {
|
const downdata = await this.down.load(url, (progress) => {
|
||||||
@@ -87,6 +113,8 @@ class ReaderWorker {
|
|||||||
}, q.abort);
|
}, q.abort);
|
||||||
|
|
||||||
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
|
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
|
||||||
|
|
||||||
|
downloadSize = downdata.length;
|
||||||
await fs.writeFile(downloadedFilename, downdata);
|
await fs.writeFile(downloadedFilename, downdata);
|
||||||
} else {//uploaded file
|
} else {//uploaded file
|
||||||
const fileHash = url.substr(7);
|
const fileHash = url.substr(7);
|
||||||
@@ -94,8 +122,7 @@ class ReaderWorker {
|
|||||||
if (!await fs.pathExists(downloadedFilename)) {
|
if (!await fs.pathExists(downloadedFilename)) {
|
||||||
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
||||||
try {
|
try {
|
||||||
downloadedFilename = await this.restoreRemoteFile(fileHash);
|
await this.restoreRemoteFile(fileHash, '/upload');
|
||||||
isRestored = true;
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
||||||
}
|
}
|
||||||
@@ -142,34 +169,19 @@ class ReaderWorker {
|
|||||||
|
|
||||||
//finish
|
//finish
|
||||||
const finishFilename = path.basename(compFilename);
|
const finishFilename = path.basename(compFilename);
|
||||||
wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
|
|
||||||
|
|
||||||
//лениво сохраним compFilename в удаленном хранилище
|
const result = {path: `/tmp/${finishFilename}`, size: stat.size};
|
||||||
if (this.remoteWebDavStorage) {
|
if (downloadSize >= 0)
|
||||||
(async() => {
|
result.downloadSize = downloadSize;
|
||||||
await utils.sleep(20*1000);
|
|
||||||
try {
|
|
||||||
//log(`remoteWebDavStorage.putFile ${path.basename(compFilename)}`);
|
|
||||||
await this.remoteWebDavStorage.putFile(compFilename);
|
|
||||||
} catch (e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
|
wState.finish(result);
|
||||||
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
|
|
||||||
(async() => {
|
//асинхронно через 30 сек добавим в очередь на отправку
|
||||||
await utils.sleep(30*1000);
|
//т.к. gzipFileIfNotExists может переупаковать файл
|
||||||
try {
|
(async() => {
|
||||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
await utils.sleep(30*1000);
|
||||||
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
|
this.pushRemoteSend(compFilename, '/tmp');
|
||||||
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
|
})();
|
||||||
} catch (e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
@@ -211,6 +223,7 @@ class ReaderWorker {
|
|||||||
|
|
||||||
if (!await fs.pathExists(outFilename)) {
|
if (!await fs.pathExists(outFilename)) {
|
||||||
await fs.move(file.path, outFilename);
|
await fs.move(file.path, outFilename);
|
||||||
|
this.pushRemoteSend(outFilename, '/upload');
|
||||||
} else {
|
} else {
|
||||||
await utils.touchFile(outFilename);
|
await utils.touchFile(outFilename);
|
||||||
await fs.remove(file.path);
|
await fs.remove(file.path);
|
||||||
@@ -219,14 +232,42 @@ class ReaderWorker {
|
|||||||
return `disk://${hash}`;
|
return `disk://${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreRemoteFile(filename) {
|
async saveFileBuf(buf) {
|
||||||
|
const hash = await utils.getBufHash(buf, 'sha256', 'hex');
|
||||||
|
const outFilename = `${this.config.uploadDir}/${hash}`;
|
||||||
|
|
||||||
|
if (!await fs.pathExists(outFilename)) {
|
||||||
|
await fs.writeFile(outFilename, buf);
|
||||||
|
this.pushRemoteSend(outFilename, '/upload');
|
||||||
|
} else {
|
||||||
|
await utils.touchFile(outFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `disk://${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFileTouch(url) {
|
||||||
|
const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
|
||||||
|
|
||||||
|
await utils.touchFile(outFilename);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreRemoteFile(filename, remoteDir) {
|
||||||
|
let targetDir = '';
|
||||||
|
if (this.dirConfig[remoteDir])
|
||||||
|
targetDir = this.dirConfig[remoteDir].dir;
|
||||||
|
else
|
||||||
|
throw new Error(`restoreRemoteFile: unknown remoteDir value (${remoteDir})`);
|
||||||
|
|
||||||
const basename = path.basename(filename);
|
const basename = path.basename(filename);
|
||||||
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
const targetName = `${targetDir}/${basename}`;
|
||||||
|
|
||||||
if (!await fs.pathExists(targetName)) {
|
if (!await fs.pathExists(targetName)) {
|
||||||
let found = false;
|
let found = false;
|
||||||
if (this.remoteWebDavStorage) {
|
if (this.remoteStorage) {
|
||||||
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
|
found = await this.remoteStorage.getFileSuccess(targetName, remoteDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
@@ -237,83 +278,170 @@ class ReaderWorker {
|
|||||||
return targetName;
|
return targetName;
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreCachedFile(filename) {
|
pushRemoteSend(fileName, remoteDir) {
|
||||||
const workerId = this.workerState.generateWorkerId();
|
if (this.remoteStorage
|
||||||
const wState = this.workerState.getControl(workerId);
|
&& this.dirConfig[remoteDir]
|
||||||
wState.set({state: 'start'});
|
&& this.dirConfig[remoteDir].moveToRemote) {
|
||||||
|
this.remoteFilesToSend.push({fileName, remoteDir});
|
||||||
(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 basename = path.basename(filename);
|
|
||||||
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message.indexOf('404') < 0)
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
wState.set({state: 'error', error: e.message});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return workerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async periodicCleanDir(dir, maxSize, timeout) {
|
|
||||||
try {
|
|
||||||
const list = await fs.readdir(dir);
|
|
||||||
|
|
||||||
let size = 0;
|
|
||||||
let files = [];
|
|
||||||
for (const name of list) {
|
|
||||||
const stat = await fs.stat(`${dir}/${name}`);
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
size += stat.size;
|
|
||||||
files.push({name, stat});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
|
|
||||||
|
|
||||||
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
let j = 0;
|
|
||||||
while (i < files.length && size > maxSize) {
|
|
||||||
const file = files[i];
|
|
||||||
const oldFile = `${dir}/${file.name}`;
|
|
||||||
|
|
||||||
let remoteSuccess = true;
|
|
||||||
//отправляем только this.config.tempPublicDir
|
|
||||||
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
|
|
||||||
remoteSuccess = false;
|
|
||||||
try {
|
|
||||||
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
|
|
||||||
await this.remoteWebDavStorage.putFile(oldFile);
|
|
||||||
remoteSuccess = true;
|
|
||||||
} catch (e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//реально удаляем только если сохранили в хранилище
|
|
||||||
if (remoteSuccess || size > maxSize*1.2) {
|
|
||||||
await fs.remove(oldFile);
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
|
|
||||||
size -= file.stat.size;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
log(`removed ${j} files`);
|
|
||||||
} catch(e) {
|
|
||||||
log(LM_ERR, e.stack);
|
|
||||||
} finally {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.periodicCleanDir(dir, maxSize, timeout);
|
|
||||||
}, timeout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async remoteSendFile(sendFileRec) {
|
||||||
|
const {fileName, remoteDir} = sendFileRec;
|
||||||
|
const sent = this.remoteSent;
|
||||||
|
|
||||||
|
if (!fileName || sent[fileName])
|
||||||
|
return;
|
||||||
|
|
||||||
|
log(`remoteSendFile ${remoteDir}/${path.basename(fileName)}`);
|
||||||
|
|
||||||
|
//отправляем в remoteStorage
|
||||||
|
await this.remoteStorage.putFile(fileName, remoteDir);
|
||||||
|
|
||||||
|
sent[fileName] = true;
|
||||||
|
await this.appDb.insert({table: 'remote_sent', ignore: true, rows: [{id: fileName, remoteDir}]});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remoteSendAll() {
|
||||||
|
if (!this.remoteStorage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newSendQueue = [];
|
||||||
|
while (this.remoteFilesToSend.length) {
|
||||||
|
const sendFileRec = this.remoteFilesToSend.shift();
|
||||||
|
|
||||||
|
if (sendFileRec.remoteDir
|
||||||
|
&& this.dirConfig[sendFileRec.remoteDir]
|
||||||
|
&& this.dirConfig[sendFileRec.remoteDir].moveToRemote) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.remoteSendFile(sendFileRec);
|
||||||
|
} catch (e) {
|
||||||
|
newSendQueue.push(sendFileRec)
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remoteFilesToSend = newSendQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanDir(config) {
|
||||||
|
const {dir, remoteDir, maxSize, moveToRemote} = config;
|
||||||
|
const sent = this.remoteSent;
|
||||||
|
|
||||||
|
const list = await fs.readdir(dir);
|
||||||
|
|
||||||
|
let size = 0;
|
||||||
|
let files = [];
|
||||||
|
for (const filename of list) {
|
||||||
|
const filePath = `${dir}/${filename}`;
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
size += stat.size;
|
||||||
|
files.push({name: filePath, stat});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LM_WARN, `clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
|
||||||
|
|
||||||
|
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
||||||
|
|
||||||
|
//удаленное хранилище
|
||||||
|
if (moveToRemote && this.remoteStorage) {
|
||||||
|
const foundFiles = new Set();
|
||||||
|
for (const file of files) {
|
||||||
|
foundFiles.add(file.name);
|
||||||
|
|
||||||
|
//отсылаем на всякий случай перед удалением, если вдруг remoteSendAll не справился
|
||||||
|
try {
|
||||||
|
await this.remoteSendFile({fileName: file.name, remoteDir});
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//почистим remoteSent и БД
|
||||||
|
//несколько неоптимально, таскает все записи из таблицы
|
||||||
|
const rows = await this.appDb.select({table: 'remote_sent'});
|
||||||
|
for (const row of rows) {
|
||||||
|
if ((row.remoteDir === remoteDir && !foundFiles.has(row.id))
|
||||||
|
|| !this.dirConfig[row.remoteDir]) {
|
||||||
|
delete sent[row.id];
|
||||||
|
await this.appDb.delete({table: 'remote_sent', where: `@@id(${this.appDb.esc(row.id)})`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let j = 0;
|
||||||
|
while (i < files.length && size > maxSize) {
|
||||||
|
const file = files[i];
|
||||||
|
const oldFile = file.name;
|
||||||
|
|
||||||
|
//реально удаляем только если сохранили в хранилище или размер dir увеличен в 1.5 раза
|
||||||
|
if (!(moveToRemote && this.remoteStorage)
|
||||||
|
|| (moveToRemote && this.remoteStorage && sent[oldFile])
|
||||||
|
|| size > maxSize*1.5) {
|
||||||
|
await fs.remove(oldFile);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size -= file.stat.size;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(LM_WARN, `removed ${j} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicCleanDir() {
|
||||||
|
try {
|
||||||
|
if (!this.remoteSent)
|
||||||
|
this.remoteSent = {};
|
||||||
|
|
||||||
|
//инициализация this.remoteSent
|
||||||
|
if (this.remoteStorage) {
|
||||||
|
const rows = await this.appDb.select({table: 'remote_sent'});
|
||||||
|
for (const row of rows) {
|
||||||
|
this.remoteSent[row.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastCleanDirTime = 0;
|
||||||
|
let lastRemoteSendTime = 0;
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
//отсылка в удаленное хранилище
|
||||||
|
if (Date.now() - lastRemoteSendTime >= remoteSendPeriod) {
|
||||||
|
try {
|
||||||
|
await this.remoteSendAll();
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRemoteSendTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
//чистка папок
|
||||||
|
if (Date.now() - lastCleanDirTime >= cleanDirPeriod) {
|
||||||
|
for (const config of Object.values(this.dirConfig)) {
|
||||||
|
try {
|
||||||
|
await this.cleanDir(config);
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCleanDirTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(60*1000);//интервал проверки 1 минута
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_FATAL, e.message);
|
||||||
|
ayncExit.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ReaderWorker;
|
module.exports = ReaderWorker;
|
||||||
98
server/core/RemoteStorage.js
Normal file
98
server/core/RemoteStorage.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const WebSocketConnection = require('./WebSocketConnection');
|
||||||
|
|
||||||
|
class RemoteStorage {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = Object.assign({}, config);
|
||||||
|
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
|
||||||
|
|
||||||
|
this.accessToken = this.config.accessToken;
|
||||||
|
|
||||||
|
this.wsc = new WebSocketConnection(config.url, 10, 30, {rejectUnauthorized: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsRequest(query) {
|
||||||
|
const response = await this.wsc.message(
|
||||||
|
await this.wsc.send(Object.assign({accessToken: this.accessToken}, query), 600),
|
||||||
|
600
|
||||||
|
);
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsStat(fileName) {
|
||||||
|
return await this.wsRequest({action: 'get-stat', fileName});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsGetFile(fileName) {
|
||||||
|
return this.wsRequest({action: 'get-file', fileName});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsPutFile(fileName, data) {//data base64 encoded string
|
||||||
|
return this.wsRequest({action: 'put-file', fileName, data});
|
||||||
|
}
|
||||||
|
|
||||||
|
async wsDelFile(fileName) {
|
||||||
|
return this.wsRequest({action: 'del-file', fileName});
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRemoteFileName(fileName, dir = '') {
|
||||||
|
const base = path.basename(fileName);
|
||||||
|
if (base.length > 3) {
|
||||||
|
return `${dir}/${base.substr(0, 3)}/${base}`;
|
||||||
|
} else {
|
||||||
|
return `${dir}/${base}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async putFile(fileName, dir = '') {
|
||||||
|
if (!await fs.pathExists(fileName)) {
|
||||||
|
throw new Error(`File not found: ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFilename = this.makeRemoteFileName(fileName, dir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localStat = await fs.stat(fileName);
|
||||||
|
let remoteStat = await this.wsStat(remoteFilename);
|
||||||
|
remoteStat = remoteStat.stat;
|
||||||
|
|
||||||
|
if (remoteStat.isFile && localStat.size == remoteStat.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.wsDelFile(remoteFilename);
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fs.readFile(fileName, 'base64');
|
||||||
|
await this.wsPutFile(remoteFilename, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFile(fileName, dir = '') {
|
||||||
|
if (await fs.pathExists(fileName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFilename = this.makeRemoteFileName(fileName, dir);
|
||||||
|
|
||||||
|
const response = await this.wsGetFile(remoteFilename);
|
||||||
|
await fs.writeFile(fileName, response.data, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileSuccess(filename, dir = '') {
|
||||||
|
try {
|
||||||
|
await this.getFile(filename, dir);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RemoteStorage;
|
||||||
@@ -46,16 +46,16 @@ class RemoteWebDavStorage {
|
|||||||
return await this.wdc.createDirectory(dirname);
|
return await this.wdc.createDirectory(dirname);
|
||||||
}
|
}
|
||||||
|
|
||||||
async putFile(filename) {
|
async putFile(filename, dir = '') {
|
||||||
if (!await fs.pathExists(filename)) {
|
if (!await fs.pathExists(filename)) {
|
||||||
throw new Error(`File not found: ${filename}`);
|
throw new Error(`File not found: ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = path.basename(filename);
|
const base = path.basename(filename);
|
||||||
let remoteFilename = `/${base}`;
|
let remoteFilename = `${dir}/${base}`;
|
||||||
|
|
||||||
if (base.length > 3) {
|
if (base.length > 3) {
|
||||||
const remoteDir = `/${base.substr(0, 3)}`;
|
const remoteDir = `${dir}/${base.substr(0, 3)}`;
|
||||||
try {
|
try {
|
||||||
await this.mkdir(remoteDir);
|
await this.mkdir(remoteDir);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -79,24 +79,24 @@ class RemoteWebDavStorage {
|
|||||||
await this.writeFile(remoteFilename, data);
|
await this.writeFile(remoteFilename, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFile(filename) {
|
async getFile(filename, dir = '') {
|
||||||
if (await fs.pathExists(filename)) {
|
if (await fs.pathExists(filename)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = path.basename(filename);
|
const base = path.basename(filename);
|
||||||
let remoteFilename = `/${base}`;
|
let remoteFilename = `${dir}/${base}`;
|
||||||
if (base.length > 3) {
|
if (base.length > 3) {
|
||||||
remoteFilename = `/${base.substr(0, 3)}/${base}`;
|
remoteFilename = `${dir}/${base.substr(0, 3)}/${base}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.readFile(remoteFilename);
|
const data = await this.readFile(remoteFilename);
|
||||||
await fs.writeFile(filename, data);
|
await fs.writeFile(filename, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileSuccess(filename) {
|
async getFileSuccess(filename, dir = '') {
|
||||||
try {
|
try {
|
||||||
await this.getFile(filename);
|
await this.getFile(filename, dir);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ const cleanPeriod = 5*1000;//5 секунд
|
|||||||
|
|
||||||
class WebSocketConnection {
|
class WebSocketConnection {
|
||||||
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
||||||
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
|
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30, webSocketOptions = {}) {
|
||||||
//const ws = 'ws';//for nodejs
|
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
|
||||||
this.WebSocket = (isBrowser ? WebSocket : null/*for nodejs require(ws)*/);
|
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
this.webSocketOptions = webSocketOptions;
|
||||||
|
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
this.listeners = [];
|
this.listeners = [];
|
||||||
this.messageQueue = [];
|
this.messageQueue = [];
|
||||||
this.messageLifeTime = messageLifeTimeSecs*1000;
|
this.messageLifeTime = messageLifeTimeSecs*1000;
|
||||||
@@ -92,10 +94,10 @@ class WebSocketConnection {
|
|||||||
const url = this.url || `${protocol}//${window.location.host}/ws`;
|
const url = this.url || `${protocol}//${window.location.host}/ws`;
|
||||||
this.ws = new this.WebSocket(url);
|
this.ws = new this.WebSocket(url);
|
||||||
} else {
|
} else {
|
||||||
this.ws = new this.WebSocket(this.url);
|
this.ws = new this.WebSocket(this.url, this.webSocketOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onopen = (e) => {
|
const onopen = () => {
|
||||||
this.connecting = false;
|
this.connecting = false;
|
||||||
resolve(this.ws);
|
resolve(this.ws);
|
||||||
};
|
};
|
||||||
@@ -166,7 +168,7 @@ class WebSocketConnection {
|
|||||||
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
|
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
|
||||||
const requestId = this.requestId;//реентерабельность!!!
|
const requestId = this.requestId;//реентерабельность!!!
|
||||||
|
|
||||||
this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1
|
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||||
|
|
||||||
let resp = {};
|
let resp = {};
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class WorkerState {
|
|||||||
return {
|
return {
|
||||||
set: state => this.setState(workerId, state),
|
set: state => this.setState(workerId, state),
|
||||||
finish: state => this.finishState(workerId, state),
|
finish: state => this.finishState(workerId, state),
|
||||||
get: workerId => this.getState(workerId),
|
get: () => this.getState(workerId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function toBase36(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fromBase36(data) {
|
function fromBase36(data) {
|
||||||
return bs36.decode(data);
|
return Buffer.from(bs36.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function bufferRemoveZeroes(buf) {
|
function bufferRemoveZeroes(buf) {
|
||||||
@@ -34,6 +34,12 @@ function getFileHash(filename, hashName, enc) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBufHash(buf, hashName, enc) {
|
||||||
|
const hash = crypto.createHash(hashName);
|
||||||
|
hash.update(buf);
|
||||||
|
return hash.digest(enc);
|
||||||
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -129,6 +135,7 @@ module.exports = {
|
|||||||
fromBase36,
|
fromBase36,
|
||||||
bufferRemoveZeroes,
|
bufferRemoveZeroes,
|
||||||
getFileHash,
|
getFileHash,
|
||||||
|
getBufHash,
|
||||||
sleep,
|
sleep,
|
||||||
toUnixTime,
|
toUnixTime,
|
||||||
randomHexString,
|
randomHexString,
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
//TODO: удалить модуль в 2023г
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
|
|
||||||
const SqliteConnectionPool = require('./SqliteConnectionPool');
|
|
||||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
|
||||||
|
|
||||||
const migrations = {
|
|
||||||
'app': require('./migrations/app'),
|
|
||||||
'readerStorage': require('./migrations/readerStorage'),
|
|
||||||
};
|
|
||||||
|
|
||||||
let instance = null;
|
|
||||||
|
|
||||||
//singleton
|
|
||||||
class ConnManager {
|
|
||||||
constructor() {
|
|
||||||
if (!instance) {
|
|
||||||
this.inited = false;
|
|
||||||
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(config) {
|
|
||||||
this.config = config;
|
|
||||||
this._pool = {};
|
|
||||||
|
|
||||||
const force = null;//(config.branch == 'development' ? 'last' : null);
|
|
||||||
|
|
||||||
for (const poolConfig of this.config.db) {
|
|
||||||
const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
|
|
||||||
|
|
||||||
//бэкап
|
|
||||||
if (!poolConfig.noBak && await fs.pathExists(dbFileName))
|
|
||||||
await fs.copy(dbFileName, `${dbFileName}.bak`);
|
|
||||||
|
|
||||||
const connPool = new SqliteConnectionPool();
|
|
||||||
await connPool.open(poolConfig, dbFileName);
|
|
||||||
|
|
||||||
log(`Opened database "${poolConfig.poolName}"`);
|
|
||||||
//миграции
|
|
||||||
const migs = migrations[poolConfig.poolName];
|
|
||||||
if (migs && migs.data.length) {
|
|
||||||
const applied = await connPool.migrate(migs.data, migs.table, force);
|
|
||||||
if (applied.length)
|
|
||||||
log(`${applied.length} migrations applied to "${poolConfig.poolName}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._pool[poolConfig.poolName] = connPool;
|
|
||||||
}
|
|
||||||
this.inited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get pool() {
|
|
||||||
return this._pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ConnManager;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
//TODO: удалить модуль в 2023г
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
|
||||||
|
|
||||||
class Converter {
|
|
||||||
async run(config) {
|
|
||||||
log('Converter start');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const connManager = new (require('./ConnManager'))();//singleton
|
|
||||||
const storagePool = connManager.pool.readerStorage;
|
|
||||||
|
|
||||||
const jembaConnManager = new (require('./JembaConnManager'))();//singleton
|
|
||||||
const db = jembaConnManager.db['reader-storage'];
|
|
||||||
|
|
||||||
const srcDbPath = `${config.dataDir}/reader-storage.sqlite`;
|
|
||||||
if (!await fs.pathExists(srcDbPath)) {
|
|
||||||
log(LM_WARN, ' Source DB does not exist, nothing to do');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await db.select({table: 'storage', count: true});
|
|
||||||
if (rows.length && rows[0].count != 0) {
|
|
||||||
log(LM_WARN, ` Destination table already exists (found ${rows[0].count} items), nothing to do`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbSrc = await storagePool.get();
|
|
||||||
try {
|
|
||||||
const rows = await dbSrc.all(`SELECT * FROM storage`);
|
|
||||||
await db.insert({table: 'storage', rows});
|
|
||||||
log(` Inserted ${rows.length} items`);
|
|
||||||
} finally {
|
|
||||||
dbSrc.ret();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
log('Converter finish');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Converter;
|
|
||||||
@@ -14,6 +14,7 @@ class JembaConnManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
this.inited = false;
|
this.inited = false;
|
||||||
|
this._db = {};
|
||||||
|
|
||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,31 @@ class JembaConnManager {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this._db = {};
|
this._db = {};
|
||||||
|
|
||||||
|
ayncExit.add(this.close.bind(this));
|
||||||
|
|
||||||
|
const serverModes = new Set();
|
||||||
|
for (const serverCfg of this.config.servers) {
|
||||||
|
serverModes.add(serverCfg.mode);
|
||||||
|
}
|
||||||
|
|
||||||
for (const dbConfig of this.config.jembaDb) {
|
for (const dbConfig of this.config.jembaDb) {
|
||||||
|
//проверка, надо ли открывать базу, зависит от serverMode
|
||||||
|
if (dbConfig.serverMode) {
|
||||||
|
let serverMode = dbConfig.serverMode;
|
||||||
|
if (!Array.isArray(dbConfig.serverMode))
|
||||||
|
serverMode = [dbConfig.serverMode];
|
||||||
|
|
||||||
|
let modePresent = false;
|
||||||
|
for (const mode of serverMode) {
|
||||||
|
modePresent = serverModes.has(mode);
|
||||||
|
if (modePresent)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modePresent)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const dbPath = `${this.config.dataDir}/db/${dbConfig.dbName}`;
|
const dbPath = `${this.config.dataDir}/db/${dbConfig.dbName}`;
|
||||||
|
|
||||||
//бэкап
|
//бэкап
|
||||||
@@ -44,17 +69,23 @@ class JembaConnManager {
|
|||||||
} else {
|
} else {
|
||||||
dbConn = new JembaDb();
|
dbConn = new JembaDb();
|
||||||
}
|
}
|
||||||
|
this._db[dbConfig.dbName] = dbConn;
|
||||||
|
|
||||||
log(`Open "${dbConfig.dbName}" begin`);
|
log(`Open "${dbConfig.dbName}" begin`);
|
||||||
await dbConn.openDb({
|
await dbConn.lock({
|
||||||
dbPath,
|
dbPath,
|
||||||
create: true,
|
create: true,
|
||||||
cacheSize: dbConfig.cacheSize,
|
softLock: true,
|
||||||
compressed: dbConfig.compressed,
|
|
||||||
forceFileClosing: dbConfig.forceFileClosing
|
tableDefaults: {
|
||||||
|
cacheSize: dbConfig.cacheSize,
|
||||||
|
compressed: dbConfig.compressed,
|
||||||
|
forceFileClosing: dbConfig.forceFileClosing,
|
||||||
|
typeCompatMode: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dbConfig.openAll) {
|
if (dbConfig.openAll || forceAutoRepair || dbConfig.autoRepair) {
|
||||||
try {
|
try {
|
||||||
await dbConn.openAll();
|
await dbConn.openAll();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -83,21 +114,15 @@ class JembaConnManager {
|
|||||||
if (applied.length)
|
if (applied.length)
|
||||||
log(`${applied.length} migrations applied to "${dbConfig.dbName}"`);
|
log(`${applied.length} migrations applied to "${dbConfig.dbName}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._db[dbConfig.dbName] = dbConn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ayncExit.add(this.close.bind(this));
|
|
||||||
|
|
||||||
this.inited = true;
|
this.inited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
if (!this.inited)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (const dbConfig of this.config.jembaDb) {
|
for (const dbConfig of this.config.jembaDb) {
|
||||||
await this._db[dbConfig.dbName].closeDb();
|
if (this._db[dbConfig.dbName])
|
||||||
|
await this._db[dbConfig.dbName].unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._db = {};
|
this._db = {};
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
//TODO: удалить модуль в 2023г
|
|
||||||
const sqlite3 = require('sqlite3');
|
|
||||||
const sqlite = require('sqlite');
|
|
||||||
|
|
||||||
const SQL = require('sql-template-strings');
|
|
||||||
|
|
||||||
class SqliteConnectionPool {
|
|
||||||
constructor() {
|
|
||||||
this.closed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async open(poolConfig, dbFileName) {
|
|
||||||
const connCount = poolConfig.connCount || 1;
|
|
||||||
const busyTimeout = poolConfig.busyTimeout || 60*1000;
|
|
||||||
const cacheSize = poolConfig.cacheSize || 2000;
|
|
||||||
|
|
||||||
this.dbFileName = dbFileName;
|
|
||||||
this.connections = [];
|
|
||||||
this.freed = new Set();
|
|
||||||
this.waitingQueue = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < connCount; i++) {
|
|
||||||
let client = await sqlite.open({
|
|
||||||
filename: dbFileName,
|
|
||||||
driver: sqlite3.Database
|
|
||||||
});
|
|
||||||
|
|
||||||
client.configure('busyTimeout', busyTimeout); //ms
|
|
||||||
await client.exec(`PRAGMA cache_size = ${cacheSize}`);
|
|
||||||
|
|
||||||
client.ret = () => {
|
|
||||||
this.freed.add(i);
|
|
||||||
if (this.waitingQueue.length) {
|
|
||||||
this.waitingQueue.shift().onFreed(i);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.freed.add(i);
|
|
||||||
this.connections[i] = client;
|
|
||||||
}
|
|
||||||
this.closed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (this.closed)
|
|
||||||
throw new Error('Connection pool closed');
|
|
||||||
|
|
||||||
const freeConnIndex = this.freed.values().next().value;
|
|
||||||
if (freeConnIndex !== undefined) {
|
|
||||||
this.freed.delete(freeConnIndex);
|
|
||||||
resolve(this.connections[freeConnIndex]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.waitingQueue.push({
|
|
||||||
onFreed: (connIndex) => {
|
|
||||||
this.freed.delete(connIndex);
|
|
||||||
resolve(this.connections[connIndex]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(query) {
|
|
||||||
const dbh = await this.get();
|
|
||||||
try {
|
|
||||||
let result = await dbh.run(query);
|
|
||||||
dbh.ret();
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
dbh.ret();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async all(query) {
|
|
||||||
const dbh = await this.get();
|
|
||||||
try {
|
|
||||||
let result = await dbh.all(query);
|
|
||||||
dbh.ret();
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
dbh.ret();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exec(query) {
|
|
||||||
const dbh = await this.get();
|
|
||||||
try {
|
|
||||||
let result = await dbh.exec(query);
|
|
||||||
dbh.ret();
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
dbh.ret();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
for (let i = 0; i < this.connections.length; i++) {
|
|
||||||
await this.connections[i].close();
|
|
||||||
}
|
|
||||||
this.closed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modified from node-sqlite/.../src/Database.js
|
|
||||||
async migrate(migs, table, force) {
|
|
||||||
const migrations = migs.sort((a, b) => Math.sign(a.id - b.id));
|
|
||||||
|
|
||||||
if (!migrations.length) {
|
|
||||||
throw new Error('No migration data');
|
|
||||||
}
|
|
||||||
|
|
||||||
migrations.map(migration => {
|
|
||||||
const data = migration.data;
|
|
||||||
const [up, down] = data.split(/^--\s+?down\b/mi);
|
|
||||||
if (!down) {
|
|
||||||
const message = `The ${migration.filename} file does not contain '-- Down' separator.`;
|
|
||||||
throw new Error(message);
|
|
||||||
} else {
|
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
migration.up = up.replace(/^-- .*?$/gm, '').trim();// Remove comments
|
|
||||||
migration.down = down.trim(); // and trim whitespaces
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a database table for migrations meta data if it doesn't exist
|
|
||||||
await this.run(`CREATE TABLE IF NOT EXISTS "${table}" (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
up TEXT NOT NULL,
|
|
||||||
down TEXT NOT NULL
|
|
||||||
)`);
|
|
||||||
|
|
||||||
// Get the list of already applied migrations
|
|
||||||
let dbMigrations = await this.all(
|
|
||||||
`SELECT id, name, up, down FROM "${table}" ORDER BY id ASC`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Undo migrations that exist only in the database but not in migs,
|
|
||||||
// also undo the last migration if the `force` option was set to `last`.
|
|
||||||
const lastMigration = migrations[migrations.length - 1];
|
|
||||||
for (const migration of dbMigrations.slice().sort((a, b) => Math.sign(b.id - a.id))) {
|
|
||||||
if (!migrations.some(x => x.id === migration.id) ||
|
|
||||||
(force === 'last' && migration.id === lastMigration.id)) {
|
|
||||||
const dbh = await this.get();
|
|
||||||
await dbh.run('BEGIN');
|
|
||||||
try {
|
|
||||||
await dbh.exec(migration.down);
|
|
||||||
await dbh.run(SQL`DELETE FROM "`.append(table).append(SQL`" WHERE id = ${migration.id}`));
|
|
||||||
await dbh.run('COMMIT');
|
|
||||||
dbMigrations = dbMigrations.filter(x => x.id !== migration.id);
|
|
||||||
} catch (err) {
|
|
||||||
await dbh.run('ROLLBACK');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
dbh.ret();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply pending migrations
|
|
||||||
let applied = [];
|
|
||||||
const lastMigrationId = dbMigrations.length ? dbMigrations[dbMigrations.length - 1].id : 0;
|
|
||||||
for (const migration of migrations) {
|
|
||||||
if (migration.id > lastMigrationId) {
|
|
||||||
const dbh = await this.get();
|
|
||||||
await dbh.run('BEGIN');
|
|
||||||
try {
|
|
||||||
await dbh.exec(migration.up);
|
|
||||||
await dbh.run(SQL`INSERT INTO "`.append(table).append(
|
|
||||||
SQL`" (id, name, up, down) VALUES (${migration.id}, ${migration.name}, ${migration.up}, ${migration.down})`)
|
|
||||||
);
|
|
||||||
await dbh.run('COMMIT');
|
|
||||||
applied.push(migration.id);
|
|
||||||
} catch (err) {
|
|
||||||
await dbh.run('ROLLBACK');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
dbh.ret();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return applied;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = SqliteConnectionPool;
|
|
||||||
12
server/db/jembaMigrations/app/001-create.js
Normal file
12
server/db/jembaMigrations/app/001-create.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: [
|
||||||
|
['create', {
|
||||||
|
table: 'remote_sent'
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
down: [
|
||||||
|
['drop', {
|
||||||
|
table: 'remote_sent'
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
};
|
||||||
22
server/db/jembaMigrations/app/002-create.js
Normal file
22
server/db/jembaMigrations/app/002-create.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: [
|
||||||
|
['create', {
|
||||||
|
/*{
|
||||||
|
id, // book URL
|
||||||
|
queryTime: Number,
|
||||||
|
checkTime: Number, // 0 - never checked
|
||||||
|
modTime: String,
|
||||||
|
size: Number,
|
||||||
|
checkSum: String, //sha256
|
||||||
|
state: Number, // 0 - not processing, 1 - processing
|
||||||
|
error: String,
|
||||||
|
}*/
|
||||||
|
table: 'buc'
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
down: [
|
||||||
|
['drop', {
|
||||||
|
table: 'buc'
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
};
|
||||||
7
server/db/jembaMigrations/app/index.js
Normal file
7
server/db/jembaMigrations/app/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
table: 'migration1',
|
||||||
|
data: [
|
||||||
|
{id: 1, name: 'create', data: require('./001-create')},
|
||||||
|
{id: 2, name: 'create', data: require('./002-create')},
|
||||||
|
]
|
||||||
|
}
|
||||||
29
server/db/jembaMigrations/book-update-server/001-create.js
Normal file
29
server/db/jembaMigrations/book-update-server/001-create.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: [
|
||||||
|
['create', {
|
||||||
|
/*{
|
||||||
|
id, // book URL
|
||||||
|
queryTime: Number,
|
||||||
|
checkTime: Number, // 0 - never checked
|
||||||
|
modTime: String,
|
||||||
|
size: Number,
|
||||||
|
checkSum: String, //sha256
|
||||||
|
state: Number, // 0 - not processing, 1 - processing
|
||||||
|
error: String,
|
||||||
|
}*/
|
||||||
|
table: 'buc',
|
||||||
|
flag: [
|
||||||
|
{name: 'notProcessing', check: `(r) => r.state === 0`},
|
||||||
|
],
|
||||||
|
index: [
|
||||||
|
{field: 'queryTime', type: 'number'},
|
||||||
|
{field: 'checkTime', type: 'number'},
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
down: [
|
||||||
|
['drop', {
|
||||||
|
table: 'buc'
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
//'app': require('./jembaMigrations/app'),
|
'app': require('./app'),
|
||||||
'reader-storage': require('./reader-storage'),
|
'reader-storage': require('./reader-storage'),
|
||||||
|
'book-update-server': require('./book-update-server'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
table: 'migration1',
|
|
||||||
data: [
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = `
|
|
||||||
-- Up
|
|
||||||
CREATE TABLE storage (id TEXT PRIMARY KEY, rev INTEGER, time INTEGER, data TEXT);
|
|
||||||
|
|
||||||
-- Down
|
|
||||||
DROP TABLE storage;
|
|
||||||
`;
|
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
|
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
|
||||||
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
|
||||||
const argv = require('minimist')(process.argv.slice(2));
|
const argv = require('minimist')(process.argv.slice(2));
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
const WebSocket = require ('ws');
|
const WebSocket = require ('ws');
|
||||||
|
|
||||||
const ayncExit = new (require('./core/AsyncExit'))();
|
const ayncExit = new (require('./core/AsyncExit'))();
|
||||||
ayncExit.init();
|
|
||||||
|
|
||||||
let log = null;
|
let log = null;
|
||||||
|
|
||||||
|
const maxPayloadSize = 50;//in MB
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
//config
|
//config
|
||||||
const configManager = new (require('./config'))();//singleton
|
const configManager = new (require('./config'))();//singleton
|
||||||
@@ -44,15 +46,8 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//connections
|
//connections
|
||||||
const connManager = new (require('./db/ConnManager'))();//singleton
|
|
||||||
await connManager.init(config);
|
|
||||||
|
|
||||||
const jembaConnManager = new (require('./db/JembaConnManager'))();//singleton
|
const jembaConnManager = new (require('./db/JembaConnManager'))();//singleton
|
||||||
await jembaConnManager.init(config, argv['auto-repair']);
|
await jembaConnManager.init(config, argv['auto-repair']);
|
||||||
|
|
||||||
//converter SQLITE => JembaDb
|
|
||||||
const converter = new (require('./db/Converter'))();
|
|
||||||
await converter.run(config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -63,8 +58,16 @@ async function main() {
|
|||||||
for (let serverCfg of config.servers) {
|
for (let serverCfg of config.servers) {
|
||||||
if (serverCfg.mode !== 'none') {
|
if (serverCfg.mode !== 'none') {
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
let server;
|
||||||
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
|
if (serverCfg.isHttps) {
|
||||||
|
const key = fs.readFileSync(`${config.dataDir}/${serverCfg.keysFile}.key`);
|
||||||
|
const cert = fs.readFileSync(`${config.dataDir}/${serverCfg.keysFile}.crt`);
|
||||||
|
|
||||||
|
server = https.createServer({key, cert}, app);
|
||||||
|
} else {
|
||||||
|
server = http.createServer(app);
|
||||||
|
}
|
||||||
|
const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
|
||||||
|
|
||||||
const serverConfig = Object.assign({}, config, serverCfg);
|
const serverConfig = Object.assign({}, config, serverCfg);
|
||||||
|
|
||||||
@@ -76,20 +79,10 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.use(compression({ level: 1 }));
|
app.use(compression({ level: 1 }));
|
||||||
app.use(express.json({limit: '10mb'}));
|
app.use(express.json({limit: `${maxPayloadSize}mb`}));
|
||||||
if (devModule)
|
if (devModule)
|
||||||
devModule.logQueries(app);
|
devModule.logQueries(app);
|
||||||
|
|
||||||
app.use(express.static(serverConfig.publicDir, {
|
|
||||||
maxAge: '30d',
|
|
||||||
setHeaders: (res, filePath) => {
|
|
||||||
if (path.basename(path.dirname(filePath)) == 'tmp') {
|
|
||||||
res.set('Content-Type', 'application/xml');
|
|
||||||
res.set('Content-Encoding', 'gzip');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
require('./routes').initRoutes(app, wss, serverConfig);
|
require('./routes').initRoutes(app, wss, serverConfig);
|
||||||
|
|
||||||
if (devModule) {
|
if (devModule) {
|
||||||
@@ -102,7 +95,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server.listen(serverConfig.port, serverConfig.ip, function() {
|
server.listen(serverConfig.port, serverConfig.ip, function() {
|
||||||
log(`Server-${serverConfig.serverName} is ready on ${serverConfig.ip}:${serverConfig.port}, mode: ${serverConfig.mode}`);
|
log(`Server "${serverConfig.serverName}" is ready on ${(serverConfig.isHttps ? 'https://' : 'http://')}${serverConfig.ip}:${serverConfig.port}, mode: ${serverConfig.mode}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
const c = require('./controllers');
|
const fs = require('fs-extra');
|
||||||
const utils = require('./core/utils');
|
const path = require('path');
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
|
|
||||||
|
const ReaderWorker = require('./core/Reader/ReaderWorker');//singleton
|
||||||
|
const log = new (require('./core/AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
const c = require('./controllers');
|
||||||
|
const utils = require('./core/utils');
|
||||||
|
|
||||||
function initRoutes(app, wss, config) {
|
function initRoutes(app, wss, config) {
|
||||||
|
//эксклюзив для update_checker
|
||||||
|
if (config.mode === 'book_update_checker') {
|
||||||
|
new c.BookUpdateCheckerController(wss, config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initStatic(app, config);
|
||||||
|
|
||||||
const misc = new c.MiscController(config);
|
const misc = new c.MiscController(config);
|
||||||
const reader = new c.ReaderController(config);
|
const reader = new c.ReaderController(config);
|
||||||
const worker = new c.WorkerController(config);
|
const worker = new c.WorkerController(config);
|
||||||
@@ -29,9 +45,7 @@ function initRoutes(app, wss, config) {
|
|||||||
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
|
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
|
||||||
['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}],
|
['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}],
|
||||||
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
|
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
|
||||||
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
|
|
||||||
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
|
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
|
||||||
['POST', '/api/worker/get-state-finish', worker.getStateFinish.bind(worker), [aAll], {}],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
//to app
|
//to app
|
||||||
@@ -78,6 +92,48 @@ function initRoutes(app, wss, config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initStatic(app, config) {
|
||||||
|
const readerWorker = new ReaderWorker(config);
|
||||||
|
|
||||||
|
//восстановление файлов в /tmp и /upload из webdav-storage, при необходимости
|
||||||
|
app.use(async(req, res, next) => {
|
||||||
|
if ((req.method !== 'GET' && req.method !== 'HEAD') ||
|
||||||
|
!(req.path.indexOf('/tmp/') === 0 || req.path.indexOf('/upload/') === 0)
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `${config.publicDir}${req.path}`;
|
||||||
|
|
||||||
|
//восстановим
|
||||||
|
try {
|
||||||
|
if (!await fs.pathExists(filePath)) {
|
||||||
|
if (req.path.indexOf('/tmp/') === 0) {
|
||||||
|
await readerWorker.restoreRemoteFile(req.path, '/tmp');
|
||||||
|
} else if (req.path.indexOf('/upload/') === 0) {
|
||||||
|
await readerWorker.restoreRemoteFile(req.path, '/upload');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, `static::restoreRemoteFile ${req.path} > ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmpDir = `${config.publicDir}/tmp`;
|
||||||
|
app.use(express.static(config.publicDir, {
|
||||||
|
maxAge: '30d',
|
||||||
|
|
||||||
|
setHeaders: (res, filePath) => {
|
||||||
|
if (path.dirname(filePath) == tmpDir) {
|
||||||
|
res.set('Content-Type', 'application/xml');
|
||||||
|
res.set('Content-Encoding', 'gzip');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initRoutes
|
initRoutes
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user