Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fa891b4fc | ||
|
|
6cb7412cf3 | ||
|
|
157322834b | ||
|
|
1a13a0fee1 | ||
|
|
37256255bf | ||
|
|
75e01c899e | ||
|
|
ef0d6eab89 | ||
|
|
5d54b1b0f4 | ||
|
|
522f953b4f | ||
|
|
15f02c7115 | ||
|
|
174c877eee | ||
|
|
fd9ec736d7 | ||
|
|
2c94025ba3 | ||
|
|
bfadf35c40 | ||
|
|
f3b69caa12 | ||
|
|
18a83a5b0b | ||
|
|
bd9669b782 | ||
|
|
e05713aa7f | ||
|
|
bc3e1f0a6f | ||
|
|
063d01b5ca | ||
|
|
81c38d7749 | ||
|
|
a29842b084 | ||
|
|
bb5adcdaf6 | ||
|
|
537e17a219 | ||
|
|
03ce50153e | ||
|
|
15d01ad7fc | ||
|
|
e2b29e2c2f | ||
|
|
ce7ae84e0f | ||
|
|
01eb545f15 | ||
|
|
706738c7f1 | ||
|
|
6afa78cde9 | ||
|
|
71f5710bba | ||
|
|
0d87043f91 | ||
|
|
e25375fb7a | ||
|
|
41822999c8 | ||
|
|
07444bc7c2 | ||
|
|
ec48e5b0b7 | ||
|
|
e8e2e9297f | ||
|
|
4f871dd5ca | ||
|
|
f5f07a591a | ||
|
|
4c11e6918f | ||
|
|
403b9c0508 | ||
|
|
ee8ba75371 | ||
|
|
a2773fb180 | ||
|
|
ca36d588fc | ||
|
|
1e65707b7f | ||
|
|
eddf34ce55 | ||
|
|
0fb43aa33c | ||
|
|
b273b02da4 | ||
|
|
0b997f9673 | ||
|
|
bdb2ae57a8 | ||
|
|
b5e563679a | ||
|
|
992c104262 | ||
|
|
555154031e | ||
|
|
acb083e429 | ||
|
|
4a527d192d | ||
|
|
39c3bf17dd | ||
|
|
afc8c84f41 | ||
|
|
a085e04c4d | ||
|
|
2f82b0db34 | ||
|
|
0124c2b17d | ||
|
|
d2cfbbc9f3 | ||
|
|
c59f48822c | ||
|
|
b2d6584c4a | ||
|
|
8f7cafb240 | ||
|
|
08fd0f15ff | ||
|
|
dbb1bfe587 | ||
|
|
fe4b7a5a85 | ||
|
|
d8df5d76e5 | ||
|
|
b65dcc5ade | ||
|
|
a5c387a19e | ||
|
|
07c38d9a9f | ||
|
|
20ac8a444b | ||
|
|
7b601c9c7f | ||
|
|
8d2f74daa4 | ||
|
|
01e82dca5f | ||
|
|
094bb407ed | ||
|
|
338baa55ec | ||
|
|
d06d20a33e | ||
|
|
d46ba6b92b | ||
|
|
ec2639039d | ||
|
|
3a211ded2e | ||
|
|
c2131e3654 | ||
|
|
594fb59395 | ||
|
|
f44378ec84 | ||
|
|
0f6b366f62 | ||
|
|
8d0a5997ee | ||
|
|
347cb3417e | ||
|
|
337fcebd10 | ||
|
|
e057b130e9 | ||
|
|
19a0765a1a | ||
|
|
b81cd3240b | ||
|
|
1e6105b076 | ||
|
|
d8d89b3463 | ||
|
|
459564cb2d | ||
|
|
084df35184 | ||
|
|
81912babeb | ||
|
|
3943fc7d95 | ||
|
|
3999dc930c | ||
|
|
d4dea16456 | ||
|
|
ed38cb33a5 | ||
|
|
4cf5a0f4c8 | ||
|
|
8f0d526af2 | ||
|
|
6ca3881841 |
@@ -66,7 +66,7 @@ class Reader {
|
||||
await utils.sleep(refreshPause);
|
||||
|
||||
i++;
|
||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||
throw new Error('Слишком долгое время ожидания');
|
||||
}
|
||||
//проверка воркера
|
||||
@@ -181,9 +181,8 @@ class Reader {
|
||||
maxUploadFileSize = 10*1024*1024;
|
||||
if (file.size > maxUploadFileSize)
|
||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('file', file, file.name);
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
|
||||
@@ -128,6 +128,10 @@ class App extends Vue {
|
||||
|
||||
this.setAppTitle();
|
||||
(async() => {
|
||||
//запросим persistent storage
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist();
|
||||
}
|
||||
await this.routerReady();
|
||||
this.redirectIfNeeded();
|
||||
})();
|
||||
|
||||
@@ -408,7 +408,7 @@ class ExternalLibs extends Vue {
|
||||
}
|
||||
|
||||
get header() {
|
||||
let result = (this.ready ? 'Библиотека' : 'Загрузка...');
|
||||
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
|
||||
if (this.ready && this.selectedLink) {
|
||||
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||
}
|
||||
@@ -787,12 +787,17 @@ class ExternalLibs extends Vue {
|
||||
|
||||
showHelp() {
|
||||
this.$root.stdDialog.alert(`
|
||||
<p>Окно 'Библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
|
||||
что особенно актуально для мобильных устройств.</p>
|
||||
|
||||
<p>'Библиотека' разрешает свободный доступ к сайту flibusta.is. Имеется возможность управлять закладками
|
||||
<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
|
||||
что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
|
||||
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
|
||||
к сожалению, в нем открываются не все страницы.
|
||||
к сожалению, в нем открываются не все страницы.</p>
|
||||
|
||||
<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
|
||||
|
||||
<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
|
||||
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
|
||||
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
|
||||
к третьим лицам.
|
||||
</p>
|
||||
|
||||
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
|
||||
|
||||
@@ -24,36 +24,34 @@
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
||||
<div>
|
||||
<div class="row" v-for="item in contents" :key="item.key">
|
||||
<q-expansion-item v-if="item.list.length"
|
||||
class="item separator-bottom"
|
||||
expand-icon-toggle
|
||||
switch-toggle-side
|
||||
expand-icon="la la-arrow-circle-down"
|
||||
>
|
||||
<template slot="header">
|
||||
<div class="row no-wrap clickable" style="width: 465px" @click="setBookPos(item.offset)">
|
||||
<div :style="item.style"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
|
||||
<div class="column justify-center">{{ item.perc }}%</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<q-item class="subitem separator-top column justify-center" v-for="subitem in item.list" :key="subitem.key">
|
||||
<div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(subitem.offset)">
|
||||
<div :style="subitem.style"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="subitem.label"></div>
|
||||
<div class="column justify-center">{{ subitem.perc }}%</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
<q-item v-else class="item separator-bottom">
|
||||
<div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(item.offset)">
|
||||
<div :style="item.style"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
|
||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||
<div class="row item q-px-sm no-wrap">
|
||||
<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"/>
|
||||
</div>
|
||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
|
||||
</div>
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div :style="item.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||
<div class="column justify-center">{{ item.perc }}%</div>
|
||||
</div>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
||||
<div v-for="subitem in item.list" :key="subitem.key" class="row subitem q-px-sm no-wrap">
|
||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||
<div class="no-expand-button"></div>
|
||||
<div :style="subitem.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
|
||||
<div class="column justify-center">{{ subitem.perc }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
|
||||
Оглавление отсутствует
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,7 +72,7 @@ import Component from 'vue-class-component';
|
||||
//import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
//import * as utils from '../../../share/utils';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
@@ -93,25 +91,19 @@ class ContentsPage extends Vue {
|
||||
async init(currentBook, parsed) {
|
||||
this.$refs.window.init();
|
||||
|
||||
if (this.parsed != parsed) {
|
||||
this.contents = [];
|
||||
await this.$nextTick();
|
||||
this.parsed = parsed;
|
||||
}
|
||||
//закладки
|
||||
|
||||
const prepareLabel = (title, bolder = false) => {
|
||||
let titleParts = title.split('<p>');
|
||||
const textParts = titleParts.filter(v => v).map(v => `<div>${v.replace(/(<([^>]+)>)/ig, '')}</div>`);
|
||||
if (bolder && textParts.length > 1)
|
||||
textParts[0] = `<b>${textParts[0]}</b>`;
|
||||
return textParts.join('');
|
||||
}
|
||||
//далее формаирование оглавления
|
||||
if (this.parsed == parsed)
|
||||
return;
|
||||
|
||||
this.parsed = parsed;
|
||||
this.contents = [];
|
||||
await this.$nextTick();
|
||||
|
||||
const insetStyle = inset => `width: ${inset*20}px`;
|
||||
const pc = parsed.contents;
|
||||
const newpc = [];
|
||||
|
||||
//преобразуем не первые разделы body в title-subtitle
|
||||
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||
let curSubtitles = [];
|
||||
let prevBodyIndex = -1;
|
||||
for (let i = 0; i < pc.length; i++) {
|
||||
@@ -132,25 +124,43 @@ class ContentsPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
const prepareLabel = (title, bolder = false) => {
|
||||
let titleParts = title.split('<p>');
|
||||
const textParts = titleParts.filter(v => v).map(v => `<div>${utils.removeHtmlTags(v)}</div>`);
|
||||
if (bolder && textParts.length > 1)
|
||||
textParts[0] = `<b>${textParts[0]}</b>`;
|
||||
return textParts.join('');
|
||||
}
|
||||
|
||||
const getIndentStyle = inset => `width: ${inset*20}px`;
|
||||
|
||||
const getLabelStyle = (inset) => {
|
||||
const fontSizes = ['110%', '100%', '90%', '85%'];
|
||||
inset = (inset > 3 ? 3 : inset);
|
||||
return `font-size: ${fontSizes[inset]}`;
|
||||
};
|
||||
|
||||
//формируем newContents
|
||||
let i = 0;
|
||||
const newContents = [];
|
||||
newpc.forEach((cont) => {
|
||||
const label = prepareLabel(cont.title, true);
|
||||
const style = insetStyle(cont.inset);
|
||||
const indentStyle = getIndentStyle(cont.inset);
|
||||
const labelStyle = getLabelStyle(cont.inset);
|
||||
|
||||
let j = 0;
|
||||
const list = [];
|
||||
cont.subtitles.forEach((sub) => {
|
||||
const l = prepareLabel(sub.title);
|
||||
const s = insetStyle(sub.inset + 1);
|
||||
const s = getIndentStyle(sub.inset + 1);
|
||||
const ls = getLabelStyle(cont.inset + 1);
|
||||
const p = parsed.para[sub.paraIndex];
|
||||
list.push({perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, style: s});
|
||||
list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s, labelStyle: ls};
|
||||
j++;
|
||||
});
|
||||
|
||||
const p = parsed.para[cont.paraIndex];
|
||||
newContents.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, style, list});
|
||||
newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
|
||||
|
||||
i++;
|
||||
});
|
||||
@@ -158,9 +168,27 @@ class ContentsPage extends Vue {
|
||||
this.contents = newContents;
|
||||
}
|
||||
|
||||
async expandClick(key) {
|
||||
const item = this.contents[key];
|
||||
const expanded = !item.expanded;
|
||||
|
||||
if (!expanded) {
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
subitems.style.height = '0';
|
||||
await utils.sleep(200);
|
||||
}
|
||||
|
||||
this.$set(this.contents, key, Object.assign({}, item, {expanded}));
|
||||
|
||||
if (expanded) {
|
||||
await this.$nextTick();
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
subitems.style.height = subitems.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
async setBookPos(newValue) {
|
||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||
await this.$nextTick();
|
||||
this.close();
|
||||
}
|
||||
|
||||
@@ -188,20 +216,32 @@ class ContentsPage extends Vue {
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
padding: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
.item, .subitem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.item:hover, .subitem:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.subitem:hover {
|
||||
background-color: #e0e0e0;
|
||||
.expand-button, .no-expand-button {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.separator-top {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
.subitems-transition {
|
||||
height: 0;
|
||||
transition: height 0.2s linear;
|
||||
overflow: hidden;
|
||||
}
|
||||
.separator-bottom {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
|
||||
.icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.expanded-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<span class="greeting"><b>{{ title }}</b></span>
|
||||
<div class="q-my-sm"></div>
|
||||
<span class="greeting">Добро пожаловать!</span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz<span v-if="isExternalConverter">, rar</span></b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
|
||||
</div>
|
||||
|
||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||
|
||||
@@ -250,11 +250,11 @@ class Reader extends Vue {
|
||||
await this.$nextTick();
|
||||
this.paramPosIgnore = false;
|
||||
}
|
||||
}, 500, {'maxWait':5000});
|
||||
}, 500, {maxWait: 5000});
|
||||
|
||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
}, 15000, {'maxWait':20000});
|
||||
}, 15000, {maxWait: 20000});
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||
@@ -593,12 +593,6 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
refreshBookSplitToPara() {
|
||||
if (this.mostRecentBook()) {
|
||||
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
|
||||
}
|
||||
}
|
||||
|
||||
recentBooksClose() {
|
||||
this.recentBooksActive = false;
|
||||
}
|
||||
@@ -688,9 +682,14 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
refreshBook() {
|
||||
if (this.mostRecentBook()) {
|
||||
this.loadBook({url: this.mostRecentBook().url, force: true});
|
||||
refreshBook(mode) {
|
||||
const mrb = this.mostRecentBook();
|
||||
if (mrb) {
|
||||
if (mode && mode == 'split') {
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, skipCheck: true, isText: true, force: true});
|
||||
} else {
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,8 +845,12 @@ class Reader extends Vue {
|
||||
|
||||
let url = encodeURI(decodeURI(opts.url));
|
||||
|
||||
//TODO: убрать конвертирование 'file://' после 06.2021
|
||||
if (url.length == 71 && url.indexOf('file://') == 0)
|
||||
url = url.replace(/^file/, 'disk');
|
||||
|
||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||
(url.indexOf('file://') != 0))
|
||||
(url.indexOf('disk://') != 0))
|
||||
url = 'http://' + url;
|
||||
|
||||
// уже просматривается сейчас
|
||||
@@ -878,6 +881,7 @@ class Reader extends Vue {
|
||||
wasOpened = (wasOpened ? wasOpened : {});
|
||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
||||
|
||||
let book = null;
|
||||
|
||||
@@ -924,7 +928,8 @@ class Reader extends Vue {
|
||||
url,
|
||||
skipCheck: (opts.skipCheck ? true : false),
|
||||
isText: (opts.isText ? true : false),
|
||||
enableSitesFilter: this.enableSitesFilter
|
||||
enableSitesFilter: this.enableSitesFilter,
|
||||
uploadFileName
|
||||
},
|
||||
(state) => {
|
||||
progress.setState(state);
|
||||
@@ -940,7 +945,7 @@ class Reader extends Vue {
|
||||
});
|
||||
|
||||
// добавляем в историю
|
||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook));
|
||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
|
||||
this.mostRecentBook();
|
||||
this.addAction(bookPos);
|
||||
this.updateRoute(true);
|
||||
@@ -977,7 +982,7 @@ class Reader extends Vue {
|
||||
|
||||
progress.hide(); this.progressActive = false;
|
||||
|
||||
await this.loadBook({url});
|
||||
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
@@ -1049,7 +1054,7 @@ class Reader extends Vue {
|
||||
this.copyTextToggle();
|
||||
break;
|
||||
case 'splitToPara':
|
||||
this.refreshBookSplitToPara();
|
||||
this.refreshBook('split');
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
||||
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
|
||||
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
//import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
import Window from '../../share/Window.vue';
|
||||
@@ -112,6 +112,7 @@ class RecentBooksPage extends Vue {
|
||||
pagination = {};
|
||||
|
||||
created() {
|
||||
this.firstInit = true;
|
||||
this.pagination = {rowsPerPage: 0};
|
||||
|
||||
this.columns = [
|
||||
@@ -167,26 +168,11 @@ class RecentBooksPage extends Vue {
|
||||
this.initing = true;
|
||||
|
||||
|
||||
if (!bookManager.loaded) {
|
||||
await this.updateTableData(10);
|
||||
//для отзывчивости
|
||||
await utils.sleep(100);
|
||||
let i = 0;
|
||||
let j = 5;
|
||||
while (i < 500 && !bookManager.loaded) {
|
||||
if (i % j == 0) {
|
||||
bookManager.sortedRecentCached = null;
|
||||
await this.updateTableData(20);
|
||||
j *= 2;
|
||||
}
|
||||
|
||||
await utils.sleep(100);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
//для отзывчивости
|
||||
await utils.sleep(100);
|
||||
if (this.firstInit) {//для отзывчивости
|
||||
await this.updateTableData(20);
|
||||
this.firstInit = false;
|
||||
}
|
||||
await utils.sleep(50);
|
||||
await this.updateTableData();
|
||||
this.initing = false;
|
||||
})();
|
||||
@@ -222,30 +208,11 @@ class RecentBooksPage extends Vue {
|
||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||
}
|
||||
|
||||
const fb2 = (book.fb2 ? book.fb2 : {});
|
||||
const bt = utils.getBookTitle(book.fb2);
|
||||
|
||||
let title = fb2.bookTitle;
|
||||
if (title)
|
||||
title = `"${title}"`;
|
||||
else
|
||||
title = '';
|
||||
|
||||
let author = '';
|
||||
if (fb2.author) {
|
||||
const authorNames = fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
author = authorNames.join(', ');
|
||||
} else {//TODO: убрать в будущем
|
||||
author = _.compact([
|
||||
fb2.lastName,
|
||||
fb2.firstName,
|
||||
fb2.middleName
|
||||
]).join(' ');
|
||||
}
|
||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
||||
let title = bt.bookTitle;
|
||||
title = (title ? `"${title}"`: '');
|
||||
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
|
||||
|
||||
result.push({
|
||||
num,
|
||||
@@ -256,9 +223,10 @@ class RecentBooksPage extends Vue {
|
||||
author,
|
||||
title: `${title}${perc}${textLen}`,
|
||||
},
|
||||
descString: `${author}${title}${perc}${textLen}`,
|
||||
descString: `${author}${title}${perc}${textLen}`,//для сортировки
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
fullTitle: bt.fullTitle,
|
||||
key: book.key,
|
||||
});
|
||||
}
|
||||
@@ -291,13 +259,18 @@ class RecentBooksPage extends Vue {
|
||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||
}
|
||||
|
||||
async downloadBook(fb2path) {
|
||||
async downloadBook(fb2path, fullTitle) {
|
||||
try {
|
||||
await readerApi.checkCachedBook(fb2path);
|
||||
|
||||
const d = this.$refs.download;
|
||||
d.href = fb2path;
|
||||
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||
try {
|
||||
const fn = utils.makeValidFilename(fullTitle);
|
||||
d.download = fn.substring(0, 100) + '.fb2';
|
||||
} catch(e) {
|
||||
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||
}
|
||||
|
||||
d.click();
|
||||
} catch (e) {
|
||||
@@ -308,14 +281,6 @@ class RecentBooksPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
openOriginal(url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
openFb2(path) {
|
||||
window.open(path, '_blank');
|
||||
}
|
||||
|
||||
async handleDel(key) {
|
||||
await bookManager.delRecentBook({key});
|
||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||
@@ -331,7 +296,7 @@ class RecentBooksPage extends Vue {
|
||||
|
||||
isUrl(url) {
|
||||
if (url)
|
||||
return (url.indexOf('file://') != 0);
|
||||
return (url.indexOf('disk://') != 0);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -216,8 +216,15 @@ class ServerStorage extends Vue {
|
||||
}
|
||||
|
||||
error(message) {
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$root.notify.error(message);
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive) {
|
||||
this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
|
||||
const hint = (this.errorMessageCounter < 2 ? '' :
|
||||
'<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
|
||||
'<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
|
||||
);
|
||||
|
||||
this.$root.notify.error(message + hint);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings(force = false, doNotifySuccess = true) {
|
||||
@@ -507,10 +514,10 @@ class ServerStorage extends Vue {
|
||||
if (md.key && result[md.key])
|
||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||
|
||||
if (!bookManager.loaded) {
|
||||
/*if (!bookManager.loaded) {
|
||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||
while (!bookManager.loaded) await utils.sleep(100);
|
||||
}
|
||||
}*/
|
||||
|
||||
if (newRecent.rev != this.cachedRecent.rev)
|
||||
await this.setCachedRecent(newRecent);
|
||||
@@ -580,8 +587,8 @@ class ServerStorage extends Vue {
|
||||
let newRecent = {};
|
||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||
//ждем весь bm.recent
|
||||
while (!bookManager.loaded)
|
||||
await utils.sleep(100);
|
||||
/*while (!bookManager.loaded)
|
||||
await utils.sleep(100);*/
|
||||
|
||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||
|
||||
@@ -104,8 +104,12 @@ class UserHotKeys extends UserHotKeysProps {
|
||||
this.updateTableData();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
updateTableData() {
|
||||
let result = rstore.hotKeys.map(hk => hk.name);
|
||||
let result = rstore.hotKeys.map(hk => hk.name).filter(name => (this.mode == 'liberama.top' || name != 'libs'));
|
||||
|
||||
const search = this.search.toLowerCase();
|
||||
const codesIncludeSearch = (action) => {
|
||||
|
||||
@@ -77,9 +77,15 @@ export default class DrawHelper {
|
||||
let j = 0;
|
||||
//формируем строку
|
||||
for (const part of line.parts) {
|
||||
let tOpen = (part.style.bold ? '<b>' : '');
|
||||
let tOpen = '';
|
||||
tOpen += (part.style.bold ? '<b>' : '');
|
||||
tOpen += (part.style.italic ? '<i>' : '');
|
||||
let tClose = (part.style.italic ? '</i>' : '');
|
||||
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
|
||||
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
|
||||
let tClose = '';
|
||||
tClose += (part.style.sub ? '</span>' : '');
|
||||
tClose += (part.style.sup ? '</span>' : '');
|
||||
tClose += (part.style.italic ? '</i>' : '');
|
||||
tClose += (part.style.bold ? '</b>' : '');
|
||||
|
||||
let text = '';
|
||||
@@ -159,7 +165,7 @@ export default class DrawHelper {
|
||||
const fh = h - 2*pad;
|
||||
const fh2 = fh/2;
|
||||
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
|
||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||
const read = (bookPos + 1)/textLength;
|
||||
const t2 = `${(read*100).toFixed(2)}%`;
|
||||
|
||||
@@ -40,7 +40,7 @@ import Component from 'vue-class-component';
|
||||
import {loadCSS} from 'fg-loadcss';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {sleep} from '../../../share/utils';
|
||||
import * as utils from '../../../share/utils';
|
||||
import bookManager from '../share/bookManager';
|
||||
import DrawHelper from './DrawHelper';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
@@ -134,7 +134,7 @@ class TextPage extends Vue {
|
||||
|
||||
this.$root.$on('resize', async() => {
|
||||
this.$nextTick(this.onResize);
|
||||
await sleep(500);
|
||||
await utils.sleep(500);
|
||||
this.$nextTick(this.onResize);
|
||||
});
|
||||
}
|
||||
@@ -285,7 +285,7 @@ class TextPage extends Vue {
|
||||
|
||||
let close = null;
|
||||
(async() => {
|
||||
await sleep(500);
|
||||
await utils.sleep(500);
|
||||
if (this.fontsLoading)
|
||||
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
||||
})();
|
||||
@@ -342,7 +342,7 @@ class TextPage extends Vue {
|
||||
let i = 0;
|
||||
const t = this.parsed.testText;
|
||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
||||
await sleep(100);
|
||||
await utils.sleep(100);
|
||||
|
||||
if (this.parsed === parsed) {
|
||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||
@@ -366,7 +366,6 @@ class TextPage extends Vue {
|
||||
this.updateLayout();
|
||||
this.book = null;
|
||||
this.meta = null;
|
||||
this.fb2 = null;
|
||||
this.parsed = null;
|
||||
|
||||
this.linesUp = null;
|
||||
@@ -383,7 +382,7 @@ class TextPage extends Vue {
|
||||
try {
|
||||
//подождем ленивый парсинг
|
||||
this.stopLazyParse = true;
|
||||
while (this.doingLazyParse) await sleep(10);
|
||||
while (this.doingLazyParse) await utils.sleep(10);
|
||||
|
||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||
if (!isParsed) {
|
||||
@@ -392,21 +391,9 @@ class TextPage extends Vue {
|
||||
|
||||
this.book = await bookManager.getBook(this.lastBook);
|
||||
this.meta = bookManager.metaOnly(this.book);
|
||||
this.fb2 = this.meta.fb2;
|
||||
const bt = utils.getBookTitle(this.meta.fb2);
|
||||
|
||||
let authorNames = [];
|
||||
if (this.fb2.author) {
|
||||
authorNames = this.fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
}
|
||||
|
||||
this.title = _.compact([
|
||||
authorNames.join(', '),
|
||||
this.fb2.bookTitle
|
||||
]).join(' - ');
|
||||
this.title = bt.fullTitle;
|
||||
|
||||
this.$root.$emit('set-app-title', this.title);
|
||||
|
||||
@@ -493,7 +480,7 @@ class TextPage extends Vue {
|
||||
let wait = (timeout + 201)/100;
|
||||
while (wait > 0 && !this[stopPropertyName]) {
|
||||
wait--;
|
||||
await sleep(100);
|
||||
await utils.sleep(100);
|
||||
}
|
||||
resolve();
|
||||
})().catch(reject); });
|
||||
@@ -509,7 +496,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
//ждем анимацию
|
||||
while (this.inAnimation) await sleep(10);
|
||||
while (this.inAnimation) await utils.sleep(10);
|
||||
|
||||
this.stopScrolling = false;
|
||||
this.doingScrolling = true;
|
||||
@@ -520,7 +507,7 @@ class TextPage extends Vue {
|
||||
this.page1 = this.page2;
|
||||
this.toggleLayout = true;
|
||||
await this.$nextTick();
|
||||
await sleep(50);
|
||||
await utils.sleep(50);
|
||||
|
||||
this.cachedPos = -1;
|
||||
this.draw();
|
||||
@@ -557,7 +544,7 @@ class TextPage extends Vue {
|
||||
page.style.transform = 'none';
|
||||
page.offsetHeight;
|
||||
|
||||
while (this.doingScrolling) await sleep(10);
|
||||
while (this.doingScrolling) await utils.sleep(10);
|
||||
}
|
||||
|
||||
draw() {
|
||||
@@ -766,7 +753,7 @@ class TextPage extends Vue {
|
||||
for (let i = 0; i < this.parsed.para.length; i++) {
|
||||
j++;
|
||||
if (j > 1) {
|
||||
await sleep(1);
|
||||
await utils.sleep(1);
|
||||
j = 0;
|
||||
}
|
||||
if (this.stopLazyParse)
|
||||
@@ -788,7 +775,7 @@ class TextPage extends Vue {
|
||||
async refreshTime() {
|
||||
if (!this.timeRefreshing) {
|
||||
this.timeRefreshing = true;
|
||||
await sleep(60*1000);
|
||||
await utils.sleep(60*1000);
|
||||
|
||||
if (this.book && this.parsed.textLength) {
|
||||
this.debouncedDrawStatusBar();
|
||||
@@ -905,7 +892,7 @@ class TextPage extends Vue {
|
||||
this.settingsChanging = true;
|
||||
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
||||
this.commit('reader/setSettings', {fontSize: newSize});
|
||||
await sleep(50);
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -915,7 +902,7 @@ class TextPage extends Vue {
|
||||
this.settingsChanging = true;
|
||||
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
||||
this.commit('reader/setSettings', {fontSize: newSize});
|
||||
await sleep(50);
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -925,7 +912,7 @@ class TextPage extends Vue {
|
||||
this.settingsChanging = true;
|
||||
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
||||
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||
await sleep(50);
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -935,7 +922,7 @@ class TextPage extends Vue {
|
||||
this.settingsChanging = true;
|
||||
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
||||
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||
await sleep(50);
|
||||
await utils.sleep(50);
|
||||
this.settingsChanging = false;
|
||||
}
|
||||
}
|
||||
@@ -949,7 +936,7 @@ class TextPage extends Vue {
|
||||
let delay = 400;
|
||||
while (this.repDoing) {
|
||||
this.handleClick(pointX, pointY);
|
||||
await sleep(delay);
|
||||
await utils.sleep(delay);
|
||||
if (delay > 15)
|
||||
delay *= 0.8;
|
||||
}
|
||||
@@ -1066,7 +1053,7 @@ class TextPage extends Vue {
|
||||
|
||||
onStatusBarClick() {
|
||||
const url = this.meta.url;
|
||||
if (url && url.indexOf('file://') != 0) {
|
||||
if (url && url.indexOf('disk://') != 0) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import he from 'he';
|
||||
import sax from '../../../../server/core/sax';
|
||||
import {sleep} from '../../../share/utils';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
|
||||
@@ -90,7 +90,7 @@ export default class BookParser {
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = `data:${binaryType};base64,${data}`;
|
||||
await sleep(30*1000);
|
||||
await utils.sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
})().catch(reject); });
|
||||
@@ -112,7 +112,7 @@ export default class BookParser {
|
||||
i.onerror = reject;
|
||||
|
||||
i.src = src;
|
||||
await sleep(30*1000);
|
||||
await utils.sleep(30*1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось получить размер изображения');
|
||||
})().catch(reject); });
|
||||
@@ -216,14 +216,51 @@ export default class BookParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
||||
if (path == '/fictionbook/description/title-info/author') {
|
||||
if (!fb2.author)
|
||||
fb2.author = [];
|
||||
|
||||
fb2.author.push({});
|
||||
}
|
||||
|
||||
const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
|
||||
if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
|
||||
if (!fb2.sequence)
|
||||
fb2.sequence = [];
|
||||
|
||||
if (!isPublishSequence || !fb2.sequence.length) {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
const seq = {};
|
||||
if (attrs.name && attrs.name.value) {
|
||||
seq.name = attrs.name.value;
|
||||
}
|
||||
if (attrs.number && attrs.number.value) {
|
||||
seq.number = attrs.number.value;
|
||||
}
|
||||
|
||||
fb2.sequence.push(seq);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.indexOf('/fictionbook/body') == 0) {
|
||||
if (tag == 'body') {
|
||||
if (isFirstBody && fb2.annotation) {
|
||||
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
|
||||
ann.forEach(a => {
|
||||
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
|
||||
});
|
||||
if (ann.length)
|
||||
newParagraph(' ', 1);
|
||||
}
|
||||
|
||||
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
|
||||
const bt = utils.getBookTitle(fb2);
|
||||
if (bt.sequence) {
|
||||
newParagraph(bt.sequence, bt.sequence.length);
|
||||
newParagraph(' ', 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFirstBody)
|
||||
newParagraph(' ', 1);
|
||||
isFirstBody = false;
|
||||
@@ -248,7 +285,7 @@ export default class BookParser {
|
||||
sectionLevel++;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`<${tag}>`, 0);
|
||||
}
|
||||
|
||||
@@ -306,7 +343,7 @@ export default class BookParser {
|
||||
sectionLevel--;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`</${tag}>`, 0);
|
||||
}
|
||||
|
||||
@@ -419,7 +456,7 @@ export default class BookParser {
|
||||
};
|
||||
|
||||
const onProgress = async(prog) => {
|
||||
await sleep(1);
|
||||
await utils.sleep(1);
|
||||
callback(prog);
|
||||
};
|
||||
|
||||
@@ -441,7 +478,7 @@ export default class BookParser {
|
||||
this.textLength = paraOffset;
|
||||
|
||||
callback(100);
|
||||
await sleep(10);
|
||||
await utils.sleep(10);
|
||||
|
||||
return {fb2};
|
||||
}
|
||||
@@ -470,7 +507,7 @@ export default class BookParser {
|
||||
|
||||
splitToStyle(s) {
|
||||
let result = [];/*array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number},
|
||||
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
|
||||
image: {local: Boolean, inline: Boolean, id: String},
|
||||
text: String,
|
||||
}*/
|
||||
@@ -493,6 +530,12 @@ export default class BookParser {
|
||||
case 'emphasis':
|
||||
style.italic = true;
|
||||
break;
|
||||
case 'sup':
|
||||
style.sup = true;
|
||||
break;
|
||||
case 'sub':
|
||||
style.sub = true;
|
||||
break;
|
||||
case 'center':
|
||||
style.center = true;
|
||||
break;
|
||||
@@ -543,6 +586,12 @@ export default class BookParser {
|
||||
case 'emphasis':
|
||||
style.italic = false;
|
||||
break;
|
||||
case 'sup':
|
||||
style.sup = false;
|
||||
break;
|
||||
case 'sub':
|
||||
style.sub = false;
|
||||
break;
|
||||
case 'center':
|
||||
style.center = false;
|
||||
break;
|
||||
|
||||
@@ -4,19 +4,25 @@ import _ from 'lodash';
|
||||
import * as utils from '../../../share/utils';
|
||||
import BookParser from './BookParser';
|
||||
|
||||
const maxDataSize = 300*1024*1024;//compressed bytes
|
||||
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||
|
||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||
const bmMetaStore = localForage.createInstance({
|
||||
name: 'bmMetaStore'
|
||||
});
|
||||
|
||||
//локальный кэш самих книг, ограничение maxDataSize
|
||||
const bmDataStore = localForage.createInstance({
|
||||
name: 'bmDataStore'
|
||||
});
|
||||
|
||||
const bmRecentStore = localForage.createInstance({
|
||||
//список недавно открытых книг
|
||||
const bmRecentStoreOld = localForage.createInstance({
|
||||
name: 'bmRecentStore'
|
||||
});
|
||||
const bmRecentStoreNew = localForage.createInstance({
|
||||
name: 'bmRecentStoreNew'
|
||||
});
|
||||
|
||||
class BookManager {
|
||||
async init(settings) {
|
||||
@@ -25,15 +31,80 @@ class BookManager {
|
||||
|
||||
this.eventListeners = [];
|
||||
this.books = {};
|
||||
this.recent = {};
|
||||
|
||||
this.recentLast = await bmRecentStore.getItem('recent-last');
|
||||
if (this.recentLast) {
|
||||
this.recent[this.recentLast.key] = this.recentLast;
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
this.recent = {};
|
||||
this.saveRecent = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent', this.recent);
|
||||
}, 300, {maxWait: 800});
|
||||
|
||||
this.saveRecentItem = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}, 200, {maxWait: 300});
|
||||
|
||||
//загрузка bmRecentStore
|
||||
this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
|
||||
if (this.recentRev) {
|
||||
this.recent = await bmRecentStoreNew.getItem('recent');
|
||||
if (!this.recent)
|
||||
this.recent = {};
|
||||
|
||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||
if (this.recentItem)
|
||||
this.recent[this.recentItem.key] = this.recentItem;
|
||||
|
||||
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||
if (this.recentLastKey) {
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
await this.cleanRecentBooks();
|
||||
|
||||
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||
{
|
||||
await this.convertFileToDiskPrefix();
|
||||
if (this.recentRev > 10)
|
||||
await bmRecentStoreOld.clear();
|
||||
}
|
||||
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
||||
if (this.recentLast) {
|
||||
this.recent[this.recentLast.key] = this.recentLast;
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
let key = null;
|
||||
const len = await bmRecentStoreOld.length();
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
key = await bmRecentStoreOld.key(i);
|
||||
if (key) {
|
||||
let r = await bmRecentStoreOld.getItem(key);
|
||||
if (_.isObject(r) && r.key) {
|
||||
this.recent[r.key] = r;
|
||||
}
|
||||
} else {
|
||||
await bmRecentStoreOld.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
//размножение для дебага
|
||||
/*if (key) {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const k = this.keyFromUrl(i.toString());
|
||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
||||
}
|
||||
}*/
|
||||
|
||||
await bmRecentStoreNew.setItem('recent', this.recent);
|
||||
this.recentRev = 1;
|
||||
await bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
@@ -41,9 +112,7 @@ class BookManager {
|
||||
this.loadStored();//no await
|
||||
}
|
||||
|
||||
//Долгая асинхронная загрузка из хранилища.
|
||||
//Хранение в отдельных записях дает относительно
|
||||
//нормальное поведение при нескольких вкладках с читалкой в браузере.
|
||||
//Ленивая асинхронная загрузка bmMetaStore
|
||||
async loadStored() {
|
||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||
await utils.sleep(2000);
|
||||
@@ -70,32 +139,7 @@ class BookManager {
|
||||
}
|
||||
}
|
||||
|
||||
let key = null;
|
||||
len = await bmRecentStore.length();
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
key = await bmRecentStore.key(i);
|
||||
if (key) {
|
||||
let r = await bmRecentStore.getItem(key);
|
||||
if (_.isObject(r) && r.key) {
|
||||
this.recent[r.key] = r;
|
||||
}
|
||||
} else {
|
||||
await bmRecentStore.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
//размножение для дебага
|
||||
/*if (key) {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const k = this.keyFromUrl(i.toString());
|
||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
||||
}
|
||||
}*/
|
||||
|
||||
await this.cleanBooks();
|
||||
await this.cleanRecentBooks();
|
||||
|
||||
this.recentChanged = true;
|
||||
this.loaded = true;
|
||||
this.emit('load-stored-finish');
|
||||
}
|
||||
@@ -125,7 +169,7 @@ class BookManager {
|
||||
}
|
||||
|
||||
async deflateWithProgress(data, callback) {
|
||||
const chunkSize = 128*1024;
|
||||
const chunkSize = 512*1024;
|
||||
const deflator = new utils.pako.Deflate({level: 5});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
@@ -159,7 +203,7 @@ class BookManager {
|
||||
}
|
||||
|
||||
async inflateWithProgress(data, callback) {
|
||||
const chunkSize = 64*1024;
|
||||
const chunkSize = 512*1024;
|
||||
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
@@ -238,7 +282,7 @@ class BookManager {
|
||||
let book = this.books[meta.key];
|
||||
|
||||
if (!book && !this.loaded) {
|
||||
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||
if (book)
|
||||
this.books[meta.key] = book;
|
||||
}
|
||||
@@ -254,7 +298,7 @@ class BookManager {
|
||||
result = this.books[meta.key];
|
||||
|
||||
if (!result) {
|
||||
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||
if (result)
|
||||
this.books[meta.key] = result;
|
||||
}
|
||||
@@ -328,6 +372,43 @@ class BookManager {
|
||||
}
|
||||
|
||||
//-- recent --------------------------------------------------------------
|
||||
async recentSetItem(item = null, skipCheck = false) {
|
||||
const rev = await bmRecentStoreNew.getItem('rev');
|
||||
if (rev != this.recentRev && !skipCheck) {
|
||||
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||
Object.assign(this.recent, newRecent);
|
||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||
this.recentRev = rev;
|
||||
}
|
||||
|
||||
const prevKey = (this.recentItem ? this.recentItem.key : '');
|
||||
if (item) {
|
||||
this.recent[item.key] = item;
|
||||
this.recentItem = item;
|
||||
} else {
|
||||
this.recentItem = null;
|
||||
}
|
||||
|
||||
this.saveRecentItem();
|
||||
|
||||
if (!item || prevKey != item.key) {
|
||||
this.saveRecent();
|
||||
}
|
||||
|
||||
this.recentChanged = true;
|
||||
|
||||
if (item) {
|
||||
this.emit('recent-changed', item.key);
|
||||
} else {
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
}
|
||||
|
||||
async recentSetLastKey(key) {
|
||||
this.recentLastKey = key;
|
||||
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
|
||||
}
|
||||
|
||||
async setRecentBook(value) {
|
||||
const result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();
|
||||
@@ -341,38 +422,25 @@ class BookManager {
|
||||
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
||||
}
|
||||
|
||||
this.recent[result.key] = result;
|
||||
|
||||
await bmRecentStore.setItem(result.key, result);
|
||||
|
||||
this.recentLast = result;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
|
||||
this.recentChanged = true;
|
||||
this.emit('recent-changed', result.key);
|
||||
await this.recentSetLastKey(result.key);
|
||||
await this.recentSetItem(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getRecentBook(value) {
|
||||
let result = this.recent[value.key];
|
||||
if (!result) {
|
||||
result = await bmRecentStore.getItem(value.key);
|
||||
if (result)
|
||||
this.recent[value.key] = result;
|
||||
}
|
||||
return result;
|
||||
return this.recent[value.key];
|
||||
}
|
||||
|
||||
async delRecentBook(value) {
|
||||
this.recent[value.key].deleted = 1;
|
||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 1;
|
||||
|
||||
if (this.recentLast.key == value.key) {
|
||||
this.recentLast = null;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
if (this.recentLastKey == value.key) {
|
||||
await this.recentSetLastKey(null);
|
||||
}
|
||||
|
||||
await this.recentSetItem(item);
|
||||
this.emit('recent-deleted', value.key);
|
||||
this.emit('recent-changed', value.key);
|
||||
}
|
||||
|
||||
async cleanRecentBooks() {
|
||||
@@ -380,24 +448,49 @@ class BookManager {
|
||||
|
||||
let isDel = false;
|
||||
for (let i = 1000; i < sorted.length; i++) {
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
delete this.recent[sorted[i].key];
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
isDel = true;
|
||||
}
|
||||
|
||||
this.sortedRecentCached = null;
|
||||
|
||||
if (isDel)
|
||||
this.emit('recent-changed');
|
||||
await this.recentSetItem();
|
||||
return isDel;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (this.recentLast) {
|
||||
return this.recentLast;
|
||||
async convertFileToDiskPrefix() {
|
||||
let isConverted = false;
|
||||
|
||||
const newRecent = {};
|
||||
for (let key of Object.keys(this.recent)) {
|
||||
let newKey = key;
|
||||
let newUrl = this.recent[key].url;
|
||||
|
||||
if (newKey.indexOf('66696c65') == 0) {
|
||||
newKey = newKey.replace(/^66696c65/, '6469736b');
|
||||
if (newUrl)
|
||||
newUrl = newUrl.replace(/^file/, 'disk');
|
||||
isConverted = true;
|
||||
}
|
||||
|
||||
newRecent[newKey] = this.recent[key];
|
||||
newRecent[newKey].key = newKey;
|
||||
if (newUrl)
|
||||
newRecent[newKey].url = newUrl;
|
||||
}
|
||||
const oldRecentLast = this.recentLast;
|
||||
if (isConverted) {
|
||||
this.recent = newRecent;
|
||||
await this.recentSetItem(null, true);
|
||||
}
|
||||
return isConverted;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (this.recentLastKey) {
|
||||
return this.recent[this.recentLastKey];
|
||||
}
|
||||
const oldKey = this.recentLastKey;
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
@@ -408,10 +501,11 @@ class BookManager {
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
this.recentLast = result;
|
||||
bmRecentStore.setItem('recent-last', this.recentLast);//no await
|
||||
|
||||
const newRecentLastKey = (result ? result.key : null);
|
||||
this.recentSetLastKey(newRecentLastKey);//no await
|
||||
|
||||
if (this.recentLast !== oldRecentLast)
|
||||
if (newRecentLastKey !== oldKey)
|
||||
this.emit('recent-changed');
|
||||
|
||||
return result;
|
||||
@@ -442,24 +536,12 @@ class BookManager {
|
||||
delete mergedRecent[i];
|
||||
}
|
||||
|
||||
//"ленивое" обновление хранилища
|
||||
(async() => {
|
||||
for (const rec of Object.values(mergedRecent)) {
|
||||
if (rec.key) {
|
||||
await bmRecentStore.setItem(rec.key, rec);
|
||||
await utils.sleep(1);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
this.recent = mergedRecent;
|
||||
|
||||
this.recentLast = null;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
await this.recentSetLastKey(null);
|
||||
await this.recentSetItem(null, true);
|
||||
|
||||
this.recentChanged = true;
|
||||
this.emit('set-recent');
|
||||
this.emit('recent-changed');
|
||||
}
|
||||
|
||||
addEventListener(listener) {
|
||||
|
||||
@@ -1,4 +1,39 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
showUntil: '2020-12-08',
|
||||
header: '0.9.11 (2020-12-09)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-12-10',
|
||||
header: '0.9.10 (2020-12-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата Djvu</li>
|
||||
<li>добавлена поддержка Rar-архивов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-11-20',
|
||||
header: '0.9.9 (2020-11-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-11-12',
|
||||
header: '0.9.8 (2020-11-13)',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<q-dialog v-model="active">
|
||||
<q-dialog v-model="active" no-route-dismiss>
|
||||
<div class="column bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
|
||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
|
||||
<slot></slot>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
|
||||
@@ -21,7 +21,7 @@ import {QSlider} from 'quasar/src/components/slider';
|
||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||
import {QSeparator} from 'quasar/src/components/separator';
|
||||
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
//import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||
import {QSpinner} from 'quasar/src/components/spinner';
|
||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||
@@ -32,8 +32,7 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||
import {QDialog} from 'quasar/src/components/dialog';
|
||||
import {QChip} from 'quasar/src/components/chip';
|
||||
import {QTree} from 'quasar/src/components/tree';
|
||||
import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||
|
||||
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||
|
||||
const components = {
|
||||
//QLayout,
|
||||
@@ -50,7 +49,7 @@ const components = {
|
||||
QTabs, QTab,
|
||||
//QTabPanels, QTabPanel,
|
||||
QSeparator,
|
||||
QList, QItem, QItemSection, QItemLabel,
|
||||
//QList, QItem, QItemSection, QItemLabel,
|
||||
QTooltip,
|
||||
QSpinner,
|
||||
QTable, QTh, QTr, QTd,
|
||||
@@ -61,7 +60,7 @@ const components = {
|
||||
QDialog,
|
||||
QChip,
|
||||
QTree,
|
||||
QExpansionItem,
|
||||
//QExpansionItem,
|
||||
};
|
||||
|
||||
//directives
|
||||
|
||||
@@ -304,3 +304,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function removeHtmlTags(s) {
|
||||
return s.replace(/(<([^>]+)>)/ig, '');
|
||||
}
|
||||
|
||||
export function makeValidFilename(filename, repl = '_') {
|
||||
let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
|
||||
f = f.trim();
|
||||
while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
|
||||
f = f.substring(0, f.length - 1);
|
||||
}
|
||||
|
||||
if (f)
|
||||
return f;
|
||||
else
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
|
||||
export function getBookTitle(fb2) {
|
||||
fb2 = (fb2 ? fb2 : {});
|
||||
const result = {};
|
||||
|
||||
if (fb2.author) {
|
||||
const authorNames = fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
|
||||
result.author = authorNames.join(', ');
|
||||
}
|
||||
|
||||
if (fb2.sequence) {
|
||||
const seqs = fb2.sequence.map(s => _.compact([
|
||||
s.name,
|
||||
(s.number ? `#${s.number}` : null),
|
||||
]).join(' '));
|
||||
|
||||
result.sequence = seqs.join(', ');
|
||||
if (result.sequence)
|
||||
result.sequenceTitle = `(${result.sequence})`;
|
||||
}
|
||||
|
||||
result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
|
||||
|
||||
result.fullTitle = _.compact([
|
||||
result.author,
|
||||
result.bookTitle
|
||||
]).join(' - ');
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const readerActions = {
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Библиотека',
|
||||
'libs': 'Сетевая библиотека',
|
||||
'recentBooks': 'Открыть недавние',
|
||||
'switchToolbar': 'Показать/скрыть панель управления',
|
||||
'donate': '',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo -H -u www-data /home/beta.liberama/liberama
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/beta.liberama/liberama"
|
||||
|
||||
@@ -27,15 +27,31 @@ sudo chown www-data.www-data /home/liberama
|
||||
|
||||
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
||||
```
|
||||
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
|
||||
wget "https://download.calibre-ebook.com/5.5.0/calibre-5.5.0-x86_64.txz"
|
||||
sudo -u www-data mkdir -p /home/liberama/data/calibre
|
||||
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
|
||||
sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
|
||||
```
|
||||
|
||||
### external converter `pdfalto`, github https://github.com/kermitt2/pdfalto
|
||||
```
|
||||
git clone https://github.com/kermitt2/pdfalto
|
||||
cd pdfalto
|
||||
git submodule update --init --recursive
|
||||
cmake ./
|
||||
добавить в начало CMakeLists.txt строчку: set(CMAKE_EXE_LINKER_FLAGS "-no-pie")
|
||||
make
|
||||
|
||||
sudo -u www-data mkdir -p /home/liberama/data/pdfalto
|
||||
sudo -u www-data cp pdfalto /home/liberama/data/pdfalto
|
||||
```
|
||||
|
||||
### external converters
|
||||
```
|
||||
sudo apt install rar
|
||||
sudo apt install libreoffice
|
||||
sudo apt install poppler-utils
|
||||
sudo apt install djvulibre-bin
|
||||
sudo apt install libtiff-tools
|
||||
sudo apt install graphicsmagick-imagemagick-compat
|
||||
```
|
||||
|
||||
### nginx, server config
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if ! pgrep -x "liberama" > /dev/null ; then
|
||||
sudo -H -u www-data /home/liberama/liberama
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
||||
else
|
||||
echo "Process 'liberama' already running"
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo -H -u www-data /home/liberama/liberama &
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
|
||||
sudo service cron start
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.9.8",
|
||||
"version": "0.9.11",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -10124,6 +10124,21 @@
|
||||
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
||||
"dev": true
|
||||
},
|
||||
"pidusage": {
|
||||
"version": "2.0.21",
|
||||
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz",
|
||||
"integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.9.8",
|
||||
"version": "0.9.11",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
@@ -8,7 +8,7 @@
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --inspect --exec 'node server'",
|
||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
|
||||
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
|
||||
@@ -72,6 +72,7 @@
|
||||
"multer": "^1.4.2",
|
||||
"pako": "^1.0.11",
|
||||
"path-browserify": "^1.0.0",
|
||||
"pidusage": "^2.0.21",
|
||||
"quasar": "^1.14.3",
|
||||
"safe-buffer": "^5.2.0",
|
||||
"sjcl": "^1.0.8",
|
||||
|
||||
@@ -22,6 +22,7 @@ class ReaderController extends BaseController {
|
||||
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
||||
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
|
||||
isText: (request.hasOwnProperty('isText') ? request.isText : false),
|
||||
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
|
||||
});
|
||||
const state = this.workerState.getState(workerId);
|
||||
return (state ? state : {});
|
||||
|
||||
@@ -136,8 +136,9 @@ class WebSocketController {
|
||||
break;
|
||||
|
||||
i++;
|
||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||
if (i > 3*60*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
||||
break;
|
||||
}
|
||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ class FileDecompressor {
|
||||
constructor(limitFileSize = 0) {
|
||||
this.detector = new FileDetector();
|
||||
this.limitFileSize = limitFileSize;
|
||||
|
||||
this.rarPath = '/usr/bin/rar';
|
||||
this.rarExists = false;
|
||||
(async() => {
|
||||
if (await fs.pathExists(this.rarPath))
|
||||
this.rarExists = true;
|
||||
})();
|
||||
}
|
||||
|
||||
async decompressNested(filename, outputDir) {
|
||||
@@ -30,7 +37,11 @@ class FileDecompressor {
|
||||
files: []
|
||||
};
|
||||
|
||||
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) {
|
||||
if (!fileType || !(
|
||||
fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz'
|
||||
|| fileType.ext == 'tar' || (this.rarExists && fileType.ext == 'rar')
|
||||
)
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -94,6 +105,11 @@ class FileDecompressor {
|
||||
async decompress(fileExt, filename, outputDir) {
|
||||
let files = [];
|
||||
|
||||
if (fileExt == 'rar' && this.rarExists) {
|
||||
files = await this.unRar(filename, outputDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
switch (fileExt) {
|
||||
case 'zip':
|
||||
files = await this.unZip(filename, outputDir);
|
||||
@@ -119,17 +135,16 @@ class FileDecompressor {
|
||||
try {
|
||||
return await zip.unpack(filename, outputDir, {
|
||||
limitFileSize: this.limitFileSize,
|
||||
limitFileCount: 1000,
|
||||
limitFileCount: 10000,
|
||||
decodeEntryNameCallback: (nameRaw) => {
|
||||
return utils.bufferRemoveZeroes(nameRaw);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
fs.emptyDir(outputDir);
|
||||
return await zip.unpack(filename, outputDir, {
|
||||
limitFileSize: this.limitFileSize,
|
||||
limitFileCount: 1000,
|
||||
limitFileCount: 10000,
|
||||
decodeEntryNameCallback: (nameRaw) => {
|
||||
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
||||
const enc = textUtils.getEncodingLite(nameRaw);
|
||||
@@ -156,7 +171,7 @@ class FileDecompressor {
|
||||
|
||||
if (this.limitFileSize) {
|
||||
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
||||
reject('Файл слишком большой');
|
||||
reject(new Error('Файл слишком большой'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -222,7 +237,39 @@ class FileDecompressor {
|
||||
|
||||
inputStream.pipe(stream).pipe(outputStream);
|
||||
})().catch(reject); });
|
||||
}
|
||||
}
|
||||
|
||||
async unRar(filename, outputDir) {
|
||||
try {
|
||||
const args = ['x', '-p-', '-y', filename, `${outputDir}`];
|
||||
const result = await utils.spawnProcess(this.rarPath, {
|
||||
killAfter: 60,
|
||||
args
|
||||
});
|
||||
|
||||
if (result.code == 0) {
|
||||
const files = [];
|
||||
await utils.findFiles(async(file) => {
|
||||
const stat = await fs.stat(file);
|
||||
files.push({path: path.relative(outputDir, file), size: stat.size});
|
||||
}, outputDir);
|
||||
|
||||
return files;
|
||||
|
||||
} else {
|
||||
const error = `${result.code}|FORLOG|, exec: ${this.rarPath}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||
throw new Error(`Архиватор Rar завершился с ошибкой: ${error}`);
|
||||
}
|
||||
} catch(e) {
|
||||
if (e.status == 'killed') {
|
||||
throw new Error('Слишком долгое ожидание архиватора Rar');
|
||||
} else if (e.status == 'error') {
|
||||
throw new Error(e.error);
|
||||
} else {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async gzipBuffer(buf) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ class FileDetector {
|
||||
|
||||
fromBuffer(buffer, callback) {
|
||||
let result = null;
|
||||
|
||||
//console.log(buffer);
|
||||
const invalidSignaturesList = this.validateSigantures();
|
||||
if (invalidSignaturesList.length) {
|
||||
return callback(invalidSignaturesList);
|
||||
|
||||
@@ -676,7 +676,9 @@
|
||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" }
|
||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00220031002e0030002200" },
|
||||
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00270031002e0030002700" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -28,7 +28,7 @@ class LimitedQueue {
|
||||
get(onPlaceChange) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.destroyed)
|
||||
reject('destroyed');
|
||||
reject(new Error('destroyed'));
|
||||
|
||||
const take = () => {
|
||||
if (this.freed <= 0)
|
||||
@@ -73,7 +73,7 @@ class LimitedQueue {
|
||||
if (onPlaceChange)
|
||||
onPlaceChange(this.listeners.length);
|
||||
} else {
|
||||
reject('Превышен размер очереди ожидания');
|
||||
reject(new Error('Превышен размер очереди ожидания'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
1
server/core/Reader/BookConverter/.gitignore
vendored
1
server/core/Reader/BookConverter/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
test
|
||||
@@ -5,8 +5,9 @@ const he = require('he');
|
||||
const LimitedQueue = require('../../LimitedQueue');
|
||||
const textUtils = require('./textUtils');
|
||||
const utils = require('../../utils');
|
||||
const xmlParser = require('../../xmlParser');
|
||||
|
||||
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
|
||||
const queue = new LimitedQueue(3, 20, 2*60*1000);//2 минуты ожидание подвижек
|
||||
|
||||
class ConvertBase {
|
||||
constructor(config) {
|
||||
@@ -14,7 +15,6 @@ class ConvertBase {
|
||||
|
||||
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
|
||||
this.sofficePath = '/usr/bin/soffice';
|
||||
this.pdfToHtmlPath = '/usr/bin/pdftohtml';
|
||||
}
|
||||
|
||||
async run(data, opts) {// eslint-disable-line no-unused-vars
|
||||
@@ -27,9 +27,6 @@ class ConvertBase {
|
||||
|
||||
if (!await fs.pathExists(this.sofficePath))
|
||||
throw new Error('Внешний конвертер LibreOffice не найден');
|
||||
|
||||
if (!await fs.pathExists(this.pdfToHtmlPath))
|
||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||
}
|
||||
|
||||
async execConverter(path, args, onData, abort) {
|
||||
@@ -42,20 +39,35 @@ class ConvertBase {
|
||||
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
||||
}
|
||||
|
||||
abort = (abort ? abort : () => false);
|
||||
const myAbort = () => {
|
||||
return q.abort() || abort();
|
||||
}
|
||||
|
||||
try {
|
||||
if (myAbort())
|
||||
throw new Error('abort');
|
||||
|
||||
const result = await utils.spawnProcess(path, {
|
||||
killAfter: 600,
|
||||
killAfter: 3600,//1 час
|
||||
args,
|
||||
onData: (data) => {
|
||||
q.resetTimeout();
|
||||
if (queue.freed > 0)
|
||||
q.resetTimeout();
|
||||
onData(data);
|
||||
},
|
||||
abort
|
||||
//будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
|
||||
onUsage: (stats) => {
|
||||
if (queue.freed > 0 && stats.cpu >= 10) {
|
||||
q.resetTimeout();
|
||||
onData('.');
|
||||
}
|
||||
},
|
||||
onUsageInterval: 10,
|
||||
abort: myAbort
|
||||
});
|
||||
if (result.code != 0) {
|
||||
let error = result.code;
|
||||
if (this.config.branch == 'development')
|
||||
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||
const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -91,61 +103,14 @@ class ConvertBase {
|
||||
}
|
||||
|
||||
formatFb2(fb2) {
|
||||
let out = '<?xml version="1.0" encoding="utf-8"?>';
|
||||
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">';
|
||||
out += this.formatFb2Node(fb2);
|
||||
out += '</FictionBook>';
|
||||
return out;
|
||||
}
|
||||
|
||||
formatFb2Node(node, name) {
|
||||
let out = '';
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const n of node) {
|
||||
out += this.formatFb2Node(n);
|
||||
const out = xmlParser.formatXml({
|
||||
FictionBook: {
|
||||
_attrs: {xmlns: 'http://www.gribuser.ru/xml/fictionbook/2.0', 'xmlns:l': 'http://www.w3.org/1999/xlink'},
|
||||
_a: [fb2],
|
||||
}
|
||||
} else if (typeof node == 'string') {
|
||||
if (name)
|
||||
out += `<${name}>${this.repSpaces(node)}</${name}>`;
|
||||
else
|
||||
out += this.repSpaces(node);
|
||||
} else {
|
||||
if (node._n)
|
||||
name = node._n;
|
||||
}, 'utf-8', this.repSpaces);
|
||||
|
||||
let attrs = '';
|
||||
if (node._attrs) {
|
||||
for (let attrName in node._attrs) {
|
||||
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
|
||||
}
|
||||
}
|
||||
|
||||
let tOpen = '';
|
||||
let tBody = '';
|
||||
let tClose = '';
|
||||
if (name)
|
||||
tOpen += `<${name}${attrs}>`;
|
||||
if (node.hasOwnProperty('_t'))
|
||||
tBody += this.repSpaces(node._t);
|
||||
|
||||
for (let nodeName in node) {
|
||||
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
|
||||
continue;
|
||||
|
||||
const n = node[nodeName];
|
||||
tBody += this.formatFb2Node(n, nodeName);
|
||||
}
|
||||
|
||||
if (name)
|
||||
tClose += `</${name}>`;
|
||||
|
||||
if (attrs == '' && name == 'p' && tBody.trim() == '')
|
||||
out += '<empty-line/>'
|
||||
else
|
||||
out += `${tOpen}${tBody}${tClose}`;
|
||||
}
|
||||
return out;
|
||||
return out.replace(/<p>\s*?<\/p>/g, '<empty-line/>');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
119
server/core/Reader/BookConverter/ConvertDjvu.js
Normal file
119
server/core/Reader/BookConverter/ConvertDjvu.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const ConvertBase = require('./ConvertBase');
|
||||
|
||||
class ConvertDjvu extends ConvertBase {
|
||||
check(data, opts) {
|
||||
const {inputFiles} = opts;
|
||||
|
||||
return this.config.useExternalBookConverter &&
|
||||
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'djvu';
|
||||
}
|
||||
|
||||
async run(data, opts) {
|
||||
if (!this.check(data, opts))
|
||||
return false;
|
||||
|
||||
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||
|
||||
const ddjvuPath = '/usr/bin/ddjvu';
|
||||
if (!await fs.pathExists(ddjvuPath))
|
||||
throw new Error('Внешний конвертер ddjvu не найден');
|
||||
|
||||
const tiffsplitPath = '/usr/bin/tiffsplit';
|
||||
if (!await fs.pathExists(tiffsplitPath))
|
||||
throw new Error('Внешний конвертер tiffsplitPath не найден');
|
||||
|
||||
const mogrifyPath = '/usr/bin/mogrify';
|
||||
if (!await fs.pathExists(mogrifyPath))
|
||||
throw new Error('Внешний конвертер mogrifyPath не найден');
|
||||
|
||||
const dir = `${inputFiles.filesDir}/`;
|
||||
const inpFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
|
||||
const tifFile = `${inpFile}.tif`;
|
||||
|
||||
//конвертируем в tiff
|
||||
let perc = 0;
|
||||
await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
|
||||
perc = (perc < 100 ? perc + 1 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
|
||||
const tifFileSize = (await fs.stat(tifFile)).size;
|
||||
let limitSize = 3*this.config.maxUploadFileSize;
|
||||
if (tifFileSize > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| ${tifFileSize} > ${limitSize}`);
|
||||
}
|
||||
|
||||
//разбиваем на файлы
|
||||
await this.execConverter(tiffsplitPath, [tifFile, dir], null, abort);
|
||||
|
||||
await fs.remove(tifFile);
|
||||
|
||||
//конвертируем в jpg
|
||||
await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
|
||||
perc = (perc < 100 ? perc + 1 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
|
||||
//читаем изображения
|
||||
limitSize = 2*this.config.maxUploadFileSize;
|
||||
let imagesSize = 0;
|
||||
|
||||
const loadImage = async(image) => {
|
||||
image.data = (await fs.readFile(image.file)).toString('base64');
|
||||
image.name = path.basename(image.file);
|
||||
|
||||
imagesSize += image.data.length;
|
||||
if (imagesSize > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`);
|
||||
}
|
||||
}
|
||||
|
||||
let files = [];
|
||||
await utils.findFiles(async(file) => {
|
||||
if (path.extname(file) == '.jpg')
|
||||
files.push({name: file, base: path.basename(file)});
|
||||
}, dir);
|
||||
|
||||
files.sort((a, b) => a.base.localeCompare(b.base));
|
||||
|
||||
let images = [];
|
||||
let loading = [];
|
||||
files.forEach(f => {
|
||||
const image = {file: f.name};
|
||||
images.push(image);
|
||||
loading.push(loadImage(image));
|
||||
});
|
||||
|
||||
await Promise.all(loading);
|
||||
|
||||
//формируем fb2
|
||||
let titleInfo = {};
|
||||
let desc = {_n: 'description', 'title-info': titleInfo};
|
||||
let pars = [];
|
||||
let body = {_n: 'body', section: {_a: [pars]}};
|
||||
let binary = [];
|
||||
let fb2 = [desc, body, binary];
|
||||
|
||||
let title = '';
|
||||
if (uploadFileName)
|
||||
title = uploadFileName;
|
||||
|
||||
titleInfo['book-title'] = title;
|
||||
|
||||
for (const image of images) {
|
||||
const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': 'image/jpeg'}, _t: image.data};
|
||||
binary.push(img);
|
||||
|
||||
pars.push({_n: 'p', _t: ''});
|
||||
pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
|
||||
}
|
||||
|
||||
return this.formatFb2(fb2);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConvertDjvu;
|
||||
@@ -1,5 +1,6 @@
|
||||
const ConvertBase = require('./ConvertBase');
|
||||
const iconv = require('iconv-lite');
|
||||
const textUtils = require('./textUtils');
|
||||
|
||||
class ConvertFb2 extends ConvertBase {
|
||||
check(data, opts) {
|
||||
@@ -9,26 +10,46 @@ class ConvertFb2 extends ConvertBase {
|
||||
}
|
||||
|
||||
async run(data, opts) {
|
||||
if (!this.check(data, opts))
|
||||
let newData = data;
|
||||
|
||||
//Корректируем кодировку, 16-битные кодировки должны стать utf-8
|
||||
const encoding = textUtils.getEncoding(newData);
|
||||
if (encoding.indexOf('UTF-16') == 0) {
|
||||
newData = Buffer.from(iconv.decode(newData, encoding));
|
||||
}
|
||||
|
||||
if (!this.check(newData, opts))
|
||||
return false;
|
||||
|
||||
return this.checkEncoding(data);
|
||||
return this.checkEncoding(newData);
|
||||
}
|
||||
|
||||
checkEncoding(data) {
|
||||
let result = data;
|
||||
|
||||
const left = data.indexOf('<?xml version="1.0"');
|
||||
let q = '"';
|
||||
let left = data.indexOf('<?xml version="1.0"');
|
||||
if (left < 0) {
|
||||
left = data.indexOf('<?xml version=\'1.0\'');
|
||||
q = '\'';
|
||||
}
|
||||
|
||||
if (left >= 0) {
|
||||
const right = data.indexOf('?>', left);
|
||||
if (right >= 0) {
|
||||
const head = data.slice(left, right + 2).toString();
|
||||
const m = head.match(/encoding="(.*?)"/);
|
||||
const m = head.match(/encoding=['"](.*?)['"]/);
|
||||
if (m) {
|
||||
let encoding = m[1].toLowerCase();
|
||||
if (encoding != 'utf-8') {
|
||||
result = iconv.decode(data, encoding);
|
||||
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
|
||||
//encoding может не соответсвовать реальной кодировке файла, поэтому:
|
||||
let calcEncoding = textUtils.getEncoding(data);
|
||||
if (calcEncoding.indexOf('ISO-8859') >= 0) {
|
||||
calcEncoding = encoding;
|
||||
}
|
||||
|
||||
result = iconv.decode(data, calcEncoding);
|
||||
result = Buffer.from(result.toString().replace(m[0], `encoding=${q}utf-8${q}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
|
||||
|
||||
const ConvertHtml = require('./ConvertHtml');
|
||||
|
||||
class ConvertDocX extends ConvertHtml {
|
||||
class ConvertFb3 extends ConvertHtml {
|
||||
async check(data, opts) {
|
||||
const {inputFiles} = opts;
|
||||
if (this.config.useExternalBookConverter &&
|
||||
@@ -39,13 +39,14 @@ class ConvertDocX extends ConvertHtml {
|
||||
const title = this.getTitle(text)
|
||||
.replace(/<\/?p>/g, '')
|
||||
;
|
||||
text = `<title>${title}</title>` + text
|
||||
text = `<fb2-title>${title}</fb2-title>` + text
|
||||
.replace(/<title>/g, '<br><b>')
|
||||
.replace(/<\/title>/g, '</b><br>')
|
||||
.replace(/<subtitle>/g, '<br><br><subtitle>')
|
||||
.replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<\/subtitle>/g, '</fb2-subtitle>')
|
||||
;
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
||||
return await super.run(Buffer.from(text), {skipCheck: true});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConvertDocX;
|
||||
module.exports = ConvertFb3;
|
||||
|
||||
@@ -34,7 +34,6 @@ class ConvertHtml extends ConvertBase {
|
||||
} else {
|
||||
isText = opts.isText;
|
||||
}
|
||||
let {cutTitle} = opts;
|
||||
|
||||
let titleInfo = {};
|
||||
let desc = {_n: 'description', 'title-info': titleInfo};
|
||||
@@ -44,12 +43,17 @@ class ConvertHtml extends ConvertBase {
|
||||
let fb2 = [desc, body, binary];
|
||||
|
||||
let title = '';
|
||||
let author = '';
|
||||
let inTitle = false;
|
||||
let inSectionTitle = false;
|
||||
let inAuthor = false;
|
||||
let inSubTitle = false;
|
||||
let inImage = false;
|
||||
let image = {};
|
||||
let bold = false;
|
||||
let italic = false;
|
||||
let superscript = false;
|
||||
let subscript = false;
|
||||
let begining = true;
|
||||
|
||||
let spaceCounter = [];
|
||||
@@ -62,7 +66,7 @@ class ConvertHtml extends ConvertBase {
|
||||
};
|
||||
|
||||
const growParagraph = (text) => {
|
||||
if (!pars.length)
|
||||
if (!pars.length || pars[pars.length - 1]._n != 'p')
|
||||
newParagraph();
|
||||
|
||||
const l = pars.length;
|
||||
@@ -94,12 +98,16 @@ class ConvertHtml extends ConvertBase {
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
text = this.escapeEntities(text);
|
||||
|
||||
if (!cutCounter && !(cutTitle && inTitle)) {
|
||||
if (!(cutCounter || inTitle || inSectionTitle || inSubTitle)) {
|
||||
let tOpen = '';
|
||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (superscript ? '<sup>' : '');
|
||||
tOpen += (subscript ? '<sub>' : '');
|
||||
let tClose = ''
|
||||
tClose += (subscript ? '</sub>' : '');
|
||||
tClose += (superscript ? '</sup>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||
@@ -110,12 +118,22 @@ class ConvertHtml extends ConvertBase {
|
||||
if (inTitle && !title)
|
||||
title = text;
|
||||
|
||||
if (inAuthor && !author)
|
||||
author = text;
|
||||
|
||||
if (inSectionTitle) {
|
||||
pars.unshift({_n: 'title', _t: text});
|
||||
}
|
||||
|
||||
if (inSubTitle) {
|
||||
pars.push({_n: 'subtitle', _t: text});
|
||||
}
|
||||
|
||||
if (inImage) {
|
||||
image._t = text;
|
||||
binary.push(image);
|
||||
|
||||
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -140,15 +158,27 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tag == 'sup')
|
||||
superscript = true;
|
||||
|
||||
if (tag == 'sub')
|
||||
subscript = true;
|
||||
}
|
||||
|
||||
if (tag == 'title' || tag == 'cut-title') {
|
||||
if (tag == 'title' || tag == 'fb2-title') {
|
||||
inTitle = true;
|
||||
if (tag == 'cut-title')
|
||||
cutTitle = true;
|
||||
}
|
||||
|
||||
if (tag == 'subtitle') {
|
||||
if (tag == 'fb2-author') {
|
||||
inAuthor = true;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-section-title') {
|
||||
inSectionTitle = true;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-subtitle') {
|
||||
inSubTitle = true;
|
||||
}
|
||||
|
||||
@@ -156,7 +186,7 @@ class ConvertHtml extends ConvertBase {
|
||||
inImage = true;
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
image = {_n: 'binary', _attrs: {id: attrs.name.value, 'content-type': attrs.type.value}, _t: ''};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
@@ -179,12 +209,26 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tag == 'sup')
|
||||
superscript = false;
|
||||
|
||||
if (tag == 'sub')
|
||||
subscript = false;
|
||||
}
|
||||
|
||||
if (tag == 'title' || tag == 'cut-title')
|
||||
if (tag == 'title' || tag == 'fb2-title')
|
||||
inTitle = false;
|
||||
|
||||
if (tag == 'subtitle')
|
||||
if (tag == 'fb2-author') {
|
||||
inAuthor = false;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-section-title') {
|
||||
inSectionTitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-subtitle')
|
||||
inSubTitle = false;
|
||||
|
||||
if (tag == 'fb2-image')
|
||||
@@ -195,10 +239,17 @@ class ConvertHtml extends ConvertBase {
|
||||
|
||||
sax.parseSync(buf, {
|
||||
onStartNode, onEndNode, onTextNode,
|
||||
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image'])
|
||||
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
|
||||
});
|
||||
|
||||
titleInfo['book-title'] = title;
|
||||
if (author)
|
||||
titleInfo.author = {'last-name': author};
|
||||
|
||||
body.section._a[0] = pars;
|
||||
|
||||
//console.log(JSON.stringify(fb2, null, 2));
|
||||
|
||||
//подозрение на чистый текст, надо разбить на параграфы
|
||||
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
|
||||
let total = 0;
|
||||
@@ -218,7 +269,7 @@ class ConvertHtml extends ConvertBase {
|
||||
|
||||
let i = 0;
|
||||
//если разброс не слишком большой, выделяем параграфы
|
||||
if (d < 10 && spaceCounter.length) {
|
||||
if (d < 20 && spaceCounter.length) {
|
||||
total /= 20;
|
||||
i = spaceCounter.length - 1;
|
||||
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
||||
@@ -228,56 +279,49 @@ class ConvertHtml extends ConvertBase {
|
||||
if (parIndent > 2) parIndent--;
|
||||
|
||||
let newPars = [];
|
||||
let curPar = {};
|
||||
const newPar = () => {
|
||||
newPars.push({_n: 'p', _t: ''});
|
||||
curPar = {_n: 'p', _t: ''};
|
||||
newPars.push(curPar);
|
||||
};
|
||||
|
||||
const growPar = (text) => {
|
||||
if (!newPars.length)
|
||||
newPar();
|
||||
|
||||
const l = newPars.length;
|
||||
newPars[l - 1]._t += text;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (const par of pars) {
|
||||
if (par._n != 'p') {
|
||||
newPars.push(par);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
newPar();
|
||||
i++;
|
||||
|
||||
let j = 0;
|
||||
newPar();
|
||||
|
||||
const lines = par._t.split('\n');
|
||||
for (let line of lines) {
|
||||
line = repCrLfTab(line);
|
||||
for (let j = 0; j < lines.length; j++) {
|
||||
const line = repCrLfTab(lines[j]);
|
||||
|
||||
let l = 0;
|
||||
while (l < line.length && line[l] == ' ') {
|
||||
l++;
|
||||
}
|
||||
|
||||
if (l >= parIndent || line == '') {
|
||||
if (j > 0)
|
||||
newPar();
|
||||
j++;
|
||||
if (j > 0 &&
|
||||
(l >= parIndent ||
|
||||
(j < lines.length - 1 && line == '')
|
||||
)
|
||||
) {
|
||||
newPar();
|
||||
}
|
||||
growPar(line.trim() + ' ');
|
||||
|
||||
curPar._t += line.trim() + ' ';
|
||||
}
|
||||
}
|
||||
|
||||
body.section._a[0] = newPars;
|
||||
} else {
|
||||
body.section._a[0] = pars;
|
||||
}
|
||||
|
||||
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
|
||||
bold = false;
|
||||
italic = false;
|
||||
superscript = false;
|
||||
subscript = false;
|
||||
inSubTitle = false;
|
||||
pars = body.section._a[0];
|
||||
for (let i = 0; i < pars.length; i++) {
|
||||
@@ -297,7 +341,11 @@ class ConvertHtml extends ConvertBase {
|
||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (superscript ? '<sup>' : '');
|
||||
tOpen += (subscript ? '<sub>' : '');
|
||||
let tClose = ''
|
||||
tClose += (subscript ? '</sub>' : '');
|
||||
tClose += (superscript ? '</sup>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||
@@ -313,6 +361,10 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = true;
|
||||
if (tag == 'emphasis')
|
||||
italic = true;
|
||||
if (tag == 'sup')
|
||||
superscript = true;
|
||||
if (tag == 'sub')
|
||||
subscript = true;
|
||||
if (tag == 'subtitle')
|
||||
inSubTitle = true;
|
||||
}
|
||||
@@ -322,6 +374,10 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = false;
|
||||
if (tag == 'emphasis')
|
||||
italic = false;
|
||||
if (tag == 'sup')
|
||||
superscript = false;
|
||||
if (tag == 'sub')
|
||||
subscript = false;
|
||||
if (tag == 'subtitle')
|
||||
inSubTitle = false;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//const _ = require('lodash');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
const sax = require('../../sax');
|
||||
const utils = require('../../utils');
|
||||
const ConvertHtml = require('./ConvertHtml');
|
||||
const xmlParser = require('../../xmlParser');
|
||||
|
||||
class ConvertPdf extends ConvertHtml {
|
||||
check(data, opts) {
|
||||
@@ -16,15 +18,24 @@ class ConvertPdf extends ConvertHtml {
|
||||
async run(notUsed, opts) {
|
||||
if (!this.check(notUsed, opts))
|
||||
return false;
|
||||
|
||||
await this.checkExternalConverterPresent();
|
||||
|
||||
const {inputFiles, callback, abort} = opts;
|
||||
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||
|
||||
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
||||
const inpFile = inputFiles.sourceFile;
|
||||
const outBasename = `${inputFiles.filesDir}/${utils.randomHexString(10)}`;
|
||||
const outFile = `${outBasename}.xml`;
|
||||
const metaFile = `${outBasename}_metadata.xml`;
|
||||
|
||||
const pdfaltoPath = `${this.config.dataDir}/pdfalto/pdfalto`;
|
||||
|
||||
if (!await fs.pathExists(pdfaltoPath))
|
||||
throw new Error('Внешний конвертер pdfalto не найден');
|
||||
|
||||
//конвертируем в xml
|
||||
let perc = 0;
|
||||
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
|
||||
await this.execConverter(pdfaltoPath, [inpFile, outFile], () => {
|
||||
perc = (perc < 80 ? perc + 10 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
@@ -33,17 +44,22 @@ class ConvertPdf extends ConvertHtml {
|
||||
const data = await fs.readFile(outFile);
|
||||
callback(90);
|
||||
|
||||
await utils.sleep(100);
|
||||
|
||||
//парсим xml
|
||||
let lines = [];
|
||||
let pagelines = [];
|
||||
let line = {text: ''};
|
||||
let page = {};
|
||||
let fonts = {};
|
||||
let sectionTitleFound = false;
|
||||
|
||||
let images = [];
|
||||
let loading = [];
|
||||
let inText = false;
|
||||
let bold = false;
|
||||
let italic = false;
|
||||
|
||||
let title = '';
|
||||
let prevTop = 0;
|
||||
let author = '';
|
||||
let i = -1;
|
||||
let titleCount = 0;
|
||||
|
||||
const loadImage = async(image) => {
|
||||
const src = path.parse(image.src);
|
||||
@@ -57,7 +73,7 @@ class ConvertPdf extends ConvertHtml {
|
||||
image.type = type;
|
||||
image.name = src.base;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const putImage = (curTop) => {
|
||||
if (!isNaN(curTop) && images.length) {
|
||||
@@ -67,104 +83,180 @@ class ConvertPdf extends ConvertHtml {
|
||||
images.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (!cutCounter && inText) {
|
||||
let tOpen = (bold ? '<b>' : '');
|
||||
tOpen += (italic ? '<i>' : '');
|
||||
let tClose = (italic ? '</i>' : '');
|
||||
tClose += (bold ? '</b>' : '');
|
||||
const putPageLines = () => {
|
||||
pagelines.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left))
|
||||
|
||||
//объединяем в одну строку равные по высоте
|
||||
const pl = [];
|
||||
let pt = 0;
|
||||
let j = -1;
|
||||
pagelines.forEach(line => {
|
||||
//добавим закрывающий тег стиля
|
||||
line.text += line.tClose;
|
||||
|
||||
lines[i].text += `${tOpen}${text}${tClose} `;
|
||||
if (titleCount < 2 && text.trim() != '') {
|
||||
title += text + (titleCount ? '' : ' - ');
|
||||
titleCount++;
|
||||
//проверим, возможно это заголовок
|
||||
if (line.fonts.length == 1 && line.pageWidth) {
|
||||
const f = (line.fonts.length ? fonts[line.fonts[0]] : null);
|
||||
const centerLeft = (line.pageWidth - line.width)/2;
|
||||
if (f && f.isBold && Math.abs(centerLeft - line.left) < 3) {
|
||||
if (!sectionTitleFound) {
|
||||
line.isSectionTitle = true;
|
||||
sectionTitleFound = true;
|
||||
} else {
|
||||
line.isSubtitle = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//объединяем
|
||||
if (pt == 0 || Math.abs(pt - line.top) > 3) {
|
||||
j++;
|
||||
pl[j] = line;
|
||||
} else {
|
||||
pl[j].text += ` ${line.text}`;
|
||||
}
|
||||
pt = line.top;
|
||||
});
|
||||
|
||||
//заполняем lines
|
||||
const lastIndex = i;
|
||||
pl.forEach(line => {
|
||||
putImage(line.top);
|
||||
|
||||
//добавим пустую строку, если надо
|
||||
const prevLine = (i > lastIndex ? lines[i] : {fonts: [], top: 0});
|
||||
if (prevLine && !prevLine.isImage) {
|
||||
const f = (prevLine.fonts.length ? fonts[prevLine.fonts[0]] : (line.fonts.length ? fonts[line.fonts[0]] : null));
|
||||
if (f && f.fontSize && !line.isImage && line.top - prevLine.top > f.fontSize*1.8) {
|
||||
i++;
|
||||
lines[i] = {text: '<br>'};
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
lines[i] = line;
|
||||
});
|
||||
pagelines = [];
|
||||
putImage(100000);
|
||||
};
|
||||
|
||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (!cutCounter) {
|
||||
if (inText) {
|
||||
switch (tag) {
|
||||
case 'i':
|
||||
italic = true;
|
||||
break;
|
||||
case 'b':
|
||||
bold = true;
|
||||
break;
|
||||
if (tag == 'textstyle') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
|
||||
const fontStyle = (attrs.fontstyle && attrs.fontstyle.value ? attrs.fontstyle.value : '');
|
||||
const fontSize = (attrs.fontsize && attrs.fontsize.value ? attrs.fontsize.value : '');
|
||||
|
||||
if (fontId) {
|
||||
const styleTags = {bold: 'b', italics: 'i', superscript: 'sup', subscript: 'sub'};
|
||||
const f = fonts[fontId] = {tOpen: '', tClose: '', isBold: false, fontSize};
|
||||
|
||||
if (fontStyle) {
|
||||
const styles = fontStyle.split(' ');
|
||||
styles.forEach(style => {
|
||||
const s = styleTags[style];
|
||||
if (s) {
|
||||
f.tOpen += `<${s}>`;
|
||||
f.tClose = `</${s}>${f.tClose}`;
|
||||
if (s == 'b')
|
||||
f.isBold = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'text' && !inText) {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
const line = {
|
||||
text: '',
|
||||
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10),
|
||||
left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10),
|
||||
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
||||
};
|
||||
if (tag == 'page') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
page = {
|
||||
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||
};
|
||||
|
||||
if (line.width != 0 || line.height != 0) {
|
||||
inText = true;
|
||||
if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) {
|
||||
putImage(line.top);
|
||||
i++;
|
||||
lines[i] = line;
|
||||
}
|
||||
prevTop = line.top;
|
||||
}
|
||||
putPageLines();
|
||||
}
|
||||
|
||||
if (tag == 'textline') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
line = {
|
||||
text: '',
|
||||
top: parseInt((attrs.vpos && attrs.vpos.value ? attrs.vpos.value : null), 10),
|
||||
left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.value : null), 10),
|
||||
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
||||
tOpen: '',
|
||||
tClose: '',
|
||||
isSectionTitle: false,
|
||||
isSubtitle: false,
|
||||
pageWidth: page.width,
|
||||
fonts: [],
|
||||
};
|
||||
|
||||
if (line.width != 0 || line.height != 0) {
|
||||
pagelines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'image') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
const src = (attrs.src && attrs.src.value ? attrs.src.value : '');
|
||||
if (tag == 'string') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.content && attrs.content.value) {
|
||||
|
||||
let tOpen = '';
|
||||
let tClose = '';
|
||||
const fontId = (attrs.stylerefs && attrs.stylerefs.value ? attrs.stylerefs.value : '');
|
||||
if (fontId && fonts[fontId]) {
|
||||
tOpen = fonts[fontId].tOpen;
|
||||
tClose = fonts[fontId].tClose;
|
||||
if (!line.fonts.length || line.fonts[0] != fontId)
|
||||
line.fonts.push(fontId);
|
||||
}
|
||||
|
||||
if (line.tOpen != tOpen) {
|
||||
line.text += line.tClose + tOpen;
|
||||
line.tOpen = tOpen;
|
||||
line.tClose = tClose;
|
||||
}
|
||||
|
||||
line.text += `${line.text.length ? ' ' : ''}${attrs.content.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'illustration') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.type && attrs.type.value == 'image') {
|
||||
let src = (attrs.fileid && attrs.fileid.value ? attrs.fileid.value : '');
|
||||
if (src) {
|
||||
const image = {
|
||||
isImage: true,
|
||||
src,
|
||||
data: '',
|
||||
type: '',
|
||||
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
|
||||
top: parseInt((attrs.vpos && attrs.vpos.value ? attrs.vpos.value : null), 10) || 0,
|
||||
left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.value : null), 10) || 0,
|
||||
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10) || 0,
|
||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10) || 0,
|
||||
};
|
||||
loading.push(loadImage(image));
|
||||
images.push(image);
|
||||
images.sort((a, b) => a.top - b.top)
|
||||
const exists = images.filter(img => (img.top == image.top && img.left == image.left && img.width == image.width && img.height == image.height));
|
||||
if (!exists.length) {
|
||||
loading.push(loadImage(image));
|
||||
images.push(image);
|
||||
images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'page') {
|
||||
putImage(100000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (inText) {
|
||||
switch (tag) {
|
||||
case 'i':
|
||||
italic = false;
|
||||
break;
|
||||
case 'b':
|
||||
bold = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'text')
|
||||
inText = false;
|
||||
};
|
||||
|
||||
let buf = this.decode(data).toString();
|
||||
sax.parseSync(buf, {
|
||||
onStartNode, onEndNode, onTextNode
|
||||
onStartNode
|
||||
});
|
||||
|
||||
putImage(100000);
|
||||
putPageLines();
|
||||
|
||||
await Promise.all(loading);
|
||||
await utils.sleep(100);
|
||||
|
||||
//найдем параграфы и отступы
|
||||
const indents = [];
|
||||
@@ -185,16 +277,51 @@ class ConvertPdf extends ConvertHtml {
|
||||
}
|
||||
indents[0] = 0;
|
||||
|
||||
//title
|
||||
if (fs.pathExists(metaFile)) {
|
||||
const metaXmlString = (await fs.readFile(metaFile)).toString();
|
||||
let metaXmlParsed = xmlParser.parseXml(metaXmlString);
|
||||
metaXmlParsed = xmlParser.simplifyXmlParsed(metaXmlParsed);
|
||||
if (metaXmlParsed.metadata) {
|
||||
title = (metaXmlParsed.metadata.title ? metaXmlParsed.metadata.title._t : '');
|
||||
author = (metaXmlParsed.metadata.author ? metaXmlParsed.metadata.author._t : '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!title && uploadFileName)
|
||||
title = uploadFileName;
|
||||
|
||||
//console.log(JSON.stringify(lines, null, 2));
|
||||
//формируем текст
|
||||
let text = `<title>${title}</title>`;
|
||||
const limitSize = 2*this.config.maxUploadFileSize;
|
||||
let text = '';
|
||||
if (title)
|
||||
text += `<fb2-title>${title}</fb2-title>`;
|
||||
if (author)
|
||||
text += `<fb2-author>${author}</fb2-author>`;
|
||||
|
||||
let concat = '';
|
||||
let sp = '';
|
||||
for (const line of lines) {
|
||||
if (text.length > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||
}
|
||||
|
||||
if (line.isImage) {
|
||||
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.isSectionTitle) {
|
||||
text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.isSubtitle) {
|
||||
text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (concat == '') {
|
||||
const left = line.left || 0;
|
||||
sp = ' '.repeat(indents[left]);
|
||||
@@ -212,7 +339,9 @@ class ConvertPdf extends ConvertHtml {
|
||||
if (concat)
|
||||
text += sp + concat + "\n";
|
||||
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
|
||||
//console.log(text);
|
||||
await utils.sleep(100);
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
|
||||
if (text === false)
|
||||
return false;
|
||||
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
||||
return await super.run(Buffer.from(text), {skipCheck: true});
|
||||
}
|
||||
|
||||
getTitle(text) {
|
||||
@@ -79,7 +79,7 @@ class ConvertSites extends ConvertHtml {
|
||||
let book = this.getTitle(text);
|
||||
book = book.replace(' (fb2) | Флибуста', '');
|
||||
|
||||
const title = `<title>${author}${(author ? ' - ' : '')}${book}</title>`;
|
||||
const title = `<fb2-title>${author}${(author ? ' - ' : '')}${book}</fb2-title>`;
|
||||
|
||||
let begin = '<h3 class="book">';
|
||||
if (text.indexOf(begin) <= 0)
|
||||
@@ -95,12 +95,12 @@ class ConvertSites extends ConvertHtml {
|
||||
return text.substring(l, r)
|
||||
.replace(/blockquote class="?book"?/g, 'p')
|
||||
.replace(/<br\/?>\s*<\/h3>/g, '</h3>')
|
||||
.replace(/<h3 class="?book"?>/g, '<br><br><subtitle>')
|
||||
.replace(/<h5 class="?book"?>/g, '<br><br><subtitle>')
|
||||
.replace(/<h3>/g, '<br><br><subtitle>')
|
||||
.replace(/<h5>/g, '<br><br><subtitle>')
|
||||
.replace(/<\/h3>/g, '</subtitle><br>')
|
||||
.replace(/<\/h5>/g, '</subtitle><br>')
|
||||
.replace(/<h3 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<h5 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<h3>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<h5>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<\/h3>/g, '</fb2-subtitle><br>')
|
||||
.replace(/<\/h5>/g, '</fb2-subtitle><br>')
|
||||
.replace(/<div class="?stanza"?>/g, '<br>')
|
||||
.replace(/<div>/g, '<br>')
|
||||
+ title;
|
||||
|
||||
@@ -4,6 +4,7 @@ const FileDetector = require('../../FileDetector');
|
||||
//порядок важен
|
||||
const convertClassFactory = [
|
||||
require('./ConvertEpub'),
|
||||
require('./ConvertDjvu'),
|
||||
require('./ConvertPdf'),
|
||||
require('./ConvertRtf'),
|
||||
require('./ConvertDocX'),
|
||||
|
||||
@@ -12,7 +12,7 @@ const utils = require('../utils');
|
||||
const log = new (require('../AppLogger'))().log;//singleton
|
||||
|
||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
||||
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
|
||||
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||
|
||||
let instance = null;
|
||||
|
||||
@@ -30,7 +30,7 @@ class ReaderWorker {
|
||||
|
||||
this.workerState = new WorkerState();
|
||||
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||
this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
|
||||
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
||||
this.bookConverter = new BookConverter(this.config);
|
||||
|
||||
this.remoteWebDavStorage = false;
|
||||
@@ -81,7 +81,7 @@ class ReaderWorker {
|
||||
const decompDirname = utils.randomHexString(30);
|
||||
|
||||
//download or use uploaded
|
||||
if (url.indexOf('file://') != 0) {//download
|
||||
if (url.indexOf('disk://') != 0) {//download
|
||||
const downdata = await this.down.load(url, (progress) => {
|
||||
wState.set({progress});
|
||||
}, q.abort);
|
||||
@@ -130,6 +130,8 @@ class ReaderWorker {
|
||||
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
||||
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
||||
wState.set({progress});
|
||||
if (queue.freed > 0)
|
||||
q.resetTimeout();
|
||||
}, q.abort);
|
||||
|
||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||
@@ -171,9 +173,15 @@ class ReaderWorker {
|
||||
|
||||
} catch (e) {
|
||||
log(LM_ERR, e.stack);
|
||||
if (e.message == 'abort')
|
||||
e.message = overLoadMes;
|
||||
wState.set({state: 'error', error: e.message});
|
||||
let mes = e.message.split('|FORLOG|');
|
||||
if (mes[1])
|
||||
log(LM_ERR, mes[0] + mes[1]);
|
||||
log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
|
||||
|
||||
mes = mes[0];
|
||||
if (mes == 'abort')
|
||||
mes = overLoadMes;
|
||||
wState.set({state: 'error', error: mes});
|
||||
} finally {
|
||||
//clean
|
||||
if (q)
|
||||
@@ -208,7 +216,7 @@ class ReaderWorker {
|
||||
await fs.remove(file.path);
|
||||
}
|
||||
|
||||
return `file://${hash}`;
|
||||
return `disk://${hash}`;
|
||||
}
|
||||
|
||||
async restoreRemoteFile(filename) {
|
||||
|
||||
@@ -76,13 +76,13 @@ class ZipStreamer {
|
||||
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
||||
const entries = Object.values(unzip.entries());
|
||||
if (limitFileCount && entries.length > limitFileCount) {
|
||||
reject('Слишком много файлов');
|
||||
reject(new Error('Слишком много файлов'));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
||||
reject('Файл слишком большой');
|
||||
reject(new Error('Файл слишком большой'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ function parseSync(xstr, options) {
|
||||
onCdata: _onCdata = dummy,
|
||||
onComment: _onComment = dummy,
|
||||
onProgress: _onProgress = dummy,
|
||||
innerCut = new Set()
|
||||
innerCut = new Set(),
|
||||
lowerCase = true,
|
||||
} = options;
|
||||
|
||||
let i = 0;
|
||||
@@ -91,7 +92,8 @@ function parseSync(xstr, options) {
|
||||
} else {
|
||||
tag = tagData;
|
||||
}
|
||||
tag = tag.toLowerCase();
|
||||
if (lowerCase)
|
||||
tag = tag.toLowerCase();
|
||||
|
||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||
if (!cutCounter)
|
||||
@@ -146,7 +148,8 @@ async function parse(xstr, options) {
|
||||
onCdata: _onCdata = dummy,
|
||||
onComment: _onComment = dummy,
|
||||
onProgress: _onProgress = dummy,
|
||||
innerCut = new Set()
|
||||
innerCut = new Set(),
|
||||
lowerCase = true,
|
||||
} = options;
|
||||
|
||||
let i = 0;
|
||||
@@ -231,7 +234,8 @@ async function parse(xstr, options) {
|
||||
} else {
|
||||
tag = tagData;
|
||||
}
|
||||
tag = tag.toLowerCase();
|
||||
if (lowerCase)
|
||||
tag = tag.toLowerCase();
|
||||
|
||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||
if (!cutCounter)
|
||||
@@ -276,7 +280,7 @@ async function parse(xstr, options) {
|
||||
await _onProgress(100);
|
||||
}
|
||||
|
||||
function getAttrsSync(tail) {
|
||||
function getAttrsSync(tail, lowerCase = true) {
|
||||
let result = {};
|
||||
let name = '';
|
||||
let value = '';
|
||||
@@ -287,13 +291,16 @@ function getAttrsSync(tail) {
|
||||
let waitEq = false;
|
||||
|
||||
const pushResult = () => {
|
||||
if (lowerCase)
|
||||
name = name.toLowerCase();
|
||||
if (name != '') {
|
||||
const fn = name;
|
||||
let ns = '';
|
||||
if (name.indexOf(':') >= 0) {
|
||||
[ns, name] = name.split(':');
|
||||
if (fn.indexOf(':') >= 0) {
|
||||
[ns, name] = fn.split(':');
|
||||
}
|
||||
|
||||
result[name] = {value, ns};
|
||||
result[name] = {value, ns, fn};
|
||||
}
|
||||
name = '';
|
||||
value = '';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const baseX = require('base-x');
|
||||
const pidusage = require('pidusage');
|
||||
|
||||
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
const bs36 = baseX(BASE36);
|
||||
@@ -45,10 +47,11 @@ async function touchFile(filename) {
|
||||
}
|
||||
|
||||
function spawnProcess(cmd, opts) {
|
||||
let {args, killAfter, onData, abort} = opts;
|
||||
let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts;
|
||||
killAfter = (killAfter ? killAfter : 120);//seconds
|
||||
onData = (onData ? onData : () => {});
|
||||
args = (args ? args : []);
|
||||
onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds
|
||||
|
||||
return new Promise((resolve, reject) => { (async() => {
|
||||
let resolved = false;
|
||||
@@ -75,9 +78,19 @@ function spawnProcess(cmd, opts) {
|
||||
reject({status: 'error', error, stdout, stderr});
|
||||
});
|
||||
|
||||
//ждем процесс, контролируем его работу раз в секунду
|
||||
let onUsageCounter = onUsageInterval;
|
||||
while (!resolved) {
|
||||
await sleep(1000);
|
||||
killAfter -= 1;
|
||||
|
||||
onUsageCounter--;
|
||||
if (onUsage && onUsageCounter <= 0) {
|
||||
const stats = await pidusage(proc.pid);
|
||||
onUsage(stats);
|
||||
onUsageCounter = onUsageInterval;
|
||||
}
|
||||
|
||||
killAfter--;
|
||||
if (killAfter <= 0 || (abort && abort())) {
|
||||
process.kill(proc.pid);
|
||||
if (killAfter <= 0) {
|
||||
@@ -91,6 +104,22 @@ function spawnProcess(cmd, opts) {
|
||||
})().catch(reject); });
|
||||
}
|
||||
|
||||
async function findFiles(callback, dir) {
|
||||
if (!(callback && dir))
|
||||
return;
|
||||
let result = true;
|
||||
const files = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
const found = path.resolve(dir, file.name);
|
||||
if (file.isDirectory())
|
||||
result = await findFiles(callback, found);
|
||||
else
|
||||
await callback(found);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toBase36,
|
||||
fromBase36,
|
||||
@@ -99,5 +128,6 @@ module.exports = {
|
||||
sleep,
|
||||
randomHexString,
|
||||
touchFile,
|
||||
spawnProcess
|
||||
spawnProcess,
|
||||
findFiles
|
||||
};
|
||||
143
server/core/xmlParser.js
Normal file
143
server/core/xmlParser.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const sax = require('./sax');
|
||||
|
||||
function formatXml(xmlParsed, encoding = 'utf-8', textFilterFunc) {
|
||||
let out = `<?xml version="1.0" encoding="${encoding}"?>`;
|
||||
out += formatXmlNode(xmlParsed, textFilterFunc);
|
||||
return out;
|
||||
}
|
||||
|
||||
function formatXmlNode(node, textFilterFunc) {
|
||||
textFilterFunc = (textFilterFunc ? textFilterFunc : text => text);
|
||||
|
||||
const formatNode = (node, name) => {
|
||||
let out = '';
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const n of node) {
|
||||
out += formatNode(n);
|
||||
}
|
||||
} else if (typeof node == 'string') {
|
||||
if (name)
|
||||
out += `<${name}>${textFilterFunc(node)}</${name}>`;
|
||||
else
|
||||
out += textFilterFunc(node);
|
||||
} else {
|
||||
if (node._n)
|
||||
name = node._n;
|
||||
|
||||
let attrs = '';
|
||||
if (node._attrs) {
|
||||
for (let attrName in node._attrs) {
|
||||
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
|
||||
}
|
||||
}
|
||||
|
||||
let tOpen = '';
|
||||
let tBody = '';
|
||||
let tClose = '';
|
||||
if (name)
|
||||
tOpen += `<${name}${attrs}>`;
|
||||
if (node.hasOwnProperty('_t'))
|
||||
tBody += textFilterFunc(node._t);
|
||||
|
||||
for (let nodeName in node) {
|
||||
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
|
||||
continue;
|
||||
|
||||
const n = node[nodeName];
|
||||
tBody += formatNode(n, nodeName);
|
||||
}
|
||||
|
||||
if (name)
|
||||
tClose += `</${name}>`;
|
||||
|
||||
out += `${tOpen}${tBody}${tClose}`;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return formatNode(node);
|
||||
}
|
||||
|
||||
function parseXml(xmlString, lowerCase = true) {
|
||||
let result = {};
|
||||
let node = result;
|
||||
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
node._t = text;
|
||||
};
|
||||
|
||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (tag == '?xml')
|
||||
return;
|
||||
|
||||
const newNode = {_n: tag, _p: node};
|
||||
|
||||
if (tail) {
|
||||
const parsedAttrs = sax.getAttrsSync(tail, lowerCase);
|
||||
const atKeys = Object.keys(parsedAttrs);
|
||||
if (atKeys.length) {
|
||||
const attrs = {};
|
||||
for (let i = 0; i < atKeys.length; i++) {
|
||||
const attrName = atKeys[i];
|
||||
attrs[parsedAttrs[attrName].fn] = parsedAttrs[attrName].value;
|
||||
}
|
||||
|
||||
newNode._attrs = attrs;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node._a)
|
||||
node._a = [];
|
||||
node._a.push(newNode);
|
||||
node = newNode;
|
||||
};
|
||||
|
||||
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (node._p && node._n == tag)
|
||||
node = node._p;
|
||||
};
|
||||
|
||||
sax.parseSync(xmlString, {
|
||||
onStartNode, onEndNode, onTextNode, lowerCase
|
||||
});
|
||||
|
||||
if (result._a)
|
||||
result = result._a[0];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function simplifyXmlParsed(node) {
|
||||
|
||||
const simplifyNodeArray = (a) => {
|
||||
const result = {};
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const child = a[i];
|
||||
if (child._n && !result[child._n]) {
|
||||
result[child._n] = {};
|
||||
if (child._a) {
|
||||
result[child._n] = simplifyNodeArray(child._a);
|
||||
}
|
||||
if (child._t) {
|
||||
result[child._n]._t = child._t;
|
||||
}
|
||||
if (child._attrs) {
|
||||
result[child._n]._attrs = child._attrs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return simplifyNodeArray([node]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatXml,
|
||||
formatXmlNode,
|
||||
parseXml,
|
||||
simplifyXmlParsed
|
||||
}
|
||||
Reference in New Issue
Block a user