Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
511a308646 | ||
|
|
65c8f2cc81 | ||
|
|
238c18bc48 | ||
|
|
873a08fee1 | ||
|
|
7e89228803 | ||
|
|
fc630923a4 | ||
|
|
928f911d03 | ||
|
|
7ffcd3fe1b | ||
|
|
0efbaf643a | ||
|
|
f1bf8e54ae | ||
|
|
b4aa6ab6c8 | ||
|
|
72431f0202 | ||
|
|
04a326c0e4 | ||
|
|
931966f4f3 | ||
|
|
8808cc4779 | ||
|
|
988c959eba | ||
|
|
c0b658d9e6 | ||
|
|
3190246f34 | ||
|
|
d957b4a5f9 | ||
|
|
bef9e5705c | ||
|
|
eb2affa518 | ||
|
|
07b9a3c033 | ||
|
|
3ca14ae06a | ||
|
|
7caa0c2112 | ||
|
|
9c69f5bc01 | ||
|
|
125a2e0f17 | ||
|
|
1b4360b897 | ||
|
|
4775d6e47b | ||
|
|
33fc553c55 | ||
|
|
25cad81c50 | ||
|
|
02a2099c1f | ||
|
|
1cda186b1a | ||
|
|
f10291b6c6 | ||
|
|
26ab5d6765 | ||
|
|
5edeed0747 | ||
|
|
c878ce432f | ||
|
|
81798897c8 | ||
|
|
63840fadbc | ||
|
|
36aa057035 | ||
|
|
30afd2421c | ||
|
|
53a1d90bd8 | ||
|
|
2ecf6beef2 | ||
|
|
85910a20e9 | ||
|
|
66cf7790b3 | ||
|
|
4a9eb7e4bb | ||
|
|
07446696c1 | ||
|
|
a29f9d9a4b | ||
|
|
d49c9baec3 | ||
|
|
8c9d4a12ee | ||
|
|
fce69e4657 | ||
|
|
b387509f88 | ||
|
|
8dc8bdc0d6 | ||
|
|
00caae8363 | ||
|
|
2ead8570a7 | ||
|
|
408315466b | ||
|
|
c651836554 | ||
|
|
03a1e70fce | ||
|
|
ab5a11a24f | ||
|
|
8cd6ed472c | ||
|
|
055181b744 | ||
|
|
e331a3920b | ||
|
|
c62bccb470 | ||
|
|
ea351ea293 | ||
|
|
d806a07c60 | ||
|
|
c0ea096f1f | ||
|
|
011d4a1672 | ||
|
|
4836a737c6 | ||
|
|
5712b2ee17 | ||
|
|
32dd17694e | ||
|
|
3ebc932a6a | ||
|
|
8f351d9bef | ||
|
|
5ae3ea94e4 | ||
|
|
f203d453a4 | ||
|
|
0d5cba121b | ||
|
|
0cd6a48a46 | ||
|
|
4e07ce2b5c | ||
|
|
85a525e301 | ||
|
|
03e4a6d723 | ||
|
|
ab28af1abe | ||
|
|
7fceed5301 | ||
|
|
0077816afa | ||
|
|
cb01423147 | ||
|
|
61b0712d36 | ||
|
|
12d7843377 | ||
|
|
9293c0a0d4 | ||
|
|
bb9522197a | ||
|
|
450a2e0664 | ||
|
|
41e35f3ec8 | ||
|
|
a9bc98abe3 | ||
|
|
47bca03532 | ||
|
|
942021371c | ||
|
|
ea2f178730 | ||
|
|
4b5c8d9efe | ||
|
|
28ebf13c3a | ||
|
|
5d52e63dd9 | ||
|
|
1a0e024050 | ||
|
|
e627a0d970 |
@@ -4,7 +4,7 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
@@ -29,7 +29,8 @@ async function main() {
|
|||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
// Скачиваем 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}`);
|
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
@@ -46,7 +47,8 @@ async function main() {
|
|||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
||||||
|
|
||||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
@@ -29,7 +29,8 @@ async function main() {
|
|||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
// Скачиваем 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}`);
|
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
@@ -46,7 +47,8 @@ async function main() {
|
|||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
||||||
|
|
||||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Misc {
|
|||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
|
|
||||||
const query = {params: [
|
const query = {params: [
|
||||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
|
||||||
]};
|
]};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
Открыть выбранную закладку
|
Открыть выбранную закладку
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-input ref="search" v-model="search" class="col" rounded outlined dense bg-color="white" placeholder="Найти">
|
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
||||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
|
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||||
<q-icon name="la la-plus" size="16px" />
|
<q-icon name="la la-plus" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||||
<q-icon name="la la-minus" size="16px" />
|
<q-icon name="la la-minus" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
<span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||||
<q-icon name="la la-question-circle" size="16px" />
|
<q-icon name="la la-question-circle" size="16px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
:options="rootLinkOptions"
|
:options="rootLinkOptions"
|
||||||
style="width: 230px"
|
style="width: 230px"
|
||||||
dropdown-icon="la la-angle-down la-sm"
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
|
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
:options="selectedLinkOptions"
|
:options="selectedLinkOptions"
|
||||||
style="width: 50px"
|
style="width: 50px"
|
||||||
dropdown-icon="la la-angle-down la-sm"
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||||
>
|
>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
v-model="bookUrl"
|
v-model="bookUrl"
|
||||||
class="col q-mr-sm"
|
class="col q-mr-sm"
|
||||||
rounded outlined dense
|
outlined dense
|
||||||
bg-color="white"
|
bg-color="white"
|
||||||
placeholder="Скопируйте сюда URL книги"
|
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
|
||||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-btn :disabled="!bookUrl" rounded color="green-7" no-caps size="14px" @click="submitUrl">
|
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
|
||||||
Открыть
|
Открыть
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
Открыть в читалке
|
Открыть в читалке
|
||||||
@@ -894,14 +894,15 @@ export default vueComponent(ExternalLibs);
|
|||||||
background-color: #A0A0A0;
|
background-color: #A0A0A0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-screen-button {
|
.header-button {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-screen-button:hover {
|
.header-button:hover {
|
||||||
background-color: #69C05F;
|
color: white;
|
||||||
|
background-color: #39902F;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparent-layout {
|
.transparent-layout {
|
||||||
|
|||||||
@@ -23,15 +23,15 @@
|
|||||||
|
|
||||||
<div class="q-mb-sm" />
|
<div class="q-mb-sm" />
|
||||||
|
|
||||||
<div v-show="selectedTab == 'contents'" class="tab-panel">
|
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
<div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" />
|
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||||
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px" />
|
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
<div :style="item.indentStyle"></div>
|
<div :style="item.indentStyle"></div>
|
||||||
@@ -42,8 +42,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
<div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
|
||||||
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
|
<div
|
||||||
|
v-for="subitem in item.list"
|
||||||
|
:ref="`subitem${subitem.key}`"
|
||||||
|
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
|
||||||
|
>
|
||||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||||
<div class="no-expand-button"></div>
|
<div class="no-expand-button"></div>
|
||||||
<div :style="subitem.indentStyle"></div>
|
<div :style="subitem.indentStyle"></div>
|
||||||
@@ -61,10 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="selectedTab == 'images'" class="tab-panel">
|
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
<div class="image-thumb-box row justify-center items-center">
|
<div class="image-thumb-box row justify-center items-center">
|
||||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
||||||
@@ -124,7 +128,10 @@ const componentOptions = {
|
|||||||
watch: {
|
watch: {
|
||||||
bookPos() {
|
bookPos() {
|
||||||
this.updateBookPosSelection();
|
this.updateBookPosSelection();
|
||||||
}
|
},
|
||||||
|
selectedTab() {
|
||||||
|
this.updateBookPosScrollTop();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
class ContentsPage {
|
class ContentsPage {
|
||||||
@@ -282,31 +289,30 @@ class ContentsPage {
|
|||||||
if (!this.isVisible)
|
if (!this.isVisible)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await utils.sleep(50);
|
await this.$nextTick();
|
||||||
const bp = this.bookPos;
|
const bp = this.bookPos;
|
||||||
|
|
||||||
for (let i = 0; i < this.contents.length; i++) {
|
for (let i = 0; i < this.contents.length; i++) {
|
||||||
const item = this.contents[i];
|
const item = this.contents[i];
|
||||||
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||||
|
|
||||||
|
if (bp >= item.offset && bp < nextOffset) {
|
||||||
|
item.isBookPos = true;
|
||||||
|
} else if (item.isBookPos) {
|
||||||
|
item.isBookPos = false;
|
||||||
|
}
|
||||||
|
|
||||||
for (let j = 0; j < item.list.length; j++) {
|
for (let j = 0; j < item.list.length; j++) {
|
||||||
const subitem = item.list[j];
|
const subitem = item.list[j];
|
||||||
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||||
|
|
||||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||||
subitem.isBookPos = true;
|
subitem.isBookPos = true;
|
||||||
this.contents[i] = Object.assign(item, {list: item.list});
|
this.updateBookPosScrollTop('contents', item, subitem, j);
|
||||||
} else if (subitem.isBookPos) {
|
} else if (subitem.isBookPos) {
|
||||||
subitem.isBookPos = false;
|
subitem.isBookPos = false;
|
||||||
this.contents[i] = Object.assign(item, {list: item.list});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bp >= item.offset && bp < nextOffset) {
|
|
||||||
this.contents[i] = Object.assign(item, {isBookPos: true});
|
|
||||||
} else if (item.isBookPos) {
|
|
||||||
this.contents[i] = Object.assign(item, {isBookPos: false});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.images.length; i++) {
|
for (let i = 0; i < this.images.length; i++) {
|
||||||
@@ -314,11 +320,96 @@ class ContentsPage {
|
|||||||
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||||
|
|
||||||
if (bp >= img.offset && bp < nextOffset) {
|
if (bp >= img.offset && bp < nextOffset) {
|
||||||
this.images[i] = Object.assign(img, {isBookPos: true});
|
this.images[i].isBookPos = true;
|
||||||
} else if (img.isBookPos) {
|
} else if (img.isBookPos) {
|
||||||
this.images[i] = Object.assign(img, {isBookPos: false});
|
this.images[i].isBookPos = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateBookPosScrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*getOffsetTop(key) {
|
||||||
|
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
|
||||||
|
return (el ? el.offsetTop : 0);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
async updateBookPosScrollTop() {
|
||||||
|
try {
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
if (this.selectedTab == 'contents') {
|
||||||
|
let item;
|
||||||
|
let subitem;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
//ищем выделенные item
|
||||||
|
for(const _item of this.contents) {
|
||||||
|
if (_item.isBookPos) {
|
||||||
|
item = _item;
|
||||||
|
for (let ii = 0; ii < item.list.length; ii++) {
|
||||||
|
const _subitem = item.list[ii];
|
||||||
|
if (_subitem.isBookPos) {
|
||||||
|
subitem = _subitem;
|
||||||
|
i = ii;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//вычисляем и смещаем tabPanel.scrollTop
|
||||||
|
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
|
||||||
|
let elShift = 0;
|
||||||
|
if (subitem && item.expanded) {
|
||||||
|
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
|
||||||
|
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
|
||||||
|
} else {
|
||||||
|
elShift = el.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabPanel = this.$refs.tabPanelContents;
|
||||||
|
const halfH = tabPanel.clientHeight/2;
|
||||||
|
const newScrollTop = el.offsetTop - halfH - elShift;
|
||||||
|
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||||
|
tabPanel.scrollTop = newScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedTab == 'images') {
|
||||||
|
let item;
|
||||||
|
|
||||||
|
//ищем выделенные item
|
||||||
|
for(const _item of this.images) {
|
||||||
|
if (_item.isBookPos) {
|
||||||
|
item = _item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//вычисляем и смещаем tabPanel.scrollTop
|
||||||
|
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
|
||||||
|
|
||||||
|
const tabPanel = this.$refs.tabPanelImages;
|
||||||
|
const halfH = tabPanel.clientHeight/2;
|
||||||
|
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
|
||||||
|
|
||||||
|
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||||
|
tabPanel.scrollTop = newScrollTop;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstElem(items) {
|
||||||
|
return (Array.isArray(items) ? items[0] : items);
|
||||||
}
|
}
|
||||||
|
|
||||||
async expandClick(key) {
|
async expandClick(key) {
|
||||||
@@ -326,17 +417,17 @@ class ContentsPage {
|
|||||||
const expanded = !item.expanded;
|
const expanded = !item.expanded;
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
const subitems = this.$refs[`subitem${key}`];
|
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||||
subitems.style.height = '0';
|
subdiv.style.height = '0';
|
||||||
await utils.sleep(200);
|
await utils.sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contents[key] = Object.assign({}, item, {expanded});
|
this.contents[key].expanded = expanded;
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
const subitems = this.$refs[`subitem${key}`];
|
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||||
subitems.style.height = subitems.scrollHeight + 'px';
|
subdiv.style.height = subdiv.scrollHeight + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="col column" style="min-width: 600px">
|
<div class="col column" style="min-width: 600px">
|
||||||
<q-btn-toggle
|
<div class="bg-grey-3 row">
|
||||||
v-model="selectedTab"
|
<q-tabs
|
||||||
toggle-color="primary"
|
v-model="selectedTab"
|
||||||
no-caps unelevated
|
active-color="black"
|
||||||
:options="buttons"
|
active-bg-color="white"
|
||||||
/>
|
indicator-color="white"
|
||||||
<div class="separator"></div>
|
dense
|
||||||
|
no-caps
|
||||||
|
inline-label
|
||||||
|
class="bg-grey-4 text-grey-7"
|
||||||
|
>
|
||||||
|
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="activePage" ref="page" class="col"></component>
|
<component :is="activePage" ref="page" class="col"></component>
|
||||||
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.separator {
|
|
||||||
height: 1px;
|
|
||||||
background-color: #E0E0E0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,13 +12,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
<q-input ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" placeholder="URL книги" @keydown="onInputKeydown">
|
<q-input
|
||||||
|
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
|
||||||
|
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
|
||||||
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<input id="file" ref="file" type="file" style="display: none;" @change="loadFile" />
|
<input
|
||||||
|
id="file" ref="file" type="file"
|
||||||
|
style="display: none;"
|
||||||
|
:accept="acceptFileExt"
|
||||||
|
@change="loadFile"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||||
@@ -47,6 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||||
|
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
|
||||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||||
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
|
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
|
||||||
|
|
||||||
@@ -55,6 +64,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||||
|
|
||||||
|
<Dialog ref="dialog1" v-model="findBookVisible">
|
||||||
|
<template #header>
|
||||||
|
Подсказка ;-)
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Если вы хотите найти определенную книгу, добро пожаловать в
|
||||||
|
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
|
||||||
|
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -64,6 +85,7 @@ import vueComponent from '../../vueComponent.js';
|
|||||||
|
|
||||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||||
|
|
||||||
|
import Dialog from '../../share/Dialog.vue';
|
||||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||||
import {versionHistory} from '../versionHistory';
|
import {versionHistory} from '../versionHistory';
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
@@ -71,6 +93,7 @@ import * as utils from '../../../share/utils';
|
|||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
GithubCorner,
|
GithubCorner,
|
||||||
|
Dialog,
|
||||||
PasteTextPage,
|
PasteTextPage,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -80,6 +103,7 @@ class LoaderPage {
|
|||||||
bookUrl = null;
|
bookUrl = null;
|
||||||
loadPercent = 0;
|
loadPercent = 0;
|
||||||
pasteTextActive = false;
|
pasteTextActive = false;
|
||||||
|
findBookVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -112,6 +136,10 @@ class LoaderPage {
|
|||||||
return this.$store.state.config.version;
|
return this.$store.state.config.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get acceptFileExt() {
|
||||||
|
return this.$store.state.config.acceptFileExt;
|
||||||
|
}
|
||||||
|
|
||||||
get isExternalConverter() {
|
get isExternalConverter() {
|
||||||
return this.$store.state.config.useExternalBookConverter;
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
}
|
}
|
||||||
@@ -165,6 +193,10 @@ class LoaderPage {
|
|||||||
this.$emit('do-action', {action: 'donate'});
|
this.$emit('do-action', {action: 'donate'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findBook() {
|
||||||
|
this.findBookVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
openComments() {
|
openComments() {
|
||||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||||
}
|
}
|
||||||
@@ -181,6 +213,9 @@ class LoaderPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
|
if (this.$refs.dialog1.active)
|
||||||
|
return true;
|
||||||
|
|
||||||
if (this.pasteTextActive) {
|
if (this.pasteTextActive) {
|
||||||
return this.$refs.pasteTextPage.keyHook(event);
|
return this.$refs.pasteTextPage.keyHook(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
|
||||||
<div class="column justify-start items-center" style="height: 250px">
|
<div class="column justify-start items-center" style="height: 250px">
|
||||||
<q-circular-progress
|
<q-circular-progress
|
||||||
show-value
|
show-value
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="column no-wrap">
|
<div class="column no-wrap">
|
||||||
<div v-show="toolBarActive" ref="header" class="header">
|
<div v-show="toolBarActive" ref="header" class="header">
|
||||||
<div ref="buttons" class="row justify-between no-wrap">
|
<div ref="buttons" class="row justify-between no-wrap">
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
||||||
<q-icon name="la la-arrow-left" size="32px" />
|
<q-icon name="la la-arrow-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
|
<div class="space"></div>
|
||||||
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
||||||
<q-icon name="la la-angle-left" size="32px" />
|
<q-icon name="la la-angle-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -104,9 +105,10 @@
|
|||||||
{{ rstore.readerActions['recentBooks'] }}
|
{{ rstore.readerActions['recentBooks'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="space"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
|
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
|
||||||
<q-icon name="la la-mouse" size="32px" />
|
<q-icon name="la la-mouse" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -139,6 +141,7 @@
|
|||||||
@load-file="loadFile"
|
@load-file="loadFile"
|
||||||
@book-pos-changed="bookPosChanged"
|
@book-pos-changed="bookPosChanged"
|
||||||
@do-action="doAction"
|
@do-action="doAction"
|
||||||
|
@hide-tool-bar="hideToolBar"
|
||||||
></component>
|
></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
|
|
||||||
@@ -160,7 +163,7 @@
|
|||||||
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||||
|
|
||||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -199,6 +202,7 @@ import miscApi from '../../api/misc';
|
|||||||
|
|
||||||
import {versionHistory} from './versionHistory';
|
import {versionHistory} from './versionHistory';
|
||||||
import * as utils from '../../share/utils';
|
import * as utils from '../../share/utils';
|
||||||
|
import LockQueue from '../../share/LockQueue';
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
@@ -311,6 +315,8 @@ class Reader {
|
|||||||
this.reader = this.$store.state.reader;
|
this.reader = this.$store.state.reader;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
|
|
||||||
|
this.lock = new LockQueue(100);
|
||||||
|
|
||||||
this.$root.addEventHook('key', this.keyHook);
|
this.$root.addEventHook('key', this.keyHook);
|
||||||
|
|
||||||
this.lastActivePage = false;
|
this.lastActivePage = false;
|
||||||
@@ -343,6 +349,13 @@ class Reader {
|
|||||||
this.debouncedSetRecentBook(newValue);
|
this.debouncedSetRecentBook(newValue);
|
||||||
}, 15000, {maxWait: 20000});
|
}, 15000, {maxWait: 20000});
|
||||||
|
|
||||||
|
this.debouncedHideToolBar = _.debounce((event) => {
|
||||||
|
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
|
||||||
|
this.commit('reader/setToolBarActive', !!event.show);
|
||||||
|
this.$root.eventHook('resize');
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
});
|
});
|
||||||
@@ -351,8 +364,6 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateHeaderMinWidth();
|
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
await wallpaperStorage.init();
|
await wallpaperStorage.init();
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
@@ -402,6 +413,7 @@ class Reader {
|
|||||||
this.clickControlActive = this.clickControl;
|
this.clickControlActive = this.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
|
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
|
||||||
this.enableSitesFilter = settings.enableSitesFilter;
|
this.enableSitesFilter = settings.enableSitesFilter;
|
||||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||||
this.splitToPara = settings.splitToPara;
|
this.splitToPara = settings.splitToPara;
|
||||||
@@ -416,8 +428,6 @@ class Reader {
|
|||||||
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateHeaderMinWidth();
|
|
||||||
|
|
||||||
this.loadWallpapers();//no await
|
this.loadWallpapers();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,17 +494,6 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderMinWidth() {
|
|
||||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
|
||||||
if (this.$refs.buttons)
|
|
||||||
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
|
|
||||||
(async() => {
|
|
||||||
await utils.sleep(1000);
|
|
||||||
if (this.$refs.header)
|
|
||||||
this.$refs.header.style.overflowX = 'auto';
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkSetStorageAccessKey() {
|
checkSetStorageAccessKey() {
|
||||||
const q = this.$route.query;
|
const q = this.$route.query;
|
||||||
|
|
||||||
@@ -675,6 +674,10 @@ class Reader {
|
|||||||
this.$root.eventHook('resize');
|
this.$root.eventHook('resize');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideToolBar(event) {
|
||||||
|
this.debouncedHideToolBar(event);
|
||||||
|
}
|
||||||
|
|
||||||
fullScreenToggle() {
|
fullScreenToggle() {
|
||||||
this.fullScreenActive = !this.fullScreenActive;
|
this.fullScreenActive = !this.fullScreenActive;
|
||||||
if (this.fullScreenActive) {
|
if (this.fullScreenActive) {
|
||||||
@@ -910,7 +913,7 @@ class Reader {
|
|||||||
|
|
||||||
refreshBook() {
|
refreshBook() {
|
||||||
const mrb = this.mostRecentBook();
|
const mrb = this.mostRecentBook();
|
||||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
this.loadBook(Object.assign({}, mrb, {force: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
undoAction() {
|
undoAction() {
|
||||||
@@ -1064,7 +1067,7 @@ class Reader {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBook(opts) {
|
async _loadBook(opts) {
|
||||||
if (!opts || !opts.url) {
|
if (!opts || !opts.url) {
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
return;
|
return;
|
||||||
@@ -1074,10 +1077,6 @@ class Reader {
|
|||||||
|
|
||||||
let url = encodeURI(decodeURI(opts.url));
|
let url = encodeURI(decodeURI(opts.url));
|
||||||
|
|
||||||
//TODO: убрать конвертирование 'file://' после 06.2021
|
|
||||||
if (url.length == 71 && url.indexOf('file://') == 0)
|
|
||||||
url = url.replace(/^file/, 'disk');
|
|
||||||
|
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||||
(url.indexOf('disk://') != 0))
|
(url.indexOf('disk://') != 0))
|
||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
@@ -1104,33 +1103,36 @@ class Reader {
|
|||||||
progress.show();
|
progress.show();
|
||||||
progress.setState({state: 'parse'});
|
progress.setState({state: 'parse'});
|
||||||
|
|
||||||
// есть ли среди недавних
|
// есть ли среди загруженных
|
||||||
const key = bookManager.keyFromUrl(url);
|
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
|
||||||
let wasOpened = await bookManager.getRecentBook({key});
|
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
|
||||||
wasOpened = (wasOpened ? wasOpened : {});
|
|
||||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
wasOpened = Object.assign(wasOpened, {
|
||||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
path: (opts.path !== undefined ? opts.path : wasOpened.path),
|
||||||
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
|
||||||
|
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
|
||||||
|
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
|
||||||
|
});
|
||||||
|
|
||||||
let book = null;
|
let book = null;
|
||||||
|
|
||||||
if (!opts.force) {
|
if (!opts.force) {
|
||||||
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
||||||
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
|
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
|
||||||
progress.setState({progress: prog});
|
progress.setState({progress: prog});
|
||||||
});
|
});
|
||||||
|
|
||||||
// если есть в локальном кэше
|
// если есть в локальном кэше
|
||||||
if (bookParsed) {
|
if (bookParsed) {
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
|
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(wasOpened.bookPos);
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.blinkCachedLoadMessage();
|
this.blinkCachedLoadMessage();
|
||||||
|
|
||||||
this.checkBookPosPercent();
|
this.checkBookPosPercent();
|
||||||
await this.activateClickMapPage();
|
this.activateClickMapPage();//no await
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1146,7 @@ class Reader {
|
|||||||
});
|
});
|
||||||
book = Object.assign({}, wasOpened, {data: resp.data});
|
book = Object.assign({}, wasOpened, {data: resp.data});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//молчим
|
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1157,7 @@ class Reader {
|
|||||||
if (!book) {
|
if (!book) {
|
||||||
book = await readerApi.loadBook({
|
book = await readerApi.loadBook({
|
||||||
url,
|
url,
|
||||||
uploadFileName,
|
uploadFileName: wasOpened.uploadFileName,
|
||||||
enableSitesFilter: this.enableSitesFilter,
|
enableSitesFilter: this.enableSitesFilter,
|
||||||
skipHtmlCheck: (this.splitToPara ? true : false),
|
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||||
isText: (this.splitToPara ? true : false),
|
isText: (this.splitToPara ? true : false),
|
||||||
@@ -1172,14 +1174,44 @@ class Reader {
|
|||||||
|
|
||||||
// добавляем в bookManager
|
// добавляем в bookManager
|
||||||
progress.setState({state: 'parse', step: 5});
|
progress.setState({state: 'parse', step: 5});
|
||||||
|
|
||||||
const addedBook = await bookManager.addBook(book, (prog) => {
|
const addedBook = await bookManager.addBook(book, (prog) => {
|
||||||
progress.setState({progress: prog});
|
progress.setState({progress: prog});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// sameBookKey
|
||||||
|
if (url.indexOf('disk://') == 0) {
|
||||||
|
//ищем такой файл в загруженных
|
||||||
|
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
|
||||||
|
found = (found ? _.cloneDeep(found) : found);
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
if (wasOpened.sameBookKey != found.sameBookKey) {
|
||||||
|
//спрашиваем, надо ли объединить файлы
|
||||||
|
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
|
||||||
|
await this.$root.stdDialog.askYesNo(`
|
||||||
|
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
|
||||||
|
<br>Объединить позицию?`, 'Найдена похожая книга');
|
||||||
|
if (askResult) {
|
||||||
|
wasOpened.bookPos = found.bookPos;
|
||||||
|
wasOpened.bookPosSeen = found.bookPosSeen;
|
||||||
|
wasOpened.sameBookKey = found.sameBookKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wasOpened.sameBookKey = wasOpened.uploadFileName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wasOpened.sameBookKey = addedBook.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
|
||||||
|
delete wasOpened.loadTime;
|
||||||
|
|
||||||
// добавляем в историю
|
// добавляем в историю
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
|
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(wasOpened.bookPos);
|
||||||
this.updateRoute(true);
|
this.updateRoute(true);
|
||||||
|
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
@@ -1190,11 +1222,11 @@ class Reader {
|
|||||||
this.stopBlink = true;
|
this.stopBlink = true;
|
||||||
|
|
||||||
this.checkBookPosPercent();
|
this.checkBookPosPercent();
|
||||||
await this.activateClickMapPage();
|
this.activateClickMapPage();//no await
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
if (!this.showHelpOnErrorIfNeeded(e.message)) {
|
if (!this.showHelpOnErrorIfNeeded(url)) {
|
||||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1202,7 +1234,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;
|
this.progressActive = true;
|
||||||
|
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
@@ -1218,7 +1259,7 @@ class Reader {
|
|||||||
|
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
|
|
||||||
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
await this._loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
@@ -1226,6 +1267,15 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadFile(opts) {
|
||||||
|
await this.lock.get();
|
||||||
|
try {
|
||||||
|
await this._loadFile(opts);
|
||||||
|
} finally {
|
||||||
|
this.lock.ret();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blinkCachedLoadMessage() {
|
blinkCachedLoadMessage() {
|
||||||
if (!this.blinkCachedLoad)
|
if (!this.blinkCachedLoad)
|
||||||
return;
|
return;
|
||||||
@@ -1435,12 +1485,33 @@ export default vueComponent(Reader);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
|
height: 50px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
background-color: #1B695F;
|
background-color: #1B695F;
|
||||||
color: #000;
|
color: #000;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
height: 50px;
|
overflow-y: hidden;
|
||||||
|
scrollbar-color: #c49a60 #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-track {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #c49a60;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #b48a50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<div class="row justify-center">
|
<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-->
|
</q-btn-->
|
||||||
</div>
|
</div>
|
||||||
@@ -162,6 +162,7 @@ class ReaderDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadBufferClick() {
|
loadBufferClick() {
|
||||||
|
this.$emit('load-buffer-toggle');
|
||||||
this.urlHelpVisible = false;
|
this.urlHelpVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,88 +9,153 @@
|
|||||||
|
|
||||||
<a ref="download" style="display: none;" target="_blank"></a>
|
<a ref="download" style="display: none;" target="_blank"></a>
|
||||||
|
|
||||||
<q-table
|
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
|
||||||
class="recent-books-table col"
|
<div ref="header" class="scroll-header row bg-blue-2">
|
||||||
:rows="tableData"
|
<q-btn class="tool-button" round @click="showSameBookClick">
|
||||||
row-key="key"
|
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
|
||||||
:columns="columns"
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
:pagination="pagination"
|
Показать/скрыть версии книг
|
||||||
separator="cell"
|
</q-tooltip>
|
||||||
hide-bottom
|
</q-btn>
|
||||||
virtual-scroll
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
<template #header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th key="num" class="td-mp" style="width: 25px" :props="props">
|
|
||||||
<span v-html="props.cols[0].label"></span>
|
|
||||||
</q-th>
|
|
||||||
<q-th key="date" class="td-mp break-word" style="width: 77px" :props="props">
|
|
||||||
<span v-html="props.cols[1].label"></span>
|
|
||||||
</q-th>
|
|
||||||
<q-th key="desc" class="td-mp" style="width: 332px" :props="props" colspan="4">
|
|
||||||
<q-input ref="input" v-model="search"
|
|
||||||
outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
|
||||||
placeholder="Найти"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
<span v-html="props.cols[2].label"></span>
|
|
||||||
</q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body="props">
|
<q-btn class="tool-button" round @click="scrollToBegin">
|
||||||
<q-tr :props="props">
|
<q-icon name="la la-arrow-up" color="green-8" size="24px" />
|
||||||
<q-td key="num" :props="props" class="td-mp" auto-width>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
<div class="break-word" style="width: 25px">
|
В начало списка
|
||||||
{{ props.row.num }}
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn class="tool-button" round @click="scrollToEnd">
|
||||||
|
<q-icon name="la la-arrow-down" color="green-8" size="24px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
В конец списка
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn class="tool-button" round @click="scrollToActiveBook">
|
||||||
|
<q-icon name="la la-location-arrow" color="green-8" size="24px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
На текущую книгу
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
ref="input"
|
||||||
|
v-model="search"
|
||||||
|
class="q-ml-sm q-mt-xs"
|
||||||
|
outlined dense
|
||||||
|
style="width: 185px"
|
||||||
|
bg-color="white"
|
||||||
|
placeholder="Найти"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
ref="sortMethod"
|
||||||
|
v-model="sortMethod"
|
||||||
|
class="q-ml-sm q-mt-xs"
|
||||||
|
:options="sortMethodOptions"
|
||||||
|
style="width: 180px"
|
||||||
|
bg-color="white"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||||
|
options-html display-value-html
|
||||||
|
|
||||||
|
@update:model-value="sortMethodSelected"
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
Метод сортировки
|
||||||
|
</q-tooltip>
|
||||||
|
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-virtual-scroll
|
||||||
|
ref="virtualScroll"
|
||||||
|
v-slot="{ item, index }"
|
||||||
|
:items="tableData"
|
||||||
|
scroll-target="#vs-container"
|
||||||
|
virtual-scroll-item-size="80"
|
||||||
|
@virtual-scroll="onScroll"
|
||||||
|
>
|
||||||
|
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
|
||||||
|
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px; border-right: 1px solid #cccccc">
|
||||||
|
<q-icon name="la la-code-branch" size="24px" style="color: green" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
||||||
|
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
|
||||||
|
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="date" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
|
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
|
||||||
<div class="break-word" style="width: 68px">
|
{{ (item.group ? item.group.length + 1 : 0) }} верси{{ wordEnding((item.group ? item.group.length + 1 : 0), 1) }}
|
||||||
{{ props.row.touchDate }}<br>
|
|
||||||
{{ props.row.touchTime }}
|
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
</div>
|
||||||
|
|
||||||
<q-td key="desc" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
|
<div class="row-part column items-stretch clickable break-word" :style="{ 'width': (350 - 40*(+item.inGroup)) + 'px' }" style="font-size: 75%" @click="loadBook(item)">
|
||||||
<div class="break-word" style="width: 332px; font-size: 90%">
|
<div class="row" style="font-size: 80%">
|
||||||
<div style="color: green">
|
<div class="row justify-center row-info-top" style="width: 30px">
|
||||||
{{ props.row.desc.author }}
|
{{ item.num }}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center row-info-top" style="width: 130px">
|
||||||
|
Читался: {{ item.touchTime }}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center row-info-top" style="width: 138px">
|
||||||
|
Загружен: {{ item.loadTime }}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center row-info-top" style="width: 1px">
|
||||||
</div>
|
</div>
|
||||||
<div>{{ props.row.desc.title }}</div>
|
|
||||||
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
<div class="col q-mt-xs" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
|
||||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
<div class="text-green-10" style="font-size: 105%">
|
||||||
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
{{ item.desc.author }}
|
||||||
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
|
</div>
|
||||||
|
<div>{{ item.desc.title }}</div>
|
||||||
|
<!--div>{{ item.path }}</div-->
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
|
||||||
|
|
||||||
<q-td key="close" :props="props" class="td-mp" auto-width>
|
<div class="row q-mt-xs" style="font-size: 80%">
|
||||||
<div style="width: 38px">
|
<div class="row justify-center row-info-bottom" style="width: 60px">
|
||||||
<q-btn
|
{{ item.desc.textLen }}
|
||||||
dense
|
</div>
|
||||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
<div class="row justify-center row-info-bottom" style="width: 60px">
|
||||||
@click="handleDel(props.row.key)"
|
{{ item.desc.perc }}
|
||||||
>
|
</div>
|
||||||
<q-icon class="la la-times" size="14px" />
|
<div class="row justify-center row-info-bottom" style="width: 1px">
|
||||||
</q-btn>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
|
||||||
<q-td key="last" :props="props" class="no-mp">
|
<div class="read-bar" :style="`width: ${(340 - 40*(+item.inGroup))*item.readPart}px`"></div>
|
||||||
</q-td>
|
</div>
|
||||||
</q-tr>
|
|
||||||
</template>
|
<div class="row-part column justify-center" style="width: 80px; font-size: 75%">
|
||||||
</q-table>
|
<div>
|
||||||
|
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
|
||||||
|
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-part column justify-center">
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||||
|
@click="handleDel(item.key)"
|
||||||
|
>
|
||||||
|
<q-icon class="la la-times" size="14px" />
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -99,9 +164,10 @@
|
|||||||
import vueComponent from '../../vueComponent.js';
|
import vueComponent from '../../vueComponent.js';
|
||||||
|
|
||||||
import path from 'path-browserify';
|
import path from 'path-browserify';
|
||||||
//import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
import LockQueue from '../../../share/LockQueue';
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
import bookManager from '../share/bookManager';
|
import bookManager from '../share/bookManager';
|
||||||
import readerApi from '../../../api/reader';
|
import readerApi from '../../../api/reader';
|
||||||
@@ -111,9 +177,15 @@ const componentOptions = {
|
|||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search: function() {
|
search() {
|
||||||
this.updateTableData();
|
this.updateTableData();
|
||||||
}
|
},
|
||||||
|
sortMethod() {
|
||||||
|
this.updateTableData();
|
||||||
|
},
|
||||||
|
settings() {
|
||||||
|
this.loadSettings();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
class RecentBooksPage {
|
class RecentBooksPage {
|
||||||
@@ -122,52 +194,18 @@ class RecentBooksPage {
|
|||||||
loading = false;
|
loading = false;
|
||||||
search = '';
|
search = '';
|
||||||
tableData = [];
|
tableData = [];
|
||||||
columns = [];
|
sortMethod = '';
|
||||||
pagination = {};
|
showSameBook = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.firstInit = true;
|
this.commit = this.$store.commit;
|
||||||
this.pagination = {rowsPerPage: 0};
|
|
||||||
|
|
||||||
this.columns = [
|
this.lastScrollTop1 = 0;
|
||||||
{
|
this.lastScrollTop2 = 0;
|
||||||
name: 'num',
|
|
||||||
label: '#',
|
this.lock = new LockQueue(100);
|
||||||
align: 'center',
|
|
||||||
sortable: true,
|
this.loadSettings();
|
||||||
field: 'num',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'date',
|
|
||||||
label: 'Время<br>просм.',
|
|
||||||
align: 'left',
|
|
||||||
field: 'touchDateTime',
|
|
||||||
sortable: true,
|
|
||||||
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'desc',
|
|
||||||
label: 'Название',
|
|
||||||
align: 'left',
|
|
||||||
field: 'descString',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'links',
|
|
||||||
label: '',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'close',
|
|
||||||
label: '',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'last',
|
|
||||||
label: '',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -176,89 +214,185 @@ class RecentBooksPage {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
//this.$refs.input.focus();//плохо на планшетах
|
//this.$refs.input.focus();//плохо на планшетах
|
||||||
});
|
});
|
||||||
(async() => {//подгрузка списка
|
|
||||||
if (this.initing)
|
|
||||||
return;
|
|
||||||
this.initing = true;
|
|
||||||
|
|
||||||
if (this.firstInit) {//для отзывчивости
|
this.inited = true;
|
||||||
await this.updateTableData(20);
|
|
||||||
this.firstInit = false;
|
(async() => {
|
||||||
}
|
this.showBar();
|
||||||
await utils.sleep(50);
|
|
||||||
await this.updateTableData();
|
await this.updateTableData();
|
||||||
|
await this.scrollToActiveBook();
|
||||||
this.initing = false;
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTableData(limit) {
|
loadSettings() {
|
||||||
while (this.updating) await utils.sleep(100);
|
const settings = this.settings;
|
||||||
this.updating = true;
|
this.showSameBook = settings.recentShowSameBook;
|
||||||
let result = [];
|
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = !!limit;
|
get settings() {
|
||||||
const sorted = bookManager.getSortedRecent();
|
return this.$store.state.reader.settings;
|
||||||
|
}
|
||||||
|
|
||||||
let num = 0;
|
async updateTableData() {
|
||||||
for (let i = 0; i < sorted.length; i++) {
|
if (!this.inited)
|
||||||
const book = sorted[i];
|
return;
|
||||||
if (book.deleted)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
num++;
|
await this.lock.get();
|
||||||
if (limit && result.length >= limit)
|
try {
|
||||||
break;
|
let result = [];
|
||||||
|
|
||||||
let d = new Date();
|
const sorted = bookManager.getSortedRecent();
|
||||||
d.setTime(book.touchTime);
|
const activeBook = bookManager.mostRecentBook();
|
||||||
const t = utils.formatDate(d).split(' ');
|
|
||||||
|
|
||||||
let readPart = 0;
|
//подготовка полей
|
||||||
let perc = '';
|
for (const book of sorted) {
|
||||||
let textLen = '';
|
if (book.deleted)
|
||||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
continue;
|
||||||
if (book.textLength) {
|
|
||||||
readPart = p/book.textLength;
|
let d = new Date();
|
||||||
perc = ` [${(readPart*100).toFixed(2)}%]`;
|
d.setTime(book.touchTime);
|
||||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
const touchTime = utils.formatDate(d);
|
||||||
|
const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
|
||||||
|
d.setTime(loadTimeRaw);
|
||||||
|
const loadTime = utils.formatDate(d);
|
||||||
|
|
||||||
|
let readPart = 0;
|
||||||
|
let perc = '';
|
||||||
|
let textLen = '';
|
||||||
|
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
||||||
|
if (book.textLength) {
|
||||||
|
readPart = p/book.textLength;
|
||||||
|
perc = `${(readPart*100).toFixed(2)}%`;
|
||||||
|
textLen = `${Math.floor(readPart*book.textLength/1000)}/${Math.floor(book.textLength/1000)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bt = utils.getBookTitle(book.fb2);
|
||||||
|
|
||||||
|
let title = bt.bookTitle;
|
||||||
|
title = (title ? `"${title}"`: '');
|
||||||
|
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
touchTime,
|
||||||
|
loadTime,
|
||||||
|
desc: {
|
||||||
|
author,
|
||||||
|
title,
|
||||||
|
perc,
|
||||||
|
textLen,
|
||||||
|
},
|
||||||
|
readPart,
|
||||||
|
url: book.url,
|
||||||
|
path: book.path,
|
||||||
|
fullTitle: bt.fullTitle,
|
||||||
|
key: book.key,
|
||||||
|
sameBookKey: book.sameBookKey,
|
||||||
|
active: (activeBook.key == book.key),
|
||||||
|
activeParent: false,
|
||||||
|
inGroup: false,
|
||||||
|
|
||||||
|
//для сортировки
|
||||||
|
loadTimeRaw,
|
||||||
|
touchTimeRaw: book.touchTime,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const bt = utils.getBookTitle(book.fb2);
|
//нумерация
|
||||||
|
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
||||||
|
let num = 0;
|
||||||
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
|
num++;
|
||||||
|
result[i].num = num;
|
||||||
|
}
|
||||||
|
|
||||||
let title = bt.bookTitle;
|
//фильтрация
|
||||||
title = (title ? `"${title}"`: '');
|
const search = this.search;
|
||||||
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
|
if (search) {
|
||||||
|
result = result.filter(item => {
|
||||||
|
return !search ||
|
||||||
|
item.touchTime.includes(search) ||
|
||||||
|
item.loadTime.includes(search) ||
|
||||||
|
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
result.push({
|
//сортировка
|
||||||
num,
|
switch (this.sortMethod) {
|
||||||
touchDateTime: book.touchTime,
|
case 'loadTimeDesc':
|
||||||
touchDate: t[0],
|
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
|
||||||
touchTime: t[1],
|
break;
|
||||||
desc: {
|
case 'loadTimeAsc':
|
||||||
author,
|
result.sort((a, b) => a.loadTimeRaw - b.loadTimeRaw);
|
||||||
title: `${title}${perc}${textLen}`,
|
break;
|
||||||
},
|
case 'touchTimeDesc':
|
||||||
readPart,
|
result.sort((a, b) => b.touchTimeRaw - a.touchTimeRaw);
|
||||||
descString: `${author}${title}${perc}${textLen}`,//для сортировки
|
break;
|
||||||
url: book.url,
|
case 'touchTimeAsc':
|
||||||
path: book.path,
|
result.sort((a, b) => a.touchTimeRaw - b.touchTimeRaw);
|
||||||
fullTitle: bt.fullTitle,
|
break;
|
||||||
key: book.key,
|
case 'authorDesc':
|
||||||
});
|
result.sort((a, b) => b.desc.author.localeCompare(a.desc.author));
|
||||||
|
break;
|
||||||
|
case 'authorAsc':
|
||||||
|
result.sort((a, b) => a.desc.author.localeCompare(b.desc.author));
|
||||||
|
break;
|
||||||
|
case 'titleDesc':
|
||||||
|
result.sort((a, b) => b.desc.title.localeCompare(a.desc.title));
|
||||||
|
break;
|
||||||
|
case 'titleAsc':
|
||||||
|
result.sort((a, b) => a.desc.title.localeCompare(b.desc.title));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//группировка
|
||||||
|
const groups = {};
|
||||||
|
const parents = {};
|
||||||
|
let newResult = [];
|
||||||
|
for (const book of result) {
|
||||||
|
if (book.sameBookKey !== undefined) {
|
||||||
|
if (!groups[book.sameBookKey]) {
|
||||||
|
groups[book.sameBookKey] = [];
|
||||||
|
parents[book.sameBookKey] = book;
|
||||||
|
|
||||||
|
book.group = groups[book.sameBookKey];
|
||||||
|
newResult.push(book);
|
||||||
|
} else {
|
||||||
|
book.inGroup = true;
|
||||||
|
if (book.active)
|
||||||
|
parents[book.sameBookKey].activeParent = true;
|
||||||
|
|
||||||
|
groups[book.sameBookKey].push(book);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newResult.push(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = newResult;
|
||||||
|
|
||||||
|
//showSameBook
|
||||||
|
if (this.showSameBook) {
|
||||||
|
newResult = [];
|
||||||
|
for (const book of result) {
|
||||||
|
newResult.push(book);
|
||||||
|
if (book.group) {
|
||||||
|
for (const sameBook of book.group) {
|
||||||
|
newResult.push(sameBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = newResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
//другие стадии
|
||||||
|
//.....
|
||||||
|
|
||||||
|
this.tableData = result;
|
||||||
|
} finally {
|
||||||
|
this.lock.ret();
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = this.search;
|
|
||||||
result = result.filter(item => {
|
|
||||||
return !search ||
|
|
||||||
item.touchTime.includes(search) ||
|
|
||||||
item.touchDate.includes(search) ||
|
|
||||||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
|
||||||
});
|
|
||||||
|
|
||||||
this.tableData = result;
|
|
||||||
this.updating = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetSearch() {
|
resetSearch() {
|
||||||
@@ -266,19 +400,22 @@ class RecentBooksPage {
|
|||||||
this.$refs.input.focus();
|
this.$refs.input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
wordEnding(num) {
|
wordEnding(num, type = 0) {
|
||||||
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
const endings = [
|
||||||
|
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
|
||||||
|
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
|
||||||
|
];
|
||||||
const deci = num % 100;
|
const deci = num % 100;
|
||||||
if (deci > 10 && deci < 20) {
|
if (deci > 10 && deci < 20) {
|
||||||
return '';
|
return endings[type][0];
|
||||||
} else {
|
} else {
|
||||||
return endings[num % 10];
|
return endings[type][num % 10];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get header() {
|
get header() {
|
||||||
const len = (this.tableData ? this.tableData.length : 0);
|
const len = (this.tableData ? this.tableData.length : 0);
|
||||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
return `${(this.search ? 'Найдено' : 'Всего')} ${len} файл${this.wordEnding(len)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBook(fb2path, fullTitle) {
|
async downloadBook(fb2path, fullTitle) {
|
||||||
@@ -311,8 +448,8 @@ class RecentBooksPage {
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBook(url) {
|
loadBook(row) {
|
||||||
this.$emit('load-book', {url});
|
this.$emit('load-book', {url: row.url, path: row.path});
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +460,111 @@ class RecentBooksPage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showBar() {
|
||||||
|
this.lastScrollTop1 = this.$refs.vsContainer.scrollTop;
|
||||||
|
this.$refs.header.style.position = 'sticky';
|
||||||
|
this.$refs.header.style.top = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll() {
|
||||||
|
const curScrollTop = this.$refs.vsContainer.scrollTop;
|
||||||
|
|
||||||
|
if (this.lockScroll) {
|
||||||
|
this.lastScrollTop1 = curScrollTop;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curScrollTop - this.lastScrollTop1 > 100) {
|
||||||
|
this.$refs.header.style.top = `-${this.$refs.header.offsetHeight}px`;
|
||||||
|
this.$refs.header.style.transition = 'top 0.2s ease 0s';
|
||||||
|
|
||||||
|
this.lastScrollTop1 = curScrollTop;
|
||||||
|
} else if (curScrollTop - this.lastScrollTop2 < 0) {
|
||||||
|
this.$refs.header.style.position = 'sticky';
|
||||||
|
this.$refs.header.style.top = 0;
|
||||||
|
|
||||||
|
this.lastScrollTop1 = curScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastScrollTop2 = curScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSameBookClick() {
|
||||||
|
this.showSameBook = !this.showSameBook;
|
||||||
|
|
||||||
|
const newSettings = _.cloneDeep(this.settings);
|
||||||
|
newSettings.recentShowSameBook = this.showSameBook;
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
sortMethodSelected() {
|
||||||
|
const newSettings = _.cloneDeep(this.settings);
|
||||||
|
newSettings.recentSortMethod = this.sortMethod;
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrollToActiveBook() {
|
||||||
|
this.lockScroll = true;
|
||||||
|
try {
|
||||||
|
let activeIndex = -1;
|
||||||
|
let activeParentIndex = -1;
|
||||||
|
for (let i = 0; i < this.tableData.length; i++) {
|
||||||
|
const book = this.tableData[i];
|
||||||
|
if (book.active)
|
||||||
|
activeIndex = i;
|
||||||
|
if (book.activeParent)
|
||||||
|
activeParentIndex = i;
|
||||||
|
|
||||||
|
if (activeIndex >= 0 && activeParentIndex >= 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = (activeIndex >= 0 ? activeIndex : activeParentIndex);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.$refs.virtualScroll.scrollTo(index, 'center');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await utils.sleep(100);
|
||||||
|
this.lockScroll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrollToBegin() {
|
||||||
|
this.lockScroll = true;
|
||||||
|
try {
|
||||||
|
this.$refs.virtualScroll.scrollTo(0, 'center');
|
||||||
|
} finally {
|
||||||
|
await utils.sleep(100);
|
||||||
|
this.lockScroll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrollToEnd() {
|
||||||
|
this.lockScroll = true;
|
||||||
|
try {
|
||||||
|
this.$refs.virtualScroll.scrollTo(this.tableData.length, 'center');
|
||||||
|
} finally {
|
||||||
|
await utils.sleep(100);
|
||||||
|
this.lockScroll = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get sortMethodOptions() {
|
||||||
|
return [
|
||||||
|
{label: '<span style="font-size: 150%">↑</span> Время загрузки', value: 'loadTimeDesc'},
|
||||||
|
{label: '<span style="font-size: 150%">↓</span> Время загрузки', value: 'loadTimeAsc'},
|
||||||
|
{label: '<span style="font-size: 150%">↑</span> Время чтения', value: 'touchTimeDesc'},
|
||||||
|
{label: '<span style="font-size: 150%">↓</span> Время чтения', value: 'touchTimeAsc'},
|
||||||
|
{label: '<span style="font-size: 150%">↑</span> Автор', value: 'authorDesc'},
|
||||||
|
{label: '<span style="font-size: 150%">↓</span> Автор', value: 'authorAsc'},
|
||||||
|
{label: '<span style="font-size: 150%">↑</span> Название', value: 'titleDesc'},
|
||||||
|
{label: '<span style="font-size: 150%">↓</span> Название', value: 'titleAsc'},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$emit('recent-books-close');
|
this.$emit('recent-books-close');
|
||||||
}
|
}
|
||||||
@@ -340,29 +582,34 @@ export default vueComponent(RecentBooksPage);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.recent-books-table {
|
.recent-books-scroll {
|
||||||
width: 600px;
|
width: 573px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-header {
|
||||||
|
height: 50px;
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 2px solid #aaaaaa;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
min-height: 80px;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-part {
|
||||||
|
padding: 4px 4px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.td-mp {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 4px 4px 4px 4px !important;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-mp {
|
|
||||||
margin: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border: 0;
|
|
||||||
border-left: 1px solid #ddd !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.break-word {
|
.break-word {
|
||||||
line-height: 180%;
|
line-height: 180%;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@@ -373,22 +620,47 @@ export default vueComponent(RecentBooksPage);
|
|||||||
.read-bar {
|
.read-bar {
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background-color: #aaaaaa;
|
background-color: #aaaaaa;
|
||||||
}
|
margin-bottom: 2px;
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.recent-books-table .q-table__middle {
|
|
||||||
height: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-books-table thead tr:first-child th {
|
.even {
|
||||||
position: sticky;
|
background-color: #f2f2f2;
|
||||||
z-index: 1;
|
|
||||||
top: 0;
|
|
||||||
background-color: #c1f4cd;
|
|
||||||
}
|
}
|
||||||
.recent-books-table tr:nth-child(even) {
|
|
||||||
background-color: #f8f8f8;
|
.active-book {
|
||||||
|
background-color: #b0f0b0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-parent-book {
|
||||||
|
background-color: #ffbbbb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-info-top {
|
||||||
|
line-height: 110%;
|
||||||
|
border-left: 1px solid #cccccc;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-info-bottom {
|
||||||
|
line-height: 110%;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-button {
|
||||||
|
min-width: 30px;
|
||||||
|
width: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 10px 6px 0px 3px;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,12 +8,10 @@
|
|||||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||||
|
|
||||||
<div v-show="!initStep" class="input">
|
<div v-show="!initStep" class="input">
|
||||||
<!--input ref="input"
|
<q-input
|
||||||
placeholder="что ищем"
|
ref="input" v-model="needle"
|
||||||
:value="needle" @input="needle = $event.target.value"/-->
|
|
||||||
<q-input ref="input" v-model="needle"
|
|
||||||
class="col" outlined dense
|
class="col" outlined dense
|
||||||
placeholder="что ищем"
|
placeholder="Найти"
|
||||||
@keydown="inputKeyDown"
|
@keydown="inputKeyDown"
|
||||||
/>
|
/>
|
||||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
|
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
|
||||||
@@ -108,7 +106,7 @@ class SearchPage {
|
|||||||
this.parsed = parsed;
|
this.parsed = parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.header = 'Найти';
|
this.header = 'Поиск в тексте';
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.$refs.input.focus();
|
this.$refs.input.focus();
|
||||||
this.$refs.input.select();
|
this.$refs.input.select();
|
||||||
|
|||||||
@@ -728,10 +728,10 @@ class ServerStorage {
|
|||||||
const ids = id.split('.');
|
const ids = id.split('.');
|
||||||
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||||
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||||
items[utils.fromBase58(ids[1])] = decoded;
|
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.items = items;
|
result.items = items;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window ref="window" height="95%" width="600px" @close="close">
|
<Window ref="window" width="600px" @close="close">
|
||||||
<template #header>
|
<template #header>
|
||||||
Настройки
|
Настройки
|
||||||
</template>
|
</template>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
<div v-show="tabsScrollable" class="q-pt-lg" />
|
||||||
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
|
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
|
||||||
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
|
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
|
||||||
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
|
||||||
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
||||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
||||||
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Кнопки ---------------------------------------------------------------------->
|
<!-- Кнопки ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
|
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
|
||||||
@@include('./ButtonsTab.inc');
|
@@include('./ToolBarTab.inc');
|
||||||
</div>
|
</div>
|
||||||
<!-- Управление ------------------------------------------------------------------>
|
<!-- Управление ------------------------------------------------------------------>
|
||||||
<div v-if="selectedTab == 'keys'" class="fit column">
|
<div v-if="selectedTab == 'keys'" class="fit column">
|
||||||
@@ -702,11 +702,11 @@ export default vueComponent(SettingsPage);
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-1, .label-7 {
|
.label-1, .label-3, .label-7 {
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-2, .label-3, .label-4, .label-5 {
|
.label-2, .label-4, .label-5 {
|
||||||
width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div class="part-header">Отображение</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-3"></div>
|
||||||
|
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Скрывать/показывть панель при прокрутке текста вперед/назад
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="part-header">Показывать кнопки</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||||
|
<div class="label-3"></div>
|
||||||
|
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
ref="input"
|
ref="input"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
class="q-ml-sm col"
|
class="q-ml-sm col"
|
||||||
outlined dense rounded
|
outlined dense
|
||||||
bg-color="grey-4"
|
bg-color="grey-4"
|
||||||
placeholder="Найти"
|
placeholder="Найти"
|
||||||
@click.stop
|
@click.stop
|
||||||
|
|||||||
@@ -6,29 +6,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||||
<div v-html="page1"></div>
|
<div @copy.prevent="copyText" v-html="page1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
||||||
<div v-html="page2"></div>
|
<div @copy.prevent="copyText" v-html="page2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showStatusBar" ref="statusBar" class="layout">
|
<div v-show="showStatusBar" ref="statusBar" class="layout">
|
||||||
<div v-html="statusBar"></div>
|
<div v-html="statusBar"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="clickControl" ref="layoutEvents" class="layout events"
|
<div
|
||||||
|
v-show="clickControl" ref="layoutEvents" class="layout events"
|
||||||
oncontextmenu="return false;"
|
oncontextmenu="return false;"
|
||||||
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||||
@wheel.prevent.stop="onMouseWheel"
|
@wheel.prevent.stop="onMouseWheel"
|
||||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||||
>
|
>
|
||||||
<div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
<div
|
||||||
|
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"
|
@click.prevent.stop="onStatusBarClick"
|
||||||
v-html="statusBarClickable"
|
v-html="statusBarClickable"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
<div
|
||||||
|
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
||||||
@mousedown.prevent.stop @touchstart.stop
|
@mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"
|
@click.prevent.stop="onStatusBarClick"
|
||||||
v-html="statusBarClickable"
|
v-html="statusBarClickable"
|
||||||
@@ -46,6 +49,7 @@ import vueComponent from '../../vueComponent.js';
|
|||||||
|
|
||||||
import {loadCSS} from 'fg-loadcss';
|
import {loadCSS} from 'fg-loadcss';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
import './TextPage.css';
|
import './TextPage.css';
|
||||||
|
|
||||||
@@ -62,7 +66,14 @@ const componentOptions = {
|
|||||||
watch: {
|
watch: {
|
||||||
bookPos: function() {
|
bookPos: function() {
|
||||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||||
|
|
||||||
this.draw();
|
this.draw();
|
||||||
|
|
||||||
|
if (this.userBookPosChange) {
|
||||||
|
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
|
||||||
|
this.prevBookPos = this.bookPos;
|
||||||
|
this.userBookPosChange = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bookPosSeen: function() {
|
bookPosSeen: function() {
|
||||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||||
@@ -95,6 +106,8 @@ class TextPage {
|
|||||||
lastBook = null;
|
lastBook = null;
|
||||||
bookPos = 0;
|
bookPos = 0;
|
||||||
bookPosSeen = null;
|
bookPosSeen = null;
|
||||||
|
prevBookPos = 0;
|
||||||
|
userBookPosChange = false;
|
||||||
|
|
||||||
fontStyle = null;
|
fontStyle = null;
|
||||||
fontSize = null;
|
fontSize = null;
|
||||||
@@ -151,7 +164,7 @@ class TextPage {
|
|||||||
|
|
||||||
this.$root.addEventHook('resize', async() => {
|
this.$root.addEventHook('resize', async() => {
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
await utils.sleep(500);
|
await utils.sleep(200);
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -495,12 +508,25 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onResize() {
|
async onResize() {
|
||||||
|
if (this.resizing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.resizing = true;
|
||||||
try {
|
try {
|
||||||
|
const scrolled = this.doingScrolling;
|
||||||
|
if (scrolled)
|
||||||
|
await this.stopTextScrolling();
|
||||||
|
|
||||||
this.calcDrawProps();
|
this.calcDrawProps();
|
||||||
this.setBackground();
|
this.setBackground();
|
||||||
this.draw();
|
this.draw();
|
||||||
|
|
||||||
|
if (scrolled)
|
||||||
|
this.startTextScrolling();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
|
} finally {
|
||||||
|
this.resizing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,7 +674,7 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
|
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
|
||||||
this.doEnd(true);
|
this.doEnd(true, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,7 +697,7 @@ class TextPage {
|
|||||||
this.debouncedDrawPageDividerAndOrnament();
|
this.debouncedDrawPageDividerAndOrnament();
|
||||||
|
|
||||||
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
||||||
this.doEnd(true);
|
this.doEnd(true, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,12 +933,14 @@ class TextPage {
|
|||||||
|
|
||||||
doDown() {
|
doDown() {
|
||||||
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
|
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesDown[1].begin;
|
this.bookPos = this.linesDown[1].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doUp() {
|
doUp() {
|
||||||
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
|
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesUp[1].begin;
|
this.bookPos = this.linesUp[1].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,6 +953,7 @@ class TextPage {
|
|||||||
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = true;
|
this.pageChangeDirectionDown = true;
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesDown[i].begin;
|
this.bookPos = this.linesDown[i].begin;
|
||||||
} else
|
} else
|
||||||
this.doEnd();
|
this.doEnd();
|
||||||
@@ -940,6 +969,7 @@ class TextPage {
|
|||||||
if (i >= 0 && this.linesUp.length > i) {
|
if (i >= 0 && this.linesUp.length > i) {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = false;
|
this.pageChangeDirectionDown = false;
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = this.linesUp[i].begin;
|
this.bookPos = this.linesUp[i].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -948,10 +978,11 @@ class TextPage {
|
|||||||
doHome() {
|
doHome() {
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = false;
|
this.pageChangeDirectionDown = false;
|
||||||
|
this.userBookPosChange = true;
|
||||||
this.bookPos = 0;
|
this.bookPos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
doEnd(noAni) {
|
doEnd(noAni, isUser = true) {
|
||||||
if (this.parsed.para.length && this.pageLineCount > 0) {
|
if (this.parsed.para.length && this.pageLineCount > 0) {
|
||||||
let i = this.parsed.para.length - 1;
|
let i = this.parsed.para.length - 1;
|
||||||
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
|
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
|
||||||
@@ -962,6 +993,7 @@ class TextPage {
|
|||||||
if (!noAni)
|
if (!noAni)
|
||||||
this.currentAnimation = this.pageChangeAnimation;
|
this.currentAnimation = this.pageChangeAnimation;
|
||||||
this.pageChangeDirectionDown = true;
|
this.pageChangeDirectionDown = true;
|
||||||
|
this.userBookPosChange = isUser;
|
||||||
this.bookPos = lines[i].begin;
|
this.bookPos = lines[i].begin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1201,8 +1233,54 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyText(event) {
|
||||||
|
//все это для того, чтобы правильно расставить переносы \n при копировании текста
|
||||||
|
//прямо с текущей страницы
|
||||||
|
|
||||||
|
//подготовка, вытаскиваем весь текст страницы
|
||||||
|
const lines = this.getLines(this.bookPos);
|
||||||
|
const decodedLines = [];
|
||||||
|
for (const line of lines.linesDown) {
|
||||||
|
let lineText = '';
|
||||||
|
for (const part of line.parts) {
|
||||||
|
lineText += part.text;
|
||||||
|
}
|
||||||
|
decodedLines.push({text: he.decode(lineText), first: line.first});
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const findDecoded = (line) => {
|
||||||
|
for (let j = i; j < decodedLines.length; j++) {
|
||||||
|
const decoded = decodedLines[j];
|
||||||
|
if (decoded.text.indexOf(line) >= 0) {
|
||||||
|
i = j;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const splitted = selection.toString().split(/[\n\r]/);
|
||||||
|
|
||||||
|
let filtered = '';
|
||||||
|
//формируем filtered, учитывая переносы из decodedLines
|
||||||
|
for (const line of splitted) {
|
||||||
|
const found = findDecoded(line);
|
||||||
|
if (found && found.first) {
|
||||||
|
filtered += (filtered ? '\n' : '') + line;
|
||||||
|
} else {
|
||||||
|
filtered += (filtered ? '\r ' : '') + line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//маленькие хитрости, убираем переносы по слогам
|
||||||
|
filtered = filtered.replace(/-\r /g, '').replace(/\r /g, ' ');
|
||||||
|
|
||||||
|
event.clipboardData.setData('text/plain', filtered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vueComponent(TextPage);
|
export default vueComponent(TextPage);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
|
import path from 'path-browserify';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
|
||||||
const maxDataSize = 500*1024*1024;//compressed bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
|
const maxRecentLength = 5000;
|
||||||
|
|
||||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
@@ -17,9 +19,6 @@ const bmDataStore = localForage.createInstance({
|
|||||||
});
|
});
|
||||||
|
|
||||||
//список недавно открытых книг
|
//список недавно открытых книг
|
||||||
const bmRecentStoreOld = localForage.createInstance({
|
|
||||||
name: 'bmRecentStore'
|
|
||||||
});
|
|
||||||
const bmRecentStoreNew = localForage.createInstance({
|
const bmRecentStoreNew = localForage.createInstance({
|
||||||
name: 'bmRecentStoreNew'
|
name: 'bmRecentStoreNew'
|
||||||
});
|
});
|
||||||
@@ -39,7 +38,7 @@ class BookManager {
|
|||||||
|
|
||||||
this.saveRecentItem = _.debounce(() => {
|
this.saveRecentItem = _.debounce(() => {
|
||||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||||
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
|
||||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||||
}, 200, {maxWait: 300});
|
}, 200, {maxWait: 300});
|
||||||
|
|
||||||
@@ -54,6 +53,9 @@ class BookManager {
|
|||||||
if (this.recentItem)
|
if (this.recentItem)
|
||||||
this.recent[this.recentItem.key] = this.recentItem;
|
this.recent[this.recentItem.key] = this.recentItem;
|
||||||
|
|
||||||
|
//конвертируем в новые ключи
|
||||||
|
await this.convertRecent();
|
||||||
|
|
||||||
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||||
if (this.recentLastKey) {
|
if (this.recentLastKey) {
|
||||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||||
@@ -70,6 +72,40 @@ class BookManager {
|
|||||||
this.loadStored();//no await
|
this.loadStored();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: убрать в 2025г
|
||||||
|
async convertRecent() {
|
||||||
|
const converted = await bmRecentStoreNew.getItem('recent-converted');
|
||||||
|
|
||||||
|
if (converted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newRecent = {};
|
||||||
|
for (const book of Object.values(this.recent)) {
|
||||||
|
|
||||||
|
if (!book.path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKey = this.keyFromPath(book.path);
|
||||||
|
|
||||||
|
newRecent[newKey] = _.cloneDeep(book);
|
||||||
|
newRecent[newKey].key = newKey;
|
||||||
|
if (!newRecent[newKey].loadTime)
|
||||||
|
newRecent[newKey].loadTime = newRecent[newKey].addTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recent = newRecent;
|
||||||
|
|
||||||
|
//console.log(converted);
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
this.saveRecent();
|
||||||
|
this.emit('recent-changed');
|
||||||
|
this.emit('set-recent');
|
||||||
|
await bmRecentStoreNew.setItem('recent-converted', true);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
//Ленивая асинхронная загрузка bmMetaStore
|
//Ленивая асинхронная загрузка bmMetaStore
|
||||||
async loadStored() {
|
async loadStored() {
|
||||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||||
@@ -196,8 +232,8 @@ class BookManager {
|
|||||||
|
|
||||||
async addBook(newBook, callback) {
|
async addBook(newBook, callback) {
|
||||||
let meta = {url: newBook.url, path: newBook.path};
|
let meta = {url: newBook.url, path: newBook.path};
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
meta.addTime = Date.now();
|
meta.addTime = Date.now();//время добавления в кеш
|
||||||
|
|
||||||
const cb = (perc) => {
|
const cb = (perc) => {
|
||||||
const p = Math.round(30*perc/100);
|
const p = Math.round(30*perc/100);
|
||||||
@@ -232,10 +268,10 @@ class BookManager {
|
|||||||
async hasBookParsed(meta) {
|
async hasBookParsed(meta) {
|
||||||
if (!this.books)
|
if (!this.books)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.url)
|
if (!meta.path)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
|
||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
@@ -250,8 +286,12 @@ class BookManager {
|
|||||||
|
|
||||||
async getBook(meta, callback) {
|
async getBook(meta, callback) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
|
|
||||||
|
if (!meta.path)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
|
|
||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
@@ -261,11 +301,6 @@ class BookManager {
|
|||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
|
||||||
if (meta.path && result && meta.path != result.path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result && !result.parsed) {
|
if (result && !result.parsed) {
|
||||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||||
callback(5);
|
callback(5);
|
||||||
@@ -325,10 +360,20 @@ class BookManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFromUrl(url) {
|
/*keyFromUrl(url) {
|
||||||
return utils.stringToHex(url);
|
return utils.stringToHex(url);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
keyFromPath(bookPath) {
|
||||||
|
return path.basename(bookPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keysEqual(bookPath1, bookPath2) {
|
||||||
|
if (bookPath1 === undefined || bookPath2 === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
|
||||||
|
}
|
||||||
//-- recent --------------------------------------------------------------
|
//-- recent --------------------------------------------------------------
|
||||||
async recentSetItem(item = null, skipCheck = false) {
|
async recentSetItem(item = null, skipCheck = false) {
|
||||||
const rev = await bmRecentStoreNew.getItem('rev');
|
const rev = await bmRecentStoreNew.getItem('rev');
|
||||||
@@ -369,7 +414,10 @@ class BookManager {
|
|||||||
|
|
||||||
async setRecentBook(value) {
|
async setRecentBook(value) {
|
||||||
let result = this.metaOnly(value);
|
let result = this.metaOnly(value);
|
||||||
result.touchTime = Date.now();
|
result.touchTime = Date.now();//время последнего чтения
|
||||||
|
if (!result.loadTime)
|
||||||
|
result.loadTime = Date.now();//время загрузки файла
|
||||||
|
|
||||||
result.deleted = 0;
|
result.deleted = 0;
|
||||||
|
|
||||||
if (this.recent[result.key]) {
|
if (this.recent[result.key]) {
|
||||||
@@ -401,7 +449,7 @@ class BookManager {
|
|||||||
const sorted = this.getSortedRecent();
|
const sorted = this.getSortedRecent();
|
||||||
|
|
||||||
let isDel = false;
|
let isDel = false;
|
||||||
for (let i = 1000; i < sorted.length; i++) {
|
for (let i = maxRecentLength; i < sorted.length; i++) {
|
||||||
delete this.recent[sorted[i].key];
|
delete this.recent[sorted[i].key];
|
||||||
isDel = true;
|
isDel = true;
|
||||||
}
|
}
|
||||||
@@ -421,7 +469,7 @@ class BookManager {
|
|||||||
|
|
||||||
let max = 0;
|
let max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
for (let key in this.recent) {
|
for (const key in this.recent) {
|
||||||
const book = this.recent[key];
|
const book = this.recent[key];
|
||||||
if (!book.deleted && book.touchTime > max) {
|
if (!book.deleted && book.touchTime > max) {
|
||||||
max = book.touchTime;
|
max = book.touchTime;
|
||||||
@@ -452,6 +500,43 @@ class BookManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findRecentByUrlAndPath(url, bookPath) {
|
||||||
|
if (bookPath) {
|
||||||
|
const key = this.keyFromPath(bookPath);
|
||||||
|
const book = this.recent[key];
|
||||||
|
if (book && !book.deleted)
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max = 0;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
for (const key in this.recent) {
|
||||||
|
const book = this.recent[key];
|
||||||
|
if (!book.deleted && book.url == url && book.loadTime > max) {
|
||||||
|
max = book.loadTime;
|
||||||
|
result = book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
findRecentBySameBookKey(sameKey) {
|
||||||
|
let max = 0;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
for (const key in this.recent) {
|
||||||
|
const book = this.recent[key];
|
||||||
|
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
|
||||||
|
max = book.loadTime;
|
||||||
|
result = book;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async setRecent(value) {
|
async setRecent(value) {
|
||||||
const mergedRecent = _.cloneDeep(this.recent);
|
const mergedRecent = _.cloneDeep(this.recent);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
{
|
{
|
||||||
version: '0.11.4',
|
version: '0.11.7',
|
||||||
releaseDate: '2022-04-14',
|
releaseDate: '2022-07-12',
|
||||||
showUntil: '2022-04-13',
|
showUntil: '2022-07-19',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
|
||||||
|
<li>изменения в окне загруженных книг:</li>
|
||||||
|
<ul>
|
||||||
|
<li>добавлена группировка по версиям файла одной и той же книги</li>
|
||||||
|
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
|
||||||
|
<li>добавлены различные методы сортировки списка загруженных книг</li>
|
||||||
|
<li>нумерация всегда осуществляется по времени загрузки</li>
|
||||||
|
</ul>
|
||||||
|
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.6',
|
||||||
|
releaseDate: '2022-07-02',
|
||||||
|
showUntil: '2022-07-01',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
|
||||||
|
<li>актуализация используемых пакетов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.5',
|
||||||
|
releaseDate: '2022-04-15',
|
||||||
|
showUntil: '2022-04-14',
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -13,32 +49,6 @@ export const versionHistory = [
|
|||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
version: '0.11.3',
|
|
||||||
releaseDate: '2022-03-29',
|
|
||||||
showUntil: '2022-03-28',
|
|
||||||
content:
|
|
||||||
`
|
|
||||||
<ul>
|
|
||||||
<li>исправления багов</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
`
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
version: '0.11.2',
|
|
||||||
releaseDate: '2022-01-11',
|
|
||||||
showUntil: '2022-01-10',
|
|
||||||
content:
|
|
||||||
`
|
|
||||||
<ul>
|
|
||||||
<li>исправления багов</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
`
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
version: '0.11.1',
|
version: '0.11.1',
|
||||||
releaseDate: '2021-12-03',
|
releaseDate: '2021-12-03',
|
||||||
|
|||||||
@@ -55,6 +55,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!--------------------------------------------------->
|
||||||
|
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
|
||||||
|
<div class="header row">
|
||||||
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
|
<div v-html="caption"></div>
|
||||||
|
</div>
|
||||||
|
<div class="close-icon column justify-center items-center">
|
||||||
|
<q-btn v-close-popup flat round dense>
|
||||||
|
<q-icon name="la la-times" size="18px"></q-icon>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mx-md">
|
||||||
|
<div v-html="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons row justify-end q-pa-md">
|
||||||
|
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||||
|
Нет
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||||
|
Да
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--------------------------------------------------->
|
<!--------------------------------------------------->
|
||||||
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
@@ -262,6 +290,23 @@ class StdDialog {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
askYesNo(message, caption, opts) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.init(message, caption, opts);
|
||||||
|
|
||||||
|
this.hideTrigger = () => {
|
||||||
|
if (this.ok) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'askYesNo';
|
||||||
|
this.active = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
prompt(message, caption, opts) {
|
prompt(message, caption, opts) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.enableValidator = false;
|
this.enableValidator = false;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default vueComponent(Window);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: linear-gradient(to bottom right, green, #59B04F);
|
background: linear-gradient(to bottom right, #007000, #59B04F);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
@@ -161,8 +161,8 @@ export default vueComponent(Window);
|
|||||||
.header-text {
|
.header-text {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: yellow;
|
color: #FFFFA0;
|
||||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,8 @@ export default vueComponent(Window);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.close-button:hover {
|
.close-button:hover {
|
||||||
background-color: #69C05F;
|
color: white;
|
||||||
|
background-color: #FF3030;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
|||||||
import {QDialog} from 'quasar/src/components/dialog';
|
import {QDialog} from 'quasar/src/components/dialog';
|
||||||
import {QChip} from 'quasar/src/components/chip';
|
import {QChip} from 'quasar/src/components/chip';
|
||||||
import {QTree} from 'quasar/src/components/tree';
|
import {QTree} from 'quasar/src/components/tree';
|
||||||
|
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
|
||||||
|
|
||||||
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
@@ -62,6 +64,7 @@ const components = {
|
|||||||
QChip,
|
QChip,
|
||||||
QTree,
|
QTree,
|
||||||
//QExpansionItem,
|
//QExpansionItem,
|
||||||
|
QVirtualScroll,
|
||||||
};
|
};
|
||||||
|
|
||||||
//directives
|
//directives
|
||||||
|
|||||||
53
client/share/LockQueue.js
Normal file
53
client/share/LockQueue.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
class LockQueue {
|
||||||
|
constructor(queueSize) {
|
||||||
|
this.queueSize = queueSize;
|
||||||
|
this.freed = true;
|
||||||
|
this.waitingQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//async
|
||||||
|
get(take = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.freed) {
|
||||||
|
if (take)
|
||||||
|
this.freed = false;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.waitingQueue.length < this.queueSize) {
|
||||||
|
this.waitingQueue.push({resolve, reject});
|
||||||
|
} else {
|
||||||
|
reject(new Error('Lock queue is too long'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ret() {
|
||||||
|
if (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().resolve();
|
||||||
|
} else {
|
||||||
|
this.freed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//async
|
||||||
|
wait() {
|
||||||
|
return this.get(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
retAll() {
|
||||||
|
while (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errAll(error = 'rejected') {
|
||||||
|
while (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().reject(new Error(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LockQueue;
|
||||||
@@ -90,7 +90,7 @@ export function toBase58(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fromBase58(data) {
|
export function fromBase58(data) {
|
||||||
return bs58.decode(data);
|
return Buffer.from(bs58.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
//base-x слишком тормозит, используем sjcl
|
//base-x слишком тормозит, используем sjcl
|
||||||
@@ -107,6 +107,10 @@ export function fromBase64(data) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasProp(obj, prop) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjDiff(oldObj, newObj, opts = {}) {
|
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||||
const {
|
const {
|
||||||
exclude = [],
|
exclude = [],
|
||||||
@@ -126,7 +130,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
|||||||
for (const key of Object.keys(oldObj)) {
|
for (const key of Object.keys(oldObj)) {
|
||||||
const kp = `${keyPath}${key}`;
|
const kp = `${keyPath}${key}`;
|
||||||
|
|
||||||
if (newObj.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||||
if (ex.has(kp))
|
if (ex.has(kp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -149,7 +153,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
|||||||
if (exAdd.has(kp))
|
if (exAdd.has(kp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!oldObj.hasOwnProperty(key)) {
|
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
|
||||||
result.add[key] = _.cloneDeep(newObj[key]);
|
result.add[key] = _.cloneDeep(newObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +217,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
|
|||||||
|
|
||||||
const change = diff.change;
|
const change = diff.change;
|
||||||
for (const key of Object.keys(change)) {
|
for (const key of Object.keys(change)) {
|
||||||
if (result.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||||
if (_.isObject(change[key])) {
|
if (_.isObject(change[key])) {
|
||||||
result[key] = applyObjDiff(result[key], change[key], opts);
|
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const readerActions = {
|
|||||||
'offlineMode': 'Автономный режим (без интернета)',
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
'contents': 'Оглавление/закладки',
|
'contents': 'Оглавление/закладки',
|
||||||
'libs': 'Сетевая библиотека',
|
'libs': 'Сетевая библиотека',
|
||||||
'recentBooks': 'Открыть недавние',
|
'recentBooks': 'Показать загруженные',
|
||||||
'switchToolbar': 'Показать/скрыть панель управления',
|
'switchToolbar': 'Показать/скрыть панель управления',
|
||||||
'donate': '',
|
'donate': '',
|
||||||
'bookBegin': 'В начало книги',
|
'bookBegin': 'В начало книги',
|
||||||
@@ -70,7 +70,7 @@ const hotKeys = [
|
|||||||
{name: 'scrolling', codes: ['Z']},
|
{name: 'scrolling', codes: ['Z']},
|
||||||
{name: 'setPosition', codes: ['P']},
|
{name: 'setPosition', codes: ['P']},
|
||||||
{name: 'search', codes: ['Ctrl+F']},
|
{name: 'search', codes: ['Ctrl+F']},
|
||||||
{name: 'copyText', codes: ['Ctrl+C']},
|
{name: 'copyText', codes: ['Ctrl+Space']},
|
||||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||||
{name: 'refresh', codes: ['R']},
|
{name: 'refresh', codes: ['R']},
|
||||||
{name: 'contents', codes: ['C']},
|
{name: 'contents', codes: ['C']},
|
||||||
@@ -185,8 +185,12 @@ const settingDefaults = {
|
|||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
|
toolBarHideOnScroll: true,
|
||||||
userHotKeys: {},
|
userHotKeys: {},
|
||||||
userWallpapers: [],
|
userWallpapers: [],
|
||||||
|
|
||||||
|
recentShowSameBook: false,
|
||||||
|
recentSortMethod: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
@@ -222,9 +226,6 @@ const libsDefaults = {
|
|||||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||||
]},
|
]},
|
||||||
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
|
||||||
{l: 'https://flibs.in', c: 'Flibs'},
|
|
||||||
]},
|
|
||||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||||
]},
|
]},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ server {
|
|||||||
|
|
||||||
server_name liberama.top;
|
server_name liberama.top;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -63,7 +63,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name b.liberama.top;
|
server_name b.liberama.top;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -140,5 +140,6 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://fantasy-worlds.org;
|
proxy_pass http://fantasy-worlds.org;
|
||||||
|
proxy_hide_header x-frame-options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if ! pgrep -x "liberama" > /dev/null ; then
|
if ! pgrep -x "liberama" > /dev/null ; then
|
||||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null"
|
||||||
else
|
else
|
||||||
echo "Process 'liberama' already running"
|
echo "Process 'liberama' already running"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ server {
|
|||||||
|
|
||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
proxy_read_timeout 1h;
|
proxy_read_timeout 1h;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
@@ -52,7 +52,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name old.omnireader.ru;
|
server_name old.omnireader.ru;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 100m;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null & disown"
|
||||||
sudo service cron start
|
sudo service cron start
|
||||||
|
|||||||
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",
|
"name": "Liberama",
|
||||||
"version": "0.11.4",
|
"version": "0.11.7",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
@@ -28,17 +28,17 @@
|
|||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.16.0",
|
||||||
"@vue/compiler-sfc": "^3.2.22",
|
"@vue/compiler-sfc": "^3.2.22",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"copy-webpack-plugin": "^9.1.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.5.1",
|
"css-loader": "^6.5.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.1.3",
|
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^9.2.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"mini-css-extract-plugin": "^2.4.4",
|
"mini-css-extract-plugin": "^2.4.4",
|
||||||
"pkg": "^5.5.1",
|
"pkg": "^5.5.1",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"terser-webpack-plugin": "^5.2.5",
|
||||||
"vue-eslint-parser": "^8.0.1",
|
"vue-eslint-parser": "^9.0.3",
|
||||||
"vue-loader": "^16.8.3",
|
"vue-loader": "^17.0.0",
|
||||||
"vue-style-loader": "^4.1.3",
|
"vue-style-loader": "^4.1.3",
|
||||||
"webpack": "^5.64.1",
|
"webpack": "^5.64.1",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack-cli": "^4.9.1",
|
||||||
@@ -50,25 +50,24 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.12.0",
|
"@quasar/extras": "^1.12.0",
|
||||||
"@vue/compat": "^3.2.21",
|
"@vue/compat": "^3.2.21",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.27.2",
|
||||||
"base-x": "^3.0.9",
|
"base-x": "^4.0.0",
|
||||||
"chardet": "^1.4.0",
|
"chardet": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fg-loadcss": "^3.1.0",
|
"fg-loadcss": "^3.1.0",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"got": "^11.8.2",
|
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jembadb": "^2.3.0",
|
"jembadb": "^3.0.8",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.5-lts.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pidusage": "^3.0.0",
|
"pidusage": "^3.0.0",
|
||||||
"quasar": "^2.3.2",
|
"quasar": "^2.7.5",
|
||||||
"safe-buffer": "^5.2.1",
|
"safe-buffer": "^5.2.1",
|
||||||
"sanitize-html": "^2.5.3",
|
"sanitize-html": "^2.5.3",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
@@ -77,8 +76,8 @@
|
|||||||
"sqlite3": "^5.0.2",
|
"sqlite3": "^5.0.2",
|
||||||
"tar-fs": "^2.1.1",
|
"tar-fs": "^2.1.1",
|
||||||
"unbzip2-stream": "^1.4.3",
|
"unbzip2-stream": "^1.4.3",
|
||||||
"vue": "^3.2.22",
|
"vue": "^3.2.37",
|
||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.1.1",
|
||||||
"vuex": "^4.0.2",
|
"vuex": "^4.0.2",
|
||||||
"vuex-persistedstate": "^4.1.0",
|
"vuex-persistedstate": "^4.1.0",
|
||||||
"webdav": "^4.7.0",
|
"webdav": "^4.7.0",
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ module.exports = {
|
|||||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||||
|
|
||||||
useExternalBookConverter: false,
|
useExternalBookConverter: false,
|
||||||
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
|
acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png',
|
||||||
|
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch'],
|
||||||
|
|
||||||
db: [
|
db: [
|
||||||
{
|
{
|
||||||
@@ -48,7 +49,7 @@ module.exports = {
|
|||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
serverName: '1',
|
serverName: '1',
|
||||||
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader'
|
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top'
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: '33080',
|
port: '33080',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,60 +25,7 @@ class WorkerController extends BaseController {
|
|||||||
res.status(400).send({error});
|
res.status(400).send({error});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: удалить бесполезную getStateFinish
|
|
||||||
async getStateFinish(req, res) {
|
|
||||||
const request = req.body;
|
|
||||||
let error = '';
|
|
||||||
try {
|
|
||||||
if (!request.workerId)
|
|
||||||
throw new Error(`key 'workerId' is wrong`);
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'text/json; charset=utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
const splitter = '-- aod2t5hDXU32bUFyqlFE next status --';
|
|
||||||
const refreshPause = 200;
|
|
||||||
let i = 0;
|
|
||||||
let prevProgress = -1;
|
|
||||||
let prevState = '';
|
|
||||||
let state;
|
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
|
||||||
state = this.workerState.getState(request.workerId);
|
|
||||||
if (!state) break;
|
|
||||||
|
|
||||||
res.write(splitter + JSON.stringify(state));
|
|
||||||
res.flush();
|
|
||||||
|
|
||||||
if (state.state != 'finish' && state.state != 'error')
|
|
||||||
await utils.sleep(refreshPause);
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
|
|
||||||
i++;
|
|
||||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
|
||||||
res.write(splitter + JSON.stringify({state: 'error', error: 'Слишком долгое время ожидания'}));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
|
||||||
prevProgress = state.progress;
|
|
||||||
prevState = state.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
res.write(splitter + JSON.stringify({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
error = e.message;
|
|
||||||
}
|
|
||||||
//bad request
|
|
||||||
res.status(400).send({error});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WorkerController;
|
module.exports = WorkerController;
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ class AsyncExit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_init(signals, codeOnSignal) {
|
_init(signals, codeOnSignal) {
|
||||||
const runSingalCallbacks = async(signal) => {
|
const runSingalCallbacks = async(signal, err, origin) => {
|
||||||
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
||||||
try {
|
try {
|
||||||
await signalCallback(signal);
|
await signalCallback(signal, err, origin);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,8 @@ class AsyncExit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const signal of signals) {
|
for (const signal of signals) {
|
||||||
process.once(signal, async() => {
|
process.once(signal, async(err, origin) => {
|
||||||
await runSingalCallbacks(signal);
|
await runSingalCallbacks(signal, err, origin);
|
||||||
this.exit(codeOnSignal);
|
this.exit(codeOnSignal);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
|
|
||||||
class FileDownloader {
|
class FileDownloader {
|
||||||
constructor(limitDownloadSize = 0) {
|
constructor(limitDownloadSize = 0) {
|
||||||
@@ -7,54 +7,82 @@ class FileDownloader {
|
|||||||
|
|
||||||
async load(url, callback, abort) {
|
async load(url, callback, abort) {
|
||||||
let errMes = '';
|
let errMes = '';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'
|
'user-agent': '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 {
|
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 (estSize > this.limitDownloadSize) {
|
||||||
|
throw new Error('Файл слишком большой');
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevProg = 0;
|
||||||
|
let transferred = 0;
|
||||||
|
|
||||||
|
const download = this.streamToBuffer(res.data, (chunk) => {
|
||||||
|
transferred += chunk.length;
|
||||||
|
if (this.limitDownloadSize) {
|
||||||
|
if (transferred > this.limitDownloadSize) {
|
||||||
|
errMes = 'Файл слишком большой';
|
||||||
|
res.request.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prog = 0;
|
||||||
|
if (estSize)
|
||||||
|
prog = Math.round(transferred/estSize*100);
|
||||||
|
else
|
||||||
|
prog = Math.round(transferred/(transferred + 200000)*100);
|
||||||
|
|
||||||
|
if (prog != prevProg && callback)
|
||||||
|
callback(prog);
|
||||||
|
prevProg = prog;
|
||||||
|
|
||||||
|
if (abort && abort()) {
|
||||||
|
errMes = 'abort';
|
||||||
|
res.request.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await download;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errMes = (errMes ? errMes : error.message);
|
errMes = (errMes ? errMes : error.message);
|
||||||
throw new Error(errMes);
|
throw new Error(errMes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = [];
|
this.outputBuffer = [];
|
||||||
|
|
||||||
await this.flushImpl(this.data)
|
await this.flushImpl(this.data)
|
||||||
.catch(e => { console.log(e); ayncExit.exit(1); } );
|
.catch(e => { console.error(`Logger error: ${e}`); ayncExit.exit(1); } );
|
||||||
this.flushing = false;
|
this.flushing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,8 +188,8 @@ class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
ayncExit.onSignal((signal) => {
|
ayncExit.onSignal((signal, err) => {
|
||||||
this.log(LM_FATAL, `Signal ${signal} received, exiting...`);
|
this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", exiting...`);
|
||||||
});
|
});
|
||||||
ayncExit.addAfter(this.close.bind(this));
|
ayncExit.addAfter(this.close.bind(this));
|
||||||
}
|
}
|
||||||
@@ -218,6 +218,8 @@ class Logger {
|
|||||||
} else {
|
} else {
|
||||||
console.log(mes);
|
console.log(mes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const chardet = require('chardet');
|
|||||||
function getEncoding(buf) {
|
function getEncoding(buf) {
|
||||||
let selected = getEncodingLite(buf);
|
let selected = getEncodingLite(buf);
|
||||||
|
|
||||||
if (selected == 'ISO-8859-5') {
|
if (selected == 'ISO-8859-5' && buf.length > 10) {
|
||||||
const charsetAll = chardet.analyse(buf.slice(0, 20000));
|
const charsetAll = chardet.analyse(buf.slice(0, 20000));
|
||||||
for (const charset of charsetAll) {
|
for (const charset of charsetAll) {
|
||||||
if (charset.name.indexOf('ISO-8859') < 0) {
|
if (charset.name.indexOf('ISO-8859') < 0) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
@@ -20,25 +21,30 @@ class JembaReaderStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doAction(act) {
|
async doAction(act) {
|
||||||
if (!_.isObject(act.items))
|
try {
|
||||||
throw new Error('items is not an object');
|
if (!_.isObject(act.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
switch (act.action) {
|
switch (act.action) {
|
||||||
case 'check':
|
case 'check':
|
||||||
result = await this.checkItems(act.items);
|
result = await this.checkItems(act.items);
|
||||||
break;
|
break;
|
||||||
case 'get':
|
case 'get':
|
||||||
result = await this.getItems(act.items);
|
result = await this.getItems(act.items);
|
||||||
break;
|
break;
|
||||||
case 'set':
|
case 'set':
|
||||||
result = await this.setItems(act.items, act.force);
|
result = await this.setItems(act.items, act.force);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown action');
|
throw new Error('Unknown action');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
log(LM_ERR, `JembaReaderStorage: ${e.message}`);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkItems(items) {
|
async checkItems(items) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function toBase36(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fromBase36(data) {
|
function fromBase36(data) {
|
||||||
return bs36.decode(data);
|
return Buffer.from(bs36.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function bufferRemoveZeroes(buf) {
|
function bufferRemoveZeroes(buf) {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ function initRoutes(app, wss, config) {
|
|||||||
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
|
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
|
||||||
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
|
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
|
||||||
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
|
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
|
||||||
['POST', '/api/worker/get-state-finish', worker.getStateFinish.bind(worker), [aAll], {}],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
//to app
|
//to app
|
||||||
|
|||||||
Reference in New Issue
Block a user