Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
@@ -29,7 +29,8 @@ async function main() {
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
@@ -46,7 +47,8 @@ async function main() {
|
||||
// Скачиваем ipfs
|
||||
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}`);
|
||||
|
||||
//распаковываем
|
||||
|
||||
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
@@ -29,7 +29,8 @@ async function main() {
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
@@ -46,7 +47,8 @@ async function main() {
|
||||
// Скачиваем ipfs
|
||||
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}`);
|
||||
|
||||
//распаковываем
|
||||
|
||||
@@ -9,7 +9,7 @@ class Misc {
|
||||
async loadConfig() {
|
||||
|
||||
const query = {params: [
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
|
||||
]};
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import * as utils from '../share/utils';
|
||||
import * as cryptoUtils from '../share/cryptoUtils';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
@@ -119,32 +120,7 @@ class Reader {
|
||||
estSize = response.headers['content-length'];
|
||||
}
|
||||
} 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;
|
||||
@@ -174,11 +150,10 @@ class Reader {
|
||||
return await axios.get(url, options);
|
||||
}
|
||||
|
||||
async uploadFile(file, maxUploadFileSize, callback) {
|
||||
if (!maxUploadFileSize)
|
||||
maxUploadFileSize = 10*1024*1024;
|
||||
async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
|
||||
if (file.size > maxUploadFileSize)
|
||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
|
||||
@@ -225,6 +200,35 @@ class Reader {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new Reader();
|
||||
@@ -11,7 +11,7 @@
|
||||
Открыть выбранную закладку
|
||||
</q-tooltip>
|
||||
</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>
|
||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
|
||||
</template>
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
</template>
|
||||
|
||||
<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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||
<q-icon name="la la-plus" size="16px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||
</span>
|
||||
<span class="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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||
<span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||
<q-icon name="la la-question-circle" size="16px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||
</span>
|
||||
@@ -32,7 +32,7 @@
|
||||
:options="rootLinkOptions"
|
||||
style="width: 230px"
|
||||
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"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -61,7 +61,7 @@
|
||||
:options="selectedLinkOptions"
|
||||
style="width: 50px"
|
||||
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"
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
@@ -73,7 +73,7 @@
|
||||
ref="input"
|
||||
v-model="bookUrl"
|
||||
class="col q-mr-sm"
|
||||
rounded outlined dense
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
|
||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||
@@ -99,7 +99,7 @@
|
||||
</template>
|
||||
</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%">
|
||||
Открыть в читалке
|
||||
@@ -894,14 +894,15 @@ export default vueComponent(ExternalLibs);
|
||||
background-color: #A0A0A0;
|
||||
}
|
||||
|
||||
.full-screen-button {
|
||||
.header-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.full-screen-button:hover {
|
||||
background-color: #69C05F;
|
||||
.header-button:hover {
|
||||
color: white;
|
||||
background-color: #39902F;
|
||||
}
|
||||
|
||||
.transparent-layout {
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
|
||||
<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 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)">
|
||||
<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 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 class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div :style="item.indentStyle"></div>
|
||||
@@ -42,8 +42,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.expanded" :ref="`subitem${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-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
|
||||
<div
|
||||
v-for="subitem in item.list"
|
||||
:ref="`subitem${subitem.key}`"
|
||||
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
|
||||
>
|
||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||
<div class="no-expand-button"></div>
|
||||
<div :style="subitem.indentStyle"></div>
|
||||
@@ -61,10 +65,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="selectedTab == 'images'" class="tab-panel">
|
||||
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
|
||||
<div>
|
||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||
<div 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="image-thumb-box row justify-center items-center">
|
||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
||||
@@ -124,7 +128,10 @@ const componentOptions = {
|
||||
watch: {
|
||||
bookPos() {
|
||||
this.updateBookPosSelection();
|
||||
}
|
||||
},
|
||||
selectedTab() {
|
||||
this.updateBookPosScrollTop();
|
||||
},
|
||||
},
|
||||
};
|
||||
class ContentsPage {
|
||||
@@ -282,31 +289,30 @@ class ContentsPage {
|
||||
if (!this.isVisible)
|
||||
return;
|
||||
|
||||
await utils.sleep(50);
|
||||
await this.$nextTick();
|
||||
const bp = this.bookPos;
|
||||
|
||||
for (let i = 0; i < this.contents.length; i++) {
|
||||
const item = this.contents[i];
|
||||
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= item.offset && bp < nextOffset) {
|
||||
item.isBookPos = true;
|
||||
} else if (item.isBookPos) {
|
||||
item.isBookPos = false;
|
||||
}
|
||||
|
||||
for (let j = 0; j < item.list.length; j++) {
|
||||
const subitem = item.list[j];
|
||||
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||
|
||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||
subitem.isBookPos = true;
|
||||
this.contents[i] = Object.assign(item, {list: item.list});
|
||||
this.updateBookPosScrollTop('contents', item, subitem, j);
|
||||
} else if (subitem.isBookPos) {
|
||||
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++) {
|
||||
@@ -314,11 +320,96 @@ class ContentsPage {
|
||||
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= img.offset && bp < nextOffset) {
|
||||
this.images[i] = Object.assign(img, {isBookPos: true});
|
||||
this.images[i].isBookPos = true;
|
||||
} 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) {
|
||||
@@ -326,17 +417,17 @@ class ContentsPage {
|
||||
const expanded = !item.expanded;
|
||||
|
||||
if (!expanded) {
|
||||
const subitems = this.$refs[`subitem${key}`];
|
||||
subitems.style.height = '0';
|
||||
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||
subdiv.style.height = '0';
|
||||
await utils.sleep(200);
|
||||
}
|
||||
|
||||
this.contents[key] = Object.assign({}, item, {expanded});
|
||||
this.contents[key].expanded = expanded;
|
||||
|
||||
if (expanded) {
|
||||
await this.$nextTick();
|
||||
const subitems = this.$refs[`subitem${key}`];
|
||||
subitems.style.height = subitems.scrollHeight + 'px';
|
||||
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||
subdiv.style.height = subdiv.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,20 @@
|
||||
</template>
|
||||
|
||||
<div class="col column" style="min-width: 600px">
|
||||
<q-btn-toggle
|
||||
v-model="selectedTab"
|
||||
toggle-color="primary"
|
||||
no-caps unelevated
|
||||
:options="buttons"
|
||||
/>
|
||||
<div class="separator"></div>
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
inline-label
|
||||
class="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>
|
||||
<component :is="activePage" ref="page" class="col"></component>
|
||||
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
</template>
|
||||
</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>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||
@@ -131,6 +136,10 @@ class LoaderPage {
|
||||
return this.$store.state.config.version;
|
||||
}
|
||||
|
||||
get acceptFileExt() {
|
||||
return this.$store.state.config.acceptFileExt;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<q-circular-progress
|
||||
show-value
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
@load-file="loadFile"
|
||||
@book-pos-changed="bookPosChanged"
|
||||
@do-action="doAction"
|
||||
@hide-tool-bar="hideToolBar"
|
||||
></component>
|
||||
</keep-alive>
|
||||
|
||||
@@ -193,6 +194,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
||||
|
||||
import bookManager from './share/bookManager';
|
||||
import wallpaperStorage from './share/wallpaperStorage';
|
||||
import coversStorage from './share/coversStorage';
|
||||
import dynamicCss from '../../share/dynamicCss';
|
||||
|
||||
import rstore from '../../store/modules/reader';
|
||||
@@ -201,6 +203,7 @@ import miscApi from '../../api/misc';
|
||||
|
||||
import {versionHistory} from './versionHistory';
|
||||
import * as utils from '../../share/utils';
|
||||
import LockQueue from '../../share/LockQueue';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
@@ -313,6 +316,8 @@ class Reader {
|
||||
this.reader = this.$store.state.reader;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
this.lock = new LockQueue(100);
|
||||
|
||||
this.$root.addEventHook('key', this.keyHook);
|
||||
|
||||
this.lastActivePage = false;
|
||||
@@ -345,6 +350,13 @@ class Reader {
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
}, 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);
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||
});
|
||||
@@ -355,6 +367,8 @@ class Reader {
|
||||
mounted() {
|
||||
(async() => {
|
||||
await wallpaperStorage.init();
|
||||
await coversStorage.init();
|
||||
|
||||
await bookManager.init(this.settings);
|
||||
bookManager.addEventListener(this.bookManagerEvent);
|
||||
|
||||
@@ -402,6 +416,7 @@ class Reader {
|
||||
this.clickControlActive = this.clickControl;
|
||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||
this.splitToPara = settings.splitToPara;
|
||||
@@ -438,22 +453,47 @@ class Reader {
|
||||
|
||||
//wallpaper css
|
||||
async loadWallpapers() {
|
||||
const wallpaperDataLength = await wallpaperStorage.getLength();
|
||||
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
|
||||
this.wallpaperDataLength = wallpaperDataLength;
|
||||
if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
|
||||
this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
|
||||
|
||||
let newCss = '';
|
||||
let updated = false;
|
||||
const wallpaperExists = new Set();
|
||||
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) {
|
||||
//здесь будем восстанавливать данные с сервера
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -662,6 +702,10 @@ class Reader {
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
|
||||
hideToolBar(event) {
|
||||
this.debouncedHideToolBar(event);
|
||||
}
|
||||
|
||||
fullScreenToggle() {
|
||||
this.fullScreenActive = !this.fullScreenActive;
|
||||
if (this.fullScreenActive) {
|
||||
@@ -897,7 +941,7 @@ class Reader {
|
||||
|
||||
refreshBook() {
|
||||
const mrb = this.mostRecentBook();
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||
this.loadBook(Object.assign({}, mrb, {force: true}));
|
||||
}
|
||||
|
||||
undoAction() {
|
||||
@@ -982,7 +1026,6 @@ class Reader {
|
||||
classResult = classDisabled;
|
||||
break;
|
||||
case 'refresh':
|
||||
case 'recentBooks':
|
||||
if (!this.mostRecentBookReactive)
|
||||
classResult = classDisabled;
|
||||
break;
|
||||
@@ -1051,7 +1094,7 @@ class Reader {
|
||||
return result;
|
||||
}
|
||||
|
||||
async loadBook(opts) {
|
||||
async _loadBook(opts) {
|
||||
if (!opts || !opts.url) {
|
||||
this.mostRecentBook();
|
||||
return;
|
||||
@@ -1061,10 +1104,6 @@ class Reader {
|
||||
|
||||
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) &&
|
||||
(url.indexOf('disk://') != 0))
|
||||
url = 'http://' + url;
|
||||
@@ -1091,33 +1130,37 @@ class Reader {
|
||||
progress.show();
|
||||
progress.setState({state: 'parse'});
|
||||
|
||||
// есть ли среди недавних
|
||||
const key = bookManager.keyFromUrl(url);
|
||||
let wasOpened = await bookManager.getRecentBook({key});
|
||||
wasOpened = (wasOpened ? wasOpened : {});
|
||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
||||
// есть ли среди загруженных
|
||||
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
|
||||
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
|
||||
|
||||
wasOpened = Object.assign(wasOpened, {
|
||||
url: (opts.url !== undefined ? opts.url : wasOpened.url),
|
||||
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;
|
||||
|
||||
if (!opts.force) {
|
||||
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
||||
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
|
||||
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
|
||||
progress.setState({progress: prog});
|
||||
});
|
||||
|
||||
// если есть в локальном кэше
|
||||
if (bookParsed) {
|
||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
|
||||
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
|
||||
this.mostRecentBook();
|
||||
this.addAction(bookPos);
|
||||
this.addAction(wasOpened.bookPos);
|
||||
this.loaderActive = false;
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.blinkCachedLoadMessage();
|
||||
|
||||
this.checkBookPosPercent();
|
||||
await this.activateClickMapPage();
|
||||
this.activateClickMapPage();//no await
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1131,7 +1174,7 @@ class Reader {
|
||||
});
|
||||
book = Object.assign({}, wasOpened, {data: resp.data});
|
||||
} catch (e) {
|
||||
//молчим
|
||||
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1142,7 +1185,7 @@ class Reader {
|
||||
if (!book) {
|
||||
book = await readerApi.loadBook({
|
||||
url,
|
||||
uploadFileName,
|
||||
uploadFileName: wasOpened.uploadFileName,
|
||||
enableSitesFilter: this.enableSitesFilter,
|
||||
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||
isText: (this.splitToPara ? true : false),
|
||||
@@ -1159,14 +1202,44 @@ class Reader {
|
||||
|
||||
// добавляем в bookManager
|
||||
progress.setState({state: 'parse', step: 5});
|
||||
|
||||
const addedBook = await bookManager.addBook(book, (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));
|
||||
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
|
||||
this.mostRecentBook();
|
||||
this.addAction(bookPos);
|
||||
this.addAction(wasOpened.bookPos);
|
||||
this.updateRoute(true);
|
||||
|
||||
this.loaderActive = false;
|
||||
@@ -1177,11 +1250,11 @@ class Reader {
|
||||
this.stopBlink = true;
|
||||
|
||||
this.checkBookPosPercent();
|
||||
await this.activateClickMapPage();
|
||||
this.activateClickMapPage();//no await
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
if (!this.showHelpOnErrorIfNeeded(e.message)) {
|
||||
if (!this.showHelpOnErrorIfNeeded(url)) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
} finally {
|
||||
@@ -1189,7 +1262,16 @@ class Reader {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
await this.$nextTick();
|
||||
@@ -1205,7 +1287,7 @@ class Reader {
|
||||
|
||||
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) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
@@ -1213,6 +1295,15 @@ class Reader {
|
||||
}
|
||||
}
|
||||
|
||||
async loadFile(opts) {
|
||||
await this.lock.get();
|
||||
try {
|
||||
await this._loadFile(opts);
|
||||
} finally {
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
blinkCachedLoadMessage() {
|
||||
if (!this.blinkCachedLoad)
|
||||
return;
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
|
||||
<!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
|
||||
Помочь проекту
|
||||
</q-btn-->
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,10 @@
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
|
||||
<div v-show="!initStep" class="input">
|
||||
<!--input ref="input"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/-->
|
||||
<q-input ref="input" v-model="needle"
|
||||
<q-input
|
||||
ref="input" v-model="needle"
|
||||
class="col" outlined dense
|
||||
placeholder="что ищем"
|
||||
placeholder="Найти"
|
||||
@keydown="inputKeyDown"
|
||||
/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
|
||||
@@ -108,7 +106,7 @@ class SearchPage {
|
||||
this.parsed = parsed;
|
||||
}
|
||||
|
||||
this.header = 'Найти';
|
||||
this.header = 'Поиск в тексте';
|
||||
await this.$nextTick();
|
||||
this.$refs.input.focus();
|
||||
this.$refs.input.select();
|
||||
|
||||
@@ -728,10 +728,10 @@ class ServerStorage {
|
||||
const ids = id.split('.');
|
||||
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||
items[utils.fromBase58(ids[1])] = decoded;
|
||||
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.items = items;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<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>
|
||||
|
||||
<div id="set-position-slider" class="slider q-px-md">
|
||||
<q-slider
|
||||
v-model="sliderValue"
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
<div class="col column justify-center">
|
||||
<div id="set-position-slider" class="slider q-px-md column justify-center">
|
||||
<q-slider
|
||||
v-model="sliderValue"
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
@@ -76,7 +78,8 @@ export default vueComponent(SetPositionPage);
|
||||
|
||||
<style scoped>
|
||||
.slider {
|
||||
margin: 20px;
|
||||
margin: 0 20px 0 20px;
|
||||
height: 35px;
|
||||
background-color: #efefef;
|
||||
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>
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<Window ref="window" height="95%" width="600px" @close="close">
|
||||
<Window ref="window" width="600px" @close="close">
|
||||
<template #header>
|
||||
Настройки
|
||||
</template>
|
||||
|
||||
<div class="col row">
|
||||
<a ref="download" style="display: none;" target="_blank"></a>
|
||||
|
||||
<div class="full-height">
|
||||
<q-tabs
|
||||
ref="tabs"
|
||||
@@ -24,7 +26,7 @@
|
||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
||||
<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="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="pagemove" icon="la la-school" label="Листание" />
|
||||
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||
@@ -82,8 +84,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Кнопки ---------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
|
||||
@@include('./ButtonsTab.inc');
|
||||
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
|
||||
@@include('./ToolBarTab.inc');
|
||||
</div>
|
||||
<!-- Управление ------------------------------------------------------------------>
|
||||
<div v-if="selectedTab == 'keys'" class="fit column">
|
||||
@@ -124,6 +126,7 @@ import NumInput from '../../share/NumInput.vue';
|
||||
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
||||
import wallpaperStorage from '../share/wallpaperStorage';
|
||||
|
||||
import readerApi from '../../../api/reader';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
import defPalette from './defPalette';
|
||||
|
||||
@@ -636,8 +639,17 @@ class SettingsPage {
|
||||
|
||||
if (index < 0)
|
||||
newUserWallpapers.push({label, cssClass});
|
||||
if (!wallpaperStorage.keyExists(cssClass))
|
||||
if (!wallpaperStorage.keyExists(cssClass)) {
|
||||
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.wallpaper = cssClass;
|
||||
@@ -664,6 +676,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) {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
@@ -702,11 +735,11 @@ export default vueComponent(SettingsPage);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label-1, .label-7 {
|
||||
.label-1, .label-3, .label-7 {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.label-2, .label-3, .label-4, .label-5 {
|
||||
.label-2, .label-4, .label-5 {
|
||||
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>
|
||||
@@ -13,7 +13,7 @@
|
||||
ref="input"
|
||||
v-model="search"
|
||||
class="q-ml-sm col"
|
||||
outlined dense rounded
|
||||
outlined dense
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
@click.stop
|
||||
|
||||
@@ -102,6 +102,11 @@
|
||||
Удалить выбранные обои
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Скачать выбранные обои
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,29 +6,32 @@
|
||||
</div>
|
||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<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 ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<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 v-show="showStatusBar" ref="statusBar" class="layout">
|
||||
<div v-html="statusBar"></div>
|
||||
</div>
|
||||
<div v-show="clickControl" ref="layoutEvents" class="layout events"
|
||||
<div
|
||||
v-show="clickControl" ref="layoutEvents" class="layout events"
|
||||
oncontextmenu="return false;"
|
||||
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||
@wheel.prevent.stop="onMouseWheel"
|
||||
@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"
|
||||
v-html="statusBarClickable"
|
||||
></div>
|
||||
</div>
|
||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
||||
<div
|
||||
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
||||
@mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"
|
||||
v-html="statusBarClickable"
|
||||
@@ -46,6 +49,7 @@ import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import {loadCSS} from 'fg-loadcss';
|
||||
import _ from 'lodash';
|
||||
import he from 'he';
|
||||
|
||||
import './TextPage.css';
|
||||
|
||||
@@ -62,7 +66,14 @@ const componentOptions = {
|
||||
watch: {
|
||||
bookPos: function() {
|
||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||
|
||||
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() {
|
||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||
@@ -95,6 +106,8 @@ class TextPage {
|
||||
lastBook = null;
|
||||
bookPos = 0;
|
||||
bookPosSeen = null;
|
||||
prevBookPos = 0;
|
||||
userBookPosChange = false;
|
||||
|
||||
fontStyle = null;
|
||||
fontSize = null;
|
||||
@@ -151,7 +164,7 @@ class TextPage {
|
||||
|
||||
this.$root.addEventHook('resize', async() => {
|
||||
this.$nextTick(this.onResize);
|
||||
await utils.sleep(500);
|
||||
await utils.sleep(200);
|
||||
this.$nextTick(this.onResize);
|
||||
});
|
||||
}
|
||||
@@ -495,12 +508,25 @@ class TextPage {
|
||||
}
|
||||
|
||||
async onResize() {
|
||||
if (this.resizing)
|
||||
return;
|
||||
|
||||
this.resizing = true;
|
||||
try {
|
||||
const scrolled = this.doingScrolling;
|
||||
if (scrolled)
|
||||
await this.stopTextScrolling();
|
||||
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
|
||||
if (scrolled)
|
||||
this.startTextScrolling();
|
||||
} catch (e) {
|
||||
//
|
||||
} finally {
|
||||
this.resizing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,7 +674,7 @@ class TextPage {
|
||||
}
|
||||
|
||||
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
|
||||
this.doEnd(true);
|
||||
this.doEnd(true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -671,7 +697,7 @@ class TextPage {
|
||||
this.debouncedDrawPageDividerAndOrnament();
|
||||
|
||||
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
||||
this.doEnd(true);
|
||||
this.doEnd(true, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -907,12 +933,14 @@ class TextPage {
|
||||
|
||||
doDown() {
|
||||
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesDown[1].begin;
|
||||
}
|
||||
}
|
||||
|
||||
doUp() {
|
||||
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesUp[1].begin;
|
||||
}
|
||||
}
|
||||
@@ -925,6 +953,7 @@ class TextPage {
|
||||
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = true;
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesDown[i].begin;
|
||||
} else
|
||||
this.doEnd();
|
||||
@@ -940,6 +969,7 @@ class TextPage {
|
||||
if (i >= 0 && this.linesUp.length > i) {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = false;
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesUp[i].begin;
|
||||
}
|
||||
}
|
||||
@@ -948,10 +978,11 @@ class TextPage {
|
||||
doHome() {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = false;
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = 0;
|
||||
}
|
||||
|
||||
doEnd(noAni) {
|
||||
doEnd(noAni, isUser = true) {
|
||||
if (this.parsed.para.length && this.pageLineCount > 0) {
|
||||
let i = this.parsed.para.length - 1;
|
||||
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
|
||||
@@ -962,6 +993,7 @@ class TextPage {
|
||||
if (!noAni)
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = true;
|
||||
this.userBookPosChange = isUser;
|
||||
this.bookPos = lines[i].begin;
|
||||
}
|
||||
}
|
||||
@@ -1201,8 +1233,54 @@ class TextPage {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
const maxParaLength = 10000;
|
||||
const maxParaTextLength = 10000;
|
||||
|
||||
// defaults
|
||||
const defaultSettings = {
|
||||
@@ -83,6 +85,7 @@ export default class BookParser {
|
||||
let binaryId = '';
|
||||
let binaryType = '';
|
||||
let dimPromises = [];
|
||||
this.coverPageId = '';
|
||||
|
||||
//оглавление
|
||||
this.contents = [];
|
||||
@@ -226,13 +229,26 @@ export default class BookParser {
|
||||
paraOffset += len;
|
||||
};
|
||||
|
||||
const growParagraph = (text, len) => {
|
||||
const growParagraph = (text, len, textRaw) => {
|
||||
//начальный параграф
|
||||
if (paraIndex < 0) {
|
||||
newParagraph();
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
//ограничение на размер куска текста в параграфе
|
||||
if (textRaw && textRaw.length > maxParaTextLength) {
|
||||
while (textRaw.length > 0) {
|
||||
const textPart = textRaw.substring(0, maxParaTextLength);
|
||||
textRaw = textRaw.substring(maxParaTextLength);
|
||||
|
||||
newParagraph();
|
||||
growParagraph(textPart, textPart.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += text;
|
||||
} else if (inTitle) {
|
||||
@@ -240,6 +256,14 @@ export default class BookParser {
|
||||
}
|
||||
|
||||
const p = para[paraIndex];
|
||||
|
||||
//ограничение на размер параграфа
|
||||
if (p.length > maxParaLength) {
|
||||
newParagraph();
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
p.length += len;
|
||||
p.text += text;
|
||||
paraOffset += len;
|
||||
@@ -266,7 +290,7 @@ export default class BookParser {
|
||||
const href = attrs.href.value;
|
||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||
const {id, local} = this.imageHrefToId(href);
|
||||
if (href[0] == '#') {//local
|
||||
if (local) {//local
|
||||
imageNum++;
|
||||
|
||||
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||
@@ -278,6 +302,11 @@ export default class BookParser {
|
||||
|
||||
if (inPara && this.sets.showInlineImagesInCenter)
|
||||
newParagraph();
|
||||
|
||||
//coverpage
|
||||
if (path == '/fictionbook/description/title-info/coverpage/image') {
|
||||
this.coverPageId = id;
|
||||
}
|
||||
} else {//external
|
||||
imageNum++;
|
||||
|
||||
@@ -536,7 +565,7 @@ export default class BookParser {
|
||||
tClose += (center ? '</center>' : '');
|
||||
|
||||
if (text != ' ')
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
|
||||
else
|
||||
growParagraph(' ', 1);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import localForage from 'localforage';
|
||||
import path from 'path-browserify';
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
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 maxRecentLength = 5000;
|
||||
|
||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||
const bmMetaStore = localForage.createInstance({
|
||||
@@ -17,9 +21,6 @@ const bmDataStore = localForage.createInstance({
|
||||
});
|
||||
|
||||
//список недавно открытых книг
|
||||
const bmRecentStoreOld = localForage.createInstance({
|
||||
name: 'bmRecentStore'
|
||||
});
|
||||
const bmRecentStoreNew = localForage.createInstance({
|
||||
name: 'bmRecentStoreNew'
|
||||
});
|
||||
@@ -39,7 +40,7 @@ class BookManager {
|
||||
|
||||
this.saveRecentItem = _.debounce(() => {
|
||||
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);
|
||||
}, 200, {maxWait: 300});
|
||||
|
||||
@@ -54,6 +55,9 @@ class BookManager {
|
||||
if (this.recentItem)
|
||||
this.recent[this.recentItem.key] = this.recentItem;
|
||||
|
||||
//конвертируем в новые ключи
|
||||
await this.convertRecent();
|
||||
|
||||
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||
if (this.recentLastKey) {
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||
@@ -70,6 +74,40 @@ class BookManager {
|
||||
this.loadStored();//no await
|
||||
}
|
||||
|
||||
//TODO: убрать в 2025г
|
||||
async convertRecent() {
|
||||
const converted = await bmRecentStoreNew.getItem('recent-converted');
|
||||
|
||||
if (converted)
|
||||
return;
|
||||
|
||||
const newRecent = {};
|
||||
for (const book of Object.values(this.recent)) {
|
||||
|
||||
if (!book.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newKey = this.keyFromPath(book.path);
|
||||
|
||||
newRecent[newKey] = _.cloneDeep(book);
|
||||
newRecent[newKey].key = newKey;
|
||||
if (!newRecent[newKey].loadTime)
|
||||
newRecent[newKey].loadTime = newRecent[newKey].addTime;
|
||||
}
|
||||
|
||||
this.recent = newRecent;
|
||||
|
||||
//console.log(converted);
|
||||
(async() => {
|
||||
await utils.sleep(3000);
|
||||
this.saveRecent();
|
||||
this.emit('recent-changed');
|
||||
this.emit('set-recent');
|
||||
await bmRecentStoreNew.setItem('recent-converted', true);
|
||||
})();
|
||||
}
|
||||
|
||||
//Ленивая асинхронная загрузка bmMetaStore
|
||||
async loadStored() {
|
||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||
@@ -196,8 +234,8 @@ class BookManager {
|
||||
|
||||
async addBook(newBook, callback) {
|
||||
let meta = {url: newBook.url, path: newBook.path};
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.addTime = Date.now();
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
meta.addTime = Date.now();//время добавления в кеш
|
||||
|
||||
const cb = (perc) => {
|
||||
const p = Math.round(30*perc/100);
|
||||
@@ -232,10 +270,10 @@ class BookManager {
|
||||
async hasBookParsed(meta) {
|
||||
if (!this.books)
|
||||
return false;
|
||||
if (!meta.url)
|
||||
if (!meta.path)
|
||||
return false;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
|
||||
let book = this.books[meta.key];
|
||||
|
||||
@@ -250,8 +288,12 @@ class BookManager {
|
||||
|
||||
async getBook(meta, callback) {
|
||||
let result = undefined;
|
||||
|
||||
if (!meta.path)
|
||||
return;
|
||||
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
|
||||
result = this.books[meta.key];
|
||||
|
||||
@@ -261,11 +303,6 @@ class BookManager {
|
||||
this.books[meta.key] = result;
|
||||
}
|
||||
|
||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
||||
if (meta.path && result && meta.path != result.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && !result.parsed) {
|
||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||
callback(5);
|
||||
@@ -310,9 +347,38 @@ class BookManager {
|
||||
const parsed = new BookParser(this.settings);
|
||||
|
||||
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, {
|
||||
length: data.length,
|
||||
textLength: parsed.textLength,
|
||||
coverPageUrl,
|
||||
parsed
|
||||
});
|
||||
|
||||
@@ -325,10 +391,20 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
keyFromUrl(url) {
|
||||
/*keyFromUrl(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 --------------------------------------------------------------
|
||||
async recentSetItem(item = null, skipCheck = false) {
|
||||
const rev = await bmRecentStoreNew.getItem('rev');
|
||||
@@ -369,7 +445,10 @@ class BookManager {
|
||||
|
||||
async setRecentBook(value) {
|
||||
let result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();
|
||||
result.touchTime = Date.now();//время последнего чтения
|
||||
if (!result.loadTime)
|
||||
result.loadTime = Date.now();//время загрузки файла
|
||||
|
||||
result.deleted = 0;
|
||||
|
||||
if (this.recent[result.key]) {
|
||||
@@ -385,9 +464,9 @@ class BookManager {
|
||||
return this.recent[value.key];
|
||||
}
|
||||
|
||||
async delRecentBook(value) {
|
||||
async delRecentBook(value, delFlag = 1) {
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 1;
|
||||
item.deleted = delFlag;
|
||||
|
||||
if (this.recentLastKey == value.key) {
|
||||
await this.recentSetLastKey(null);
|
||||
@@ -397,11 +476,18 @@ class BookManager {
|
||||
this.emit('recent-deleted', value.key);
|
||||
}
|
||||
|
||||
async restoreRecentBook(value) {
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 0;
|
||||
|
||||
await this.recentSetItem(item);
|
||||
}
|
||||
|
||||
async cleanRecentBooks() {
|
||||
const sorted = this.getSortedRecent();
|
||||
|
||||
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];
|
||||
isDel = true;
|
||||
}
|
||||
@@ -421,7 +507,7 @@ class BookManager {
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
for (let key in this.recent) {
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.touchTime > max) {
|
||||
max = book.touchTime;
|
||||
@@ -452,6 +538,43 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
findRecentByUrlAndPath(url, bookPath) {
|
||||
if (bookPath) {
|
||||
const key = this.keyFromPath(bookPath);
|
||||
const book = this.recent[key];
|
||||
if (book && !book.deleted)
|
||||
return book;
|
||||
}
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.url == url && book.loadTime > max) {
|
||||
max = book.loadTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
findRecentBySameBookKey(sameKey) {
|
||||
let max = 0;
|
||||
let result = null;
|
||||
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
|
||||
max = book.loadTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async setRecent(value) {
|
||||
const mergedRecent = _.cloneDeep(this.recent);
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
async getKeys() {
|
||||
return await wpStore.keys();
|
||||
}
|
||||
|
||||
keyExists(key) {//не асинхронная
|
||||
return this.cachedKeys.includes(key);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,54 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
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',
|
||||
|
||||
@@ -55,6 +55,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Нет
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||
Да
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
@@ -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) {
|
||||
return new Promise((resolve) => {
|
||||
this.enableValidator = false;
|
||||
|
||||
@@ -153,7 +153,7 @@ export default vueComponent(Window);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(to bottom right, green, #59B04F);
|
||||
background: linear-gradient(to bottom right, #007000, #59B04F);
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
}
|
||||
@@ -161,8 +161,8 @@ export default vueComponent(Window);
|
||||
.header-text {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: yellow;
|
||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
||||
color: #FFFFA0;
|
||||
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -174,7 +174,8 @@ export default vueComponent(Window);
|
||||
}
|
||||
|
||||
.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 {QChip} from 'quasar/src/components/chip';
|
||||
import {QTree} from 'quasar/src/components/tree';
|
||||
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
|
||||
|
||||
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||
|
||||
const components = {
|
||||
@@ -62,6 +64,7 @@ const components = {
|
||||
QChip,
|
||||
QTree,
|
||||
//QExpansionItem,
|
||||
QVirtualScroll,
|
||||
};
|
||||
|
||||
//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;
|
||||
@@ -90,7 +90,7 @@ export function toBase58(data) {
|
||||
}
|
||||
|
||||
export function fromBase58(data) {
|
||||
return bs58.decode(data);
|
||||
return Buffer.from(bs58.decode(data));
|
||||
}
|
||||
|
||||
//base-x слишком тормозит, используем sjcl
|
||||
@@ -107,6 +107,10 @@ export function fromBase64(data) {
|
||||
));
|
||||
}
|
||||
|
||||
export function hasProp(obj, prop) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
}
|
||||
|
||||
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
const {
|
||||
exclude = [],
|
||||
@@ -126,7 +130,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
for (const key of Object.keys(oldObj)) {
|
||||
const kp = `${keyPath}${key}`;
|
||||
|
||||
if (newObj.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||
if (ex.has(kp))
|
||||
continue;
|
||||
|
||||
@@ -149,7 +153,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
if (exAdd.has(kp))
|
||||
continue;
|
||||
|
||||
if (!oldObj.hasOwnProperty(key)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
|
||||
result.add[key] = _.cloneDeep(newObj[key]);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +217,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
|
||||
|
||||
const change = diff.change;
|
||||
for (const key of Object.keys(change)) {
|
||||
if (result.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||
if (_.isObject(change[key])) {
|
||||
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||
} else {
|
||||
@@ -359,4 +363,50 @@ export function getBookTitle(fb2) {
|
||||
]).join(' - ');
|
||||
|
||||
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); });
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const readerActions = {
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Сетевая библиотека',
|
||||
'recentBooks': 'Открыть недавние',
|
||||
'recentBooks': 'Показать загруженные',
|
||||
'switchToolbar': 'Показать/скрыть панель управления',
|
||||
'donate': '',
|
||||
'bookBegin': 'В начало книги',
|
||||
@@ -70,7 +70,7 @@ const hotKeys = [
|
||||
{name: 'scrolling', codes: ['Z']},
|
||||
{name: 'setPosition', codes: ['P']},
|
||||
{name: 'search', codes: ['Ctrl+F']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'copyText', codes: ['Ctrl+Space']},
|
||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||
{name: 'refresh', codes: ['R']},
|
||||
{name: 'contents', codes: ['C']},
|
||||
@@ -185,8 +185,14 @@ const settingDefaults = {
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
toolBarHideOnScroll: true,
|
||||
userHotKeys: {},
|
||||
userWallpapers: [],
|
||||
|
||||
recentShowSameBook: false,
|
||||
recentSortMethod: '',
|
||||
|
||||
needUpdateSettingsView: 0,
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
@@ -222,9 +228,6 @@ const libsDefaults = {
|
||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||
]},
|
||||
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
||||
{l: 'https://flibs.in', c: 'Flibs'},
|
||||
]},
|
||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||
]},
|
||||
|
||||
@@ -6,6 +6,7 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
server_name beta.liberama.top;
|
||||
set $liberama http://127.0.0.1:34082;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -15,15 +16,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -32,6 +38,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -50,6 +61,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name b.beta.liberama.top;
|
||||
set $liberama http://127.0.0.1:34082;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -59,15 +71,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -76,6 +93,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
|
||||
@@ -6,6 +6,7 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
server_name beta.omnireader.ru;
|
||||
set $liberama http://127.0.0.1:34081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -15,15 +16,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -32,6 +38,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name beta.omnireader.ru;
|
||||
set $liberama http://127.0.0.1:34081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -10,15 +11,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -27,6 +33,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
|
||||
@@ -17,8 +17,9 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
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;
|
||||
|
||||
gzip on;
|
||||
@@ -26,12 +27,16 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
@@ -44,6 +49,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -62,8 +72,9 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
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;
|
||||
|
||||
gzip on;
|
||||
@@ -71,15 +82,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -88,6 +104,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -140,5 +161,6 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_pass http://fantasy-worlds.org;
|
||||
proxy_hide_header x-frame-options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
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
|
||||
echo "Process 'liberama' already running"
|
||||
fi
|
||||
|
||||
@@ -6,8 +6,9 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
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;
|
||||
|
||||
gzip on;
|
||||
@@ -15,12 +16,16 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
@@ -33,6 +38,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -52,7 +62,7 @@ server {
|
||||
listen 80;
|
||||
server_name old.omnireader.ru;
|
||||
|
||||
client_max_body_size 50m;
|
||||
client_max_body_size 100m;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1024;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name omnireader.ru;
|
||||
set $liberama http://127.0.0.1:44081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -10,12 +11,16 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
@@ -27,6 +32,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/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
|
||||
|
||||
11271
package-lock.json
generated
11271
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.11.5",
|
||||
"version": "0.11.8",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
@@ -28,17 +28,17 @@
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@vue/compiler-sfc": "^3.2.22",
|
||||
"babel-loader": "^8.2.3",
|
||||
"copy-webpack-plugin": "^9.1.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"css-minimizer-webpack-plugin": "^3.1.3",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-vue": "^9.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"mini-css-extract-plugin": "^2.4.4",
|
||||
"pkg": "^5.5.1",
|
||||
"terser-webpack-plugin": "^5.2.5",
|
||||
"vue-eslint-parser": "^8.0.1",
|
||||
"vue-loader": "^16.8.3",
|
||||
"vue-eslint-parser": "^9.0.3",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"webpack": "^5.64.1",
|
||||
"webpack-cli": "^4.9.1",
|
||||
@@ -50,25 +50,24 @@
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.12.0",
|
||||
"@vue/compat": "^3.2.21",
|
||||
"axios": "^0.24.0",
|
||||
"base-x": "^3.0.9",
|
||||
"axios": "^0.27.2",
|
||||
"base-x": "^4.0.0",
|
||||
"chardet": "^1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.17.1",
|
||||
"fg-loadcss": "^3.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"got": "^11.8.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"he": "^1.2.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jembadb": "^2.3.0",
|
||||
"jembadb": "^3.0.8",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.5",
|
||||
"multer": "^1.4.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pako": "^2.0.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pidusage": "^3.0.0",
|
||||
"quasar": "^2.3.2",
|
||||
"quasar": "^2.7.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"sanitize-html": "^2.5.3",
|
||||
"sjcl": "^1.0.8",
|
||||
@@ -77,8 +76,8 @@
|
||||
"sqlite3": "^5.0.2",
|
||||
"tar-fs": "^2.1.1",
|
||||
"unbzip2-stream": "^1.4.3",
|
||||
"vue": "^3.2.22",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue": "^3.2.37",
|
||||
"vue-router": "^4.1.1",
|
||||
"vuex": "^4.0.2",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
"webdav": "^4.7.0",
|
||||
|
||||
@@ -22,7 +22,8 @@ module.exports = {
|
||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||
|
||||
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', 'branch'],
|
||||
|
||||
db: [
|
||||
{
|
||||
@@ -48,14 +49,14 @@ module.exports = {
|
||||
servers: [
|
||||
{
|
||||
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',
|
||||
port: '33080',
|
||||
},
|
||||
],
|
||||
|
||||
remoteWebDavStorage: false,
|
||||
/*
|
||||
remoteWebDavStorage: false,
|
||||
remoteWebDavStorage: {
|
||||
url: '127.0.0.1:1900',
|
||||
username: '',
|
||||
@@ -63,5 +64,12 @@ module.exports = {
|
||||
},
|
||||
*/
|
||||
|
||||
remoteStorage: false,
|
||||
/*
|
||||
remoteStorage: {
|
||||
url: 'https://127.0.0.1:11900',
|
||||
accessToken: '',
|
||||
},
|
||||
*/
|
||||
};
|
||||
|
||||
|
||||
95
server/controllers/BookUpdateCheckerController.js
Normal file
95
server/controllers/BookUpdateCheckerController.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const WebSocket = require ('ws');
|
||||
//const _ = require('lodash');
|
||||
|
||||
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.readerStorage = new JembaReaderStorage();
|
||||
|
||||
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(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||
}
|
||||
|
||||
req = JSON.parse(message);
|
||||
|
||||
ws.lastActivity = Date.now();
|
||||
|
||||
//pong for WebSocketConnection
|
||||
this.send({_rok: 1}, req, ws);
|
||||
|
||||
switch (req.action) {
|
||||
case 'test':
|
||||
await this.test(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(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//Actions ------------------------------------------------------------------
|
||||
async test(req, ws) {
|
||||
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = BookUpdateCheckerController;
|
||||
@@ -68,24 +68,6 @@ class ReaderController extends BaseController {
|
||||
res.status(400).send({error});
|
||||
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;
|
||||
|
||||
@@ -25,6 +25,10 @@ class WebSocketController {
|
||||
ws.on('message', (message) => {
|
||||
this.onMessage(ws, message.toString());
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
log(LM_ERR, err);
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
@@ -66,10 +70,12 @@ class WebSocketController {
|
||||
await this.workerGetState(req, ws); break;
|
||||
case 'worker-get-state-finish':
|
||||
await this.workerGetStateFinish(req, ws); break;
|
||||
case 'reader-restore-cached-file':
|
||||
await this.readerRestoreCachedFile(req, ws); break;
|
||||
case 'reader-storage':
|
||||
await this.readerStorageDo(req, ws); break;
|
||||
case 'upload-file-buf':
|
||||
await this.uploadFileBuf(req, ws); break;
|
||||
case 'upload-file-touch':
|
||||
await this.uploadFileTouch(req, ws); break;
|
||||
|
||||
default:
|
||||
throw new Error(`Action not found: ${req.action}`);
|
||||
@@ -149,15 +155,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) {
|
||||
if (!req.body)
|
||||
throw new Error(`key 'body' is empty`);
|
||||
@@ -168,6 +165,20 @@ class WebSocketController {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketController;
|
||||
|
||||
@@ -25,60 +25,7 @@ class WorkerController extends BaseController {
|
||||
res.status(400).send({error});
|
||||
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;
|
||||
|
||||
@@ -3,4 +3,5 @@ module.exports = {
|
||||
ReaderController: require('./ReaderController'),
|
||||
WorkerController: require('./WorkerController'),
|
||||
WebSocketController: require('./WebSocketController'),
|
||||
BookUpdateCheckerController: require('./BookUpdateCheckerController'),
|
||||
}
|
||||
@@ -21,10 +21,10 @@ class AsyncExit {
|
||||
}
|
||||
|
||||
_init(signals, codeOnSignal) {
|
||||
const runSingalCallbacks = async(signal) => {
|
||||
const runSingalCallbacks = async(signal, err, origin) => {
|
||||
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
||||
try {
|
||||
await signalCallback(signal);
|
||||
await signalCallback(signal, err, origin);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -32,8 +32,8 @@ class AsyncExit {
|
||||
};
|
||||
|
||||
for (const signal of signals) {
|
||||
process.once(signal, async() => {
|
||||
await runSingalCallbacks(signal);
|
||||
process.once(signal, async(err, origin) => {
|
||||
await runSingalCallbacks(signal, err, origin);
|
||||
this.exit(codeOnSignal);
|
||||
});
|
||||
}
|
||||
|
||||
0
server/core/BookUpdateChecker/BUCClient.js
Normal file
0
server/core/BookUpdateChecker/BUCClient.js
Normal file
24
server/core/BookUpdateChecker/BUCServer.js
Normal file
24
server/core/BookUpdateChecker/BUCServer.js
Normal file
@@ -0,0 +1,24 @@
|
||||
let instance = null;
|
||||
|
||||
//singleton
|
||||
class BUCServer {
|
||||
constructor(config) {
|
||||
if (!instance) {
|
||||
this.config = Object.assign({}, config);
|
||||
|
||||
this.config.tempDownloadDir = `${config.tempDir}/download`;
|
||||
fs.ensureDirSync(this.config.tempDownloadDir);
|
||||
|
||||
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async main() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BUCServer;
|
||||
@@ -1,4 +1,4 @@
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
|
||||
class FileDownloader {
|
||||
constructor(limitDownloadSize = 0) {
|
||||
@@ -7,54 +7,82 @@ class FileDownloader {
|
||||
|
||||
async load(url, callback, abort) {
|
||||
let errMes = '';
|
||||
|
||||
const options = {
|
||||
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'
|
||||
},
|
||||
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 {
|
||||
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) {
|
||||
errMes = (errMes ? errMes : error.message);
|
||||
throw new Error(errMes);
|
||||
}
|
||||
}
|
||||
|
||||
streamToBuffer(stream, progress) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (!progress)
|
||||
progress = () => {};
|
||||
|
||||
const _buf = [];
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
_buf.push(chunk);
|
||||
progress(chunk);
|
||||
});
|
||||
stream.on('end', () => resolve(Buffer.concat(_buf)));
|
||||
stream.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
stream.on('aborted', () => {
|
||||
reject(new Error('aborted'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileDownloader;
|
||||
module.exports = FileDownloader;
|
||||
|
||||
@@ -49,7 +49,7 @@ class BaseLog {
|
||||
this.outputBuffer = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -188,8 +188,8 @@ class Logger {
|
||||
}
|
||||
|
||||
this.closed = false;
|
||||
ayncExit.onSignal((signal) => {
|
||||
this.log(LM_FATAL, `Signal ${signal} received, exiting...`);
|
||||
ayncExit.onSignal((signal, err) => {
|
||||
this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", exiting...`);
|
||||
});
|
||||
ayncExit.addAfter(this.close.bind(this));
|
||||
}
|
||||
@@ -218,6 +218,8 @@ class Logger {
|
||||
} else {
|
||||
console.log(mes);
|
||||
}
|
||||
|
||||
return mes;
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
||||
@@ -3,7 +3,7 @@ const chardet = require('chardet');
|
||||
function getEncoding(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));
|
||||
for (const charset of charsetAll) {
|
||||
if (charset.name.indexOf('ISO-8859') < 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@ const _ = require('lodash');
|
||||
|
||||
const utils = require('../utils');
|
||||
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||
const log = new (require('../AppLogger'))().log;//singleton
|
||||
|
||||
let instance = null;
|
||||
|
||||
@@ -20,25 +21,30 @@ class JembaReaderStorage {
|
||||
}
|
||||
|
||||
async doAction(act) {
|
||||
if (!_.isObject(act.items))
|
||||
throw new Error('items is not an object');
|
||||
try {
|
||||
if (!_.isObject(act.items))
|
||||
throw new Error('items is not an object');
|
||||
|
||||
let result = {};
|
||||
switch (act.action) {
|
||||
case 'check':
|
||||
result = await this.checkItems(act.items);
|
||||
break;
|
||||
case 'get':
|
||||
result = await this.getItems(act.items);
|
||||
break;
|
||||
case 'set':
|
||||
result = await this.setItems(act.items, act.force);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown action');
|
||||
let result = {};
|
||||
switch (act.action) {
|
||||
case 'check':
|
||||
result = await this.checkItems(act.items);
|
||||
break;
|
||||
case 'get':
|
||||
result = await this.getItems(act.items);
|
||||
break;
|
||||
case 'set':
|
||||
result = await this.setItems(act.items, act.force);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown action');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
log(LM_ERR, `JembaReaderStorage: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async checkItems(items) {
|
||||
|
||||
@@ -6,12 +6,12 @@ const WorkerState = require('../WorkerState');//singleton
|
||||
const FileDownloader = require('../FileDownloader');
|
||||
const FileDecompressor = require('../FileDecompressor');
|
||||
const BookConverter = require('./BookConverter');
|
||||
const RemoteWebDavStorage = require('../RemoteWebDavStorage');
|
||||
const RemoteStorage = require('../RemoteStorage');
|
||||
|
||||
const utils = require('../utils');
|
||||
const log = new (require('../AppLogger'))().log;//singleton
|
||||
|
||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
||||
const cleanDirPeriod = 30*60*1000;//раз в полчаса
|
||||
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||
|
||||
let instance = null;
|
||||
@@ -33,15 +33,27 @@ class ReaderWorker {
|
||||
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
||||
this.bookConverter = new BookConverter(this.config);
|
||||
|
||||
this.remoteWebDavStorage = false;
|
||||
if (config.remoteWebDavStorage) {
|
||||
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.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod);
|
||||
this.remoteConfig = {
|
||||
'/tmp': {
|
||||
dir: this.config.tempPublicDir,
|
||||
maxSize: this.config.maxTempPublicDirSize,
|
||||
moveToRemote: true,
|
||||
},
|
||||
'/upload': {
|
||||
dir: this.config.uploadDir,
|
||||
maxSize: this.config.maxUploadPublicDirSize,
|
||||
moveToRemote: true,
|
||||
}
|
||||
};
|
||||
|
||||
this.periodicCleanDir(this.remoteConfig);//no await
|
||||
|
||||
instance = this;
|
||||
}
|
||||
@@ -54,7 +66,6 @@ class ReaderWorker {
|
||||
let decompDir = '';
|
||||
let downloadedFilename = '';
|
||||
let isUploaded = false;
|
||||
let isRestored = false;
|
||||
let convertFilename = '';
|
||||
|
||||
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
|
||||
@@ -94,8 +105,7 @@ class ReaderWorker {
|
||||
if (!await fs.pathExists(downloadedFilename)) {
|
||||
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
||||
try {
|
||||
downloadedFilename = await this.restoreRemoteFile(fileHash);
|
||||
isRestored = true;
|
||||
await this.restoreRemoteFile(fileHash, '/upload');
|
||||
} catch(e) {
|
||||
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
||||
}
|
||||
@@ -144,33 +154,6 @@ class ReaderWorker {
|
||||
const finishFilename = path.basename(compFilename);
|
||||
wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
|
||||
|
||||
//лениво сохраним compFilename в удаленном хранилище
|
||||
if (this.remoteWebDavStorage) {
|
||||
(async() => {
|
||||
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
|
||||
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
|
||||
(async() => {
|
||||
await utils.sleep(30*1000);
|
||||
try {
|
||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
|
||||
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
|
||||
} catch (e) {
|
||||
log(LM_ERR, e.stack);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log(LM_ERR, e.stack);
|
||||
let mes = e.message.split('|FORLOG|');
|
||||
@@ -219,14 +202,41 @@ class ReaderWorker {
|
||||
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);
|
||||
} 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.remoteConfig[remoteDir])
|
||||
targetDir = this.remoteConfig[remoteDir].dir;
|
||||
else
|
||||
throw new Error(`restoreRemoteFile: unknown remoteDir value (${remoteDir})`);
|
||||
|
||||
const basename = path.basename(filename);
|
||||
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
||||
const targetName = `${targetDir}/${basename}`;
|
||||
|
||||
if (!await fs.pathExists(targetName)) {
|
||||
let found = false;
|
||||
if (this.remoteWebDavStorage) {
|
||||
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
|
||||
if (this.remoteStorage) {
|
||||
found = await this.remoteStorage.getFileSuccess(targetName, remoteDir);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
@@ -237,83 +247,81 @@ class ReaderWorker {
|
||||
return targetName;
|
||||
}
|
||||
|
||||
restoreCachedFile(filename) {
|
||||
const workerId = this.workerState.generateWorkerId();
|
||||
const wState = this.workerState.getControl(workerId);
|
||||
wState.set({state: 'start'});
|
||||
async cleanDir(dir, remoteDir, maxSize, moveToRemote) {
|
||||
if (!this.remoteSent)
|
||||
this.remoteSent = {};
|
||||
if (!this.remoteSent[remoteDir])
|
||||
this.remoteSent[remoteDir] = {};
|
||||
|
||||
(async() => {
|
||||
try {
|
||||
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
|
||||
const sent = this.remoteSent[remoteDir];
|
||||
|
||||
const targetName = await this.restoreRemoteFile(filename);
|
||||
const stat = await fs.stat(targetName);
|
||||
const list = await fs.readdir(dir);
|
||||
|
||||
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});
|
||||
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(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
|
||||
|
||||
return workerId;
|
||||
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
||||
|
||||
if (moveToRemote && this.remoteStorage) {
|
||||
for (const file of files) {
|
||||
if (sent[file.name])
|
||||
continue;
|
||||
|
||||
//отправляем в remoteStorage
|
||||
try {
|
||||
log(`remoteStorage.putFile ${remoteDir}/${path.basename(file.name)}`);
|
||||
await this.remoteStorage.putFile(file.name, remoteDir);
|
||||
sent[file.name] = true;
|
||||
} catch (e) {
|
||||
log(LM_ERR, e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
delete sent[oldFile];
|
||||
j++;
|
||||
}
|
||||
|
||||
size -= file.stat.size;
|
||||
i++;
|
||||
}
|
||||
log(`removed ${j} files`);
|
||||
}
|
||||
|
||||
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});
|
||||
async periodicCleanDir(cleanConfig) {
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
for (const [remoteDir, config] of Object.entries(cleanConfig)) {
|
||||
try {
|
||||
await this.cleanDir(config.dir, remoteDir, config.maxSize, config.moveToRemote);
|
||||
} catch(e) {
|
||||
log(LM_ERR, e.stack);
|
||||
}
|
||||
}
|
||||
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);
|
||||
await utils.sleep(cleanDirPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ReaderWorker;
|
||||
97
server/core/RemoteStorage.js
Normal file
97
server/core/RemoteStorage.js
Normal file
@@ -0,0 +1,97 @@
|
||||
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);
|
||||
}
|
||||
|
||||
async wsQuery(query) {
|
||||
const response = await this.wsc.message(
|
||||
await this.wsc.send(Object.assign({accessToken: this.accessToken}, query))
|
||||
);
|
||||
if (response.error)
|
||||
throw new Error(response.error);
|
||||
return response;
|
||||
}
|
||||
|
||||
async wsStat(fileName) {
|
||||
return await this.wsQuery({action: 'get-stat', fileName});
|
||||
}
|
||||
|
||||
async wsGetFile(fileName) {
|
||||
return this.wsQuery({action: 'get-file', fileName});
|
||||
}
|
||||
|
||||
async wsPutFile(fileName, data) {//data base64 encoded string
|
||||
return this.wsQuery({action: 'put-file', fileName, data});
|
||||
}
|
||||
|
||||
async wsDelFile(fileName) {
|
||||
return this.wsQuery({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);
|
||||
}
|
||||
|
||||
async putFile(filename) {
|
||||
async putFile(filename, dir = '') {
|
||||
if (!await fs.pathExists(filename)) {
|
||||
throw new Error(`File not found: ${filename}`);
|
||||
}
|
||||
|
||||
const base = path.basename(filename);
|
||||
let remoteFilename = `/${base}`;
|
||||
let remoteFilename = `${dir}/${base}`;
|
||||
|
||||
if (base.length > 3) {
|
||||
const remoteDir = `/${base.substr(0, 3)}`;
|
||||
const remoteDir = `${dir}/${base.substr(0, 3)}`;
|
||||
try {
|
||||
await this.mkdir(remoteDir);
|
||||
} catch (e) {
|
||||
@@ -79,24 +79,24 @@ class RemoteWebDavStorage {
|
||||
await this.writeFile(remoteFilename, data);
|
||||
}
|
||||
|
||||
async getFile(filename) {
|
||||
async getFile(filename, dir = '') {
|
||||
if (await fs.pathExists(filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const base = path.basename(filename);
|
||||
let remoteFilename = `/${base}`;
|
||||
let remoteFilename = `${dir}/${base}`;
|
||||
if (base.length > 3) {
|
||||
remoteFilename = `/${base.substr(0, 3)}/${base}`;
|
||||
remoteFilename = `${dir}/${base.substr(0, 3)}/${base}`;
|
||||
}
|
||||
|
||||
const data = await this.readFile(remoteFilename);
|
||||
await fs.writeFile(filename, data);
|
||||
}
|
||||
|
||||
async getFileSuccess(filename) {
|
||||
async getFileSuccess(filename, dir = '') {
|
||||
try {
|
||||
await this.getFile(filename);
|
||||
await this.getFile(filename, dir);
|
||||
return true;
|
||||
} catch (e) {
|
||||
//
|
||||
|
||||
@@ -94,7 +94,7 @@ class WebSocketConnection {
|
||||
this.ws = new this.WebSocket(this.url);
|
||||
}
|
||||
|
||||
const onopen = (e) => {
|
||||
const onopen = () => {
|
||||
this.connecting = false;
|
||||
resolve(this.ws);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ function toBase36(data) {
|
||||
}
|
||||
|
||||
function fromBase36(data) {
|
||||
return bs36.decode(data);
|
||||
return Buffer.from(bs36.decode(data));
|
||||
}
|
||||
|
||||
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) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -129,6 +135,7 @@ module.exports = {
|
||||
fromBase36,
|
||||
bufferRemoveZeroes,
|
||||
getFileHash,
|
||||
getBufHash,
|
||||
sleep,
|
||||
toUnixTime,
|
||||
randomHexString,
|
||||
|
||||
16
server/db/jembaMigrations/book-update-server/001-create.js
Normal file
16
server/db/jembaMigrations/book-update-server/001-create.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
up: [
|
||||
['create', {
|
||||
table: 'checked',
|
||||
index: [
|
||||
{field: 'queryTime', type: 'number'},
|
||||
{field: 'checkTime', type: 'number'},
|
||||
]
|
||||
}],
|
||||
],
|
||||
down: [
|
||||
['drop', {
|
||||
table: 'checked'
|
||||
}],
|
||||
]
|
||||
};
|
||||
6
server/db/jembaMigrations/book-update-server/index.js
Normal file
6
server/db/jembaMigrations/book-update-server/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
table: 'migration1',
|
||||
data: [
|
||||
{id: 1, name: 'create', data: require('./001-create')}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
//'app': require('./jembaMigrations/app'),
|
||||
'reader-storage': require('./reader-storage'),
|
||||
'book-update-server': require('./book-update-server'),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
|
||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const argv = require('minimist')(process.argv.slice(2));
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
@@ -11,6 +12,8 @@ const ayncExit = new (require('./core/AsyncExit'))();
|
||||
|
||||
let log = null;
|
||||
|
||||
const maxPayloadSize = 50;//in MB
|
||||
|
||||
async function init() {
|
||||
//config
|
||||
const configManager = new (require('./config'))();//singleton
|
||||
@@ -63,7 +66,7 @@ async function main() {
|
||||
if (serverCfg.mode !== 'none') {
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 });
|
||||
const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
|
||||
|
||||
const serverConfig = Object.assign({}, config, serverCfg);
|
||||
|
||||
@@ -75,20 +78,10 @@ async function main() {
|
||||
}
|
||||
|
||||
app.use(compression({ level: 1 }));
|
||||
app.use(express.json({limit: '10mb'}));
|
||||
app.use(express.json({limit: `${maxPayloadSize}mb`}));
|
||||
if (devModule)
|
||||
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);
|
||||
|
||||
if (devModule) {
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
const c = require('./controllers');
|
||||
const utils = require('./core/utils');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
const express = require('express');
|
||||
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) {
|
||||
//эксклюзив для update_checker
|
||||
if (config.mode === 'book_update_checker') {
|
||||
new c.BookUpdateCheckerController(wss, config);
|
||||
return;
|
||||
}
|
||||
|
||||
initStatic(app, config);
|
||||
|
||||
const misc = new c.MiscController(config);
|
||||
const reader = new c.ReaderController(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/storage', reader.storage.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-finish', worker.getStateFinish.bind(worker), [aAll], {}],
|
||||
];
|
||||
|
||||
//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: ${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 = {
|
||||
initRoutes
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user