Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef12a84285 | ||
|
|
6a18ae3f27 | ||
|
|
a250e95950 | ||
|
|
b174ae452b | ||
|
|
0b63bce357 | ||
|
|
de0d10e792 | ||
|
|
b358b340b4 | ||
|
|
455aba7f4f | ||
|
|
fde0437157 | ||
|
|
480c95bd63 | ||
|
|
972f957685 | ||
|
|
40ff04e5dc | ||
|
|
b3c028bd7a | ||
|
|
51ec6a54fa | ||
|
|
7a29b16ee8 | ||
|
|
7af6fd8248 | ||
|
|
e1c93169b5 | ||
|
|
f4716d5a1e | ||
|
|
f5c06ce420 | ||
|
|
9492f85d80 | ||
|
|
b1303a3ba2 | ||
|
|
5c9cfe5e6f | ||
|
|
b89b5322b8 | ||
|
|
945feba6b2 | ||
|
|
c8af4b907b | ||
|
|
298e8928cf | ||
|
|
8cb67d2976 | ||
|
|
32b8382641 | ||
|
|
007e97463b | ||
|
|
e4f190698d | ||
|
|
b3be07b17e | ||
|
|
72f8977071 | ||
|
|
3dbf00344e | ||
|
|
ffdf0b12cd | ||
|
|
a51150c729 | ||
|
|
37e14b397c | ||
|
|
e48af7ee7d | ||
|
|
3eb3dd371a | ||
|
|
8ef6551560 | ||
|
|
b1f5f3dd28 |
@@ -16,6 +16,7 @@
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||
<q-tab name="images" icon="la la-image" label="Изображения" />
|
||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
@@ -25,7 +26,7 @@
|
||||
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
||||
<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 class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
|
||||
</div>
|
||||
@@ -40,7 +41,7 @@
|
||||
</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 v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
|
||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||
<div class="no-expand-button"></div>
|
||||
<div :style="subitem.indentStyle"></div>
|
||||
@@ -56,6 +57,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'images'">
|
||||
<div>
|
||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div class="image-thumb-box row justify-center items-center">
|
||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
|
||||
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
|
||||
</div>
|
||||
<div class="no-expand-button column justify-center items-center">
|
||||
<div class="image-num">{{ item.num }}</div>
|
||||
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
|
||||
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
|
||||
<div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
|
||||
Изображения отсутствуют
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
|
||||
<div class="column justify-center items-center" style="height: 100px">
|
||||
Раздел находится в разработке
|
||||
@@ -74,16 +102,29 @@ import Component from 'vue-class-component';
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ContentsPageProps = Vue.extend({
|
||||
props: {
|
||||
bookPos: Number,
|
||||
isVisible: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
bookPos: function() {
|
||||
this.updateBookPosSelection();
|
||||
}
|
||||
},
|
||||
})
|
||||
class ContentsPage extends Vue {
|
||||
class ContentsPage extends ContentsPageProps {
|
||||
selectedTab = 'contents';
|
||||
contents = [];
|
||||
images = [];
|
||||
imageSrc = [];
|
||||
imageLoaded = [];
|
||||
|
||||
created() {
|
||||
}
|
||||
@@ -93,34 +134,48 @@ class ContentsPage extends Vue {
|
||||
|
||||
//закладки
|
||||
|
||||
//далее формаирование оглавления
|
||||
if (this.parsed == parsed)
|
||||
//проверим, надо ли обновлять списки
|
||||
if (this.parsed == parsed) {
|
||||
this.updateBookPosSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
//далее формирование оглавления
|
||||
this.parsed = parsed;
|
||||
this.contents = [];
|
||||
await this.$nextTick();
|
||||
|
||||
const pc = parsed.contents;
|
||||
const newpc = [];
|
||||
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||
let curSubtitles = [];
|
||||
let prevBodyIndex = -1;
|
||||
for (let i = 0; i < pc.length; i++) {
|
||||
const cont = pc[i];
|
||||
if (prevBodyIndex != cont.bodyIndex)
|
||||
curSubtitles = [];
|
||||
const ims = parsed.images;
|
||||
const newpc = [];
|
||||
if (pc.length) {//если есть оглавление
|
||||
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||
let curSubtitles = [];
|
||||
let prevBodyIndex = -1;
|
||||
for (let i = 0; i < pc.length; i++) {
|
||||
const cont = pc[i];
|
||||
if (prevBodyIndex != cont.bodyIndex)
|
||||
curSubtitles = [];
|
||||
|
||||
prevBodyIndex = cont.bodyIndex;
|
||||
prevBodyIndex = cont.bodyIndex;
|
||||
|
||||
if (cont.bodyIndex > 1) {
|
||||
if (cont.inset < 1) {
|
||||
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||
if (cont.bodyIndex > 1) {
|
||||
if (cont.inset < 1) {
|
||||
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||
} else {
|
||||
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||
}
|
||||
} else {
|
||||
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||
newpc.push(cont);
|
||||
}
|
||||
}
|
||||
} else {//попробуем вытащить из images
|
||||
for (let i = 0; i < ims.length; i++) {
|
||||
const image = ims[i];
|
||||
|
||||
if (image.alt) {
|
||||
newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
|
||||
}
|
||||
} else {
|
||||
newpc.push(cont);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +221,90 @@ class ContentsPage extends Vue {
|
||||
});
|
||||
|
||||
this.contents = newContents;
|
||||
|
||||
//формируем newImages
|
||||
const newImages = [];
|
||||
for (i = 0; i < ims.length; i++) {
|
||||
const image = ims[i];
|
||||
const bin = parsed.binary[image.id];
|
||||
const type = (bin ? bin.type : '');
|
||||
|
||||
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
|
||||
const indentStyle = getIndentStyle(1);
|
||||
const labelStyle = getLabelStyle(1);
|
||||
|
||||
const p = parsed.para[image.paraIndex];
|
||||
newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
|
||||
indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
|
||||
}
|
||||
|
||||
this.images = newImages;
|
||||
|
||||
if (this.selectedTab == 'contents' && !this.contents.length && this.images.length)
|
||||
this.selectedTab = 'images';
|
||||
|
||||
//выделим на bookPos
|
||||
this.updateBookPosSelection();
|
||||
|
||||
//асинхронная загрузка изображений
|
||||
this.imageSrc = [];
|
||||
this.imageLoaded = [];
|
||||
await utils.sleep(50);
|
||||
(async() => {
|
||||
for (i = 0; i < ims.length; i++) {
|
||||
const {id, local} = ims[i];
|
||||
const bin = this.parsed.binary[id];
|
||||
if (local)
|
||||
this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : ''));
|
||||
else
|
||||
this.$set(this.imageSrc, id, id);
|
||||
this.imageLoaded[id] = true;
|
||||
await utils.sleep(5);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async updateBookPosSelection() {
|
||||
if (!this.isVisible)
|
||||
return;
|
||||
|
||||
await utils.sleep(50);
|
||||
const bp = this.bookPos;
|
||||
|
||||
for (let i = 0; i < this.contents.length; i++) {
|
||||
const item = this.contents[i];
|
||||
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
for (let j = 0; j < item.list.length; j++) {
|
||||
const subitem = item.list[j];
|
||||
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||
|
||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||
subitem.isBookPos = true;
|
||||
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||
} else if (subitem.isBookPos) {
|
||||
subitem.isBookPos = false;
|
||||
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||
}
|
||||
}
|
||||
|
||||
if (bp >= item.offset && bp < nextOffset) {
|
||||
this.$set(this.contents, i, Object.assign(item, {isBookPos: true}));
|
||||
} else if (item.isBookPos) {
|
||||
this.$set(this.contents, i, Object.assign(item, {isBookPos: false}));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.images.length; i++) {
|
||||
const img = this.images[i];
|
||||
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= img.offset && bp < nextOffset) {
|
||||
this.$set(this.images, i, Object.assign(img, {isBookPos: true}));
|
||||
} else if (img.isBookPos) {
|
||||
this.$set(this.images, i, Object.assign(img, {isBookPos: false}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async expandClick(key) {
|
||||
@@ -219,7 +358,7 @@ class ContentsPage extends Vue {
|
||||
padding: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.item, .subitem {
|
||||
.item, .subitem, .item-book-pos, .subitem-book-pos {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
@@ -227,6 +366,22 @@ class ContentsPage extends Vue {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.item-book-pos {
|
||||
background-color: #b0f0b0;
|
||||
}
|
||||
|
||||
.subitem-book-pos {
|
||||
background-color: #d0f5d0;
|
||||
}
|
||||
|
||||
.item-book-pos:hover {
|
||||
background-color: #b0e0b0;
|
||||
}
|
||||
|
||||
.subitem-book-pos:hover {
|
||||
background-color: #d0f0d0;
|
||||
}
|
||||
|
||||
.expand-button, .no-expand-button {
|
||||
width: 40px;
|
||||
}
|
||||
@@ -244,4 +399,38 @@ class ContentsPage extends Vue {
|
||||
.expanded-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.image-num {
|
||||
font-size: 120%;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.image-type {
|
||||
border: 1px solid black;
|
||||
border-radius: 6px;
|
||||
font-size: 80%;
|
||||
padding: 2px 0 2px 0;
|
||||
width: 34px;
|
||||
}
|
||||
.it-jpg-color {
|
||||
background: linear-gradient(to right, #fabc3d, #ffec6d);
|
||||
}
|
||||
.it-png-color {
|
||||
background: linear-gradient(to right, #4bc4e5, #6bf4ff);
|
||||
}
|
||||
.it-net-color {
|
||||
background: linear-gradient(to right, #00c400, #00f400);
|
||||
}
|
||||
|
||||
.image-thumb-box {
|
||||
width: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-thumb {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.loading-img-icon {
|
||||
font-size: 250%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
<q-icon name="la la-copy" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
|
||||
<q-icon name="la la-retweet" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip>
|
||||
<button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
|
||||
<q-icon name="la la-magic" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
||||
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||
@@ -99,7 +99,7 @@
|
||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||
<ContentsPage v-show="contentsActive" ref="contentsPage" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||
|
||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
||||
</div>
|
||||
@@ -317,6 +317,10 @@ class Reader extends Vue {
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||
this.splitToPara = settings.splitToPara;
|
||||
this.djvuQuality = settings.djvuQuality;
|
||||
this.pdfAsText = settings.pdfAsText;
|
||||
this.pdfQuality = settings.pdfQuality;
|
||||
|
||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||
this.$root.readerActionByKeyEvent = (event) => {
|
||||
@@ -336,7 +340,7 @@ class Reader extends Vue {
|
||||
|
||||
let againMes = '';
|
||||
if (this.isFirstNeedUpdateNotify) {
|
||||
againMes = ' ЕЩЕ один раз';
|
||||
againMes = ' еще один раз';
|
||||
}
|
||||
|
||||
if (this.version != this.clientVersion)
|
||||
@@ -345,9 +349,9 @@ class Reader extends Vue {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.checkingNewVersion = false;
|
||||
}
|
||||
}
|
||||
this.isFirstNeedUpdateNotify = false;
|
||||
}
|
||||
this.isFirstNeedUpdateNotify = false;
|
||||
}
|
||||
|
||||
updateHeaderMinWidth() {
|
||||
@@ -703,6 +707,12 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
convOptionsToggle() {
|
||||
this.settingsToggle();
|
||||
if (this.settingsActive)
|
||||
this.$refs.settingsPage.selectedTab = 'convert';
|
||||
}
|
||||
|
||||
helpToggle() {
|
||||
this.helpActive = !this.helpActive;
|
||||
if (this.helpActive) {
|
||||
@@ -729,15 +739,9 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
refreshBook(mode) {
|
||||
refreshBook() {
|
||||
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});
|
||||
}
|
||||
}
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||
}
|
||||
|
||||
undoAction() {
|
||||
@@ -777,7 +781,7 @@ class Reader extends Vue {
|
||||
case 'scrolling':
|
||||
case 'search':
|
||||
case 'copyText':
|
||||
case 'splitToPara':
|
||||
case 'convOptions':
|
||||
case 'refresh':
|
||||
case 'contents':
|
||||
case 'libs':
|
||||
@@ -811,7 +815,6 @@ class Reader extends Vue {
|
||||
case 'contents':
|
||||
classResult = classDisabled;
|
||||
break;
|
||||
case 'splitToPara':
|
||||
case 'refresh':
|
||||
case 'recentBooks':
|
||||
if (!this.mostRecentBookReactive)
|
||||
@@ -973,10 +976,13 @@ class Reader extends Vue {
|
||||
if (!book) {
|
||||
book = await readerApi.loadBook({
|
||||
url,
|
||||
skipCheck: (opts.skipCheck ? true : false),
|
||||
isText: (opts.isText ? true : false),
|
||||
uploadFileName,
|
||||
enableSitesFilter: this.enableSitesFilter,
|
||||
uploadFileName
|
||||
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||
isText: (this.splitToPara ? true : false),
|
||||
djvuQuality: this.djvuQuality,
|
||||
pdfAsText: this.pdfAsText,
|
||||
pdfQuality: this.pdfQuality,
|
||||
},
|
||||
(state) => {
|
||||
progress.setState(state);
|
||||
@@ -1102,8 +1108,8 @@ class Reader extends Vue {
|
||||
case 'copyText':
|
||||
this.copyTextToggle();
|
||||
break;
|
||||
case 'splitToPara':
|
||||
this.refreshBook('split');
|
||||
case 'convOptions':
|
||||
this.convOptionsToggle();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
|
||||
@@ -57,37 +57,6 @@
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||
</span>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog3" v-model="liberamaTopVisible">
|
||||
<template slot="header">
|
||||
Здравствуйте, уважаемые читатели!
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Создан новый ресурс:<br><br>
|
||||
|
||||
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
|
||||
<br><br>
|
||||
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
|
||||
|
||||
<ul>
|
||||
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
|
||||
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
|
||||
</ul>
|
||||
|
||||
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
|
||||
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
|
||||
Спасибо, что вы с нами!
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -114,7 +83,6 @@ class ReaderDialogs extends Vue {
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
liberamaTopVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -127,14 +95,12 @@ class ReaderDialogs extends Vue {
|
||||
async init() {
|
||||
await this.showWhatsNew();
|
||||
await this.showDonation();
|
||||
await this.showLiberamaTop();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const settings = this.settings;
|
||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
|
||||
}
|
||||
|
||||
async showWhatsNew() {
|
||||
@@ -171,7 +137,6 @@ class ReaderDialogs extends Vue {
|
||||
|
||||
openDonate() {
|
||||
this.donationVisible = false;
|
||||
this.liberamaTopVisible = false;
|
||||
this.$emit('donate-toggle');
|
||||
}
|
||||
|
||||
@@ -210,24 +175,8 @@ class ReaderDialogs extends Vue {
|
||||
return this.$store.state.reader.donationRemindDate;
|
||||
}
|
||||
|
||||
async showLiberamaTop() {
|
||||
const today = utils.formatDate(new Date(), 'coDate');
|
||||
|
||||
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
|
||||
await utils.sleep(3000);
|
||||
this.liberamaTopVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
liberamaTopDialogDisable() {
|
||||
this.liberamaTopVisible = false;
|
||||
if (this.showLiberamaTopDialog2020) {
|
||||
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
|
||||
}
|
||||
}
|
||||
|
||||
keyHook() {
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
||||
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
||||
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
||||
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
||||
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
||||
@@ -53,6 +54,10 @@
|
||||
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
||||
@@include('./include/PageMoveTab.inc');
|
||||
</div>
|
||||
<!-- Конвертирование ------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
||||
@@include('./include/ConvertTab.inc');
|
||||
</div>
|
||||
<!-- Прочее ---------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
||||
@@include('./include/OthersTab.inc');
|
||||
@@ -218,6 +223,10 @@ class SettingsPage extends Vue {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return this.$store.state.reader.settings;
|
||||
}
|
||||
@@ -544,7 +553,7 @@ class SettingsPage extends Vue {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label-1 {
|
||||
.label-1, .label-7 {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
@@ -556,7 +565,7 @@ class SettingsPage extends Vue {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
|
||||
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
87
client/components/Reader/SettingsPage/include/ConvertTab.inc
Normal file
87
client/components/Reader/SettingsPage/include/ConvertTab.inc
Normal file
@@ -0,0 +1,87 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="q-mt-sm column items-center">
|
||||
<span>Настройки конвертирования применяются ко всем</span>
|
||||
<span>вновь загружаемым или обновляемым файлам</span>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">HTML, XML, TXT</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Текст</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Опция принудительно включает эвристику разбиения текста на<br>
|
||||
параграфы в случае, если формат файла определен как html,<br>
|
||||
xml или txt. Возможна нечитабельная разметка текста.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Сайты</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div v-if="isExternalConverter">
|
||||
<div class="part-header">PDF</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Формат</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
|
||||
Размер получаемого fb2-файла при этом относительно небольшой.<br>
|
||||
При отключении этой опции, pdf будет представлен как набор<br>
|
||||
изображений (аналогично ковертированию djvu).
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Качество</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
|
||||
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||
слишком большой файл, то попробуйте понизить качество.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div v-if="isExternalConverter">
|
||||
<div class="part-header">DJVU</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Качество</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
|
||||
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||
слишком большой файл, то попробуйте понизить качество.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,22 +65,6 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||
|
||||
@@ -160,12 +160,13 @@ export default class DrawHelper {
|
||||
return out;
|
||||
}
|
||||
|
||||
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength) {
|
||||
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength, imageNum, imageLength) {
|
||||
const pad = 3;
|
||||
const fh = h - 2*pad;
|
||||
const fh2 = fh/2;
|
||||
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
|
||||
const tImg = (imageNum > 0 ? ` (${imageNum}/${imageLength})` : '');
|
||||
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}${tImg}`;
|
||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||
const read = (bookPos + 1)/textLength;
|
||||
const t2 = `${(read*100).toFixed(2)}%`;
|
||||
@@ -188,7 +189,7 @@ export default class DrawHelper {
|
||||
return out;
|
||||
}
|
||||
|
||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
|
||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
|
||||
|
||||
let out = `<div class="layout" style="` +
|
||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||
@@ -206,7 +207,7 @@ export default class DrawHelper {
|
||||
|
||||
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
||||
|
||||
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength);
|
||||
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
|
||||
|
||||
out += '</div>';
|
||||
return out;
|
||||
|
||||
@@ -722,8 +722,24 @@ class TextPage extends Vue {
|
||||
message = this.statusBarMessage;
|
||||
if (!message)
|
||||
message = this.title;
|
||||
|
||||
//check image num
|
||||
let imageNum = 0;
|
||||
const len = (lines.length > 2 ? 2 : lines.length);
|
||||
loop:
|
||||
for (let j = 0; j < len; j++) {
|
||||
const line = lines[j];
|
||||
for (const part of line.parts) {
|
||||
if (part.image) {
|
||||
imageNum = part.image.num;
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
//drawing
|
||||
this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
|
||||
lines[i].end, this.parsed.textLength, message);
|
||||
lines[i].end, this.parsed.textLength, message, imageNum, this.parsed.images.length);
|
||||
|
||||
this.bookPosSeen = lines[i].end;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -54,12 +54,14 @@ export default class BookParser {
|
||||
|
||||
//оглавление
|
||||
this.contents = [];
|
||||
this.images = [];
|
||||
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
||||
let curSubtitle = {paraIndex: -1, title: ''};
|
||||
let inTitle = false;
|
||||
let inSubtitle = false;
|
||||
let sectionLevel = 0;
|
||||
let bodyIndex = 0;
|
||||
let imageNum = 0;
|
||||
|
||||
let paraIndex = -1;
|
||||
let paraOffset = 0;
|
||||
@@ -194,6 +196,7 @@ export default class BookParser {
|
||||
if (tag == 'binary') {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||
binaryType = (binaryType == 'image/jpg' ? 'image/jpeg' : binaryType);
|
||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||
}
|
||||
@@ -202,16 +205,27 @@ export default class BookParser {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
const href = attrs.href.value;
|
||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||
const {id, local} = this.imageHrefToId(href);
|
||||
if (href[0] == '#') {//local
|
||||
imageNum++;
|
||||
|
||||
if (inPara && !this.showInlineImagesInCenter && !center)
|
||||
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
|
||||
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||
else
|
||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
|
||||
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||
|
||||
if (inPara && this.showInlineImagesInCenter)
|
||||
newParagraph(' ', 1);
|
||||
} else {//external
|
||||
imageNum++;
|
||||
|
||||
dimPromises.push(getExternalImageDimensions(href));
|
||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
|
||||
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -488,6 +502,15 @@ export default class BookParser {
|
||||
return {fb2};
|
||||
}
|
||||
|
||||
imageHrefToId(id) {
|
||||
let local = false;
|
||||
if (id[0] == '#') {
|
||||
id = id.substr(1);
|
||||
local = true;
|
||||
}
|
||||
return {id, local};
|
||||
}
|
||||
|
||||
findParaIndex(bookPos) {
|
||||
let result = undefined;
|
||||
//дихотомия
|
||||
@@ -553,28 +576,21 @@ export default class BookParser {
|
||||
case 'image': {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
let id = attrs.href.value;
|
||||
let local = false;
|
||||
if (id[0] == '#') {
|
||||
id = id.substr(1);
|
||||
local = true;
|
||||
}
|
||||
image = {local, inline: false, id};
|
||||
image = this.imageHrefToId(attrs.href.value);
|
||||
image.inline = false;
|
||||
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'image-inline': {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
let id = attrs.href.value;
|
||||
let local = false;
|
||||
if (id[0] == '#') {
|
||||
id = id.substr(1);
|
||||
local = true;
|
||||
}
|
||||
const img = this.imageHrefToId(attrs.href.value);
|
||||
img.inline = true;
|
||||
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||
result.push({
|
||||
style: Object.assign({}, style),
|
||||
image: {local, inline: true, id},
|
||||
image: img,
|
||||
text: ''
|
||||
});
|
||||
}
|
||||
@@ -801,6 +817,7 @@ export default class BookParser {
|
||||
paraIndex,
|
||||
w: imageWidth,
|
||||
h: imageHeight,
|
||||
num: part.image.num
|
||||
}});
|
||||
lines.push(line);
|
||||
line = {begin: line.end + 1, parts: []};
|
||||
@@ -811,7 +828,7 @@ export default class BookParser {
|
||||
line.last = true;
|
||||
line.parts.push({style, text: ' ',
|
||||
image: {local: part.image.local, inline: false, id: part.image.id,
|
||||
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight}
|
||||
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight, num: part.image.num}
|
||||
});
|
||||
|
||||
continue;
|
||||
@@ -823,7 +840,7 @@ export default class BookParser {
|
||||
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
||||
imgW += bin.w*imgH/bin.h;
|
||||
line.parts.push({style, text: '',
|
||||
image: {local: part.image.local, inline: true, id: part.image.id}});
|
||||
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
showUntil: '2020-12-17',
|
||||
header: '0.9.12 (2020-12-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена вкладка "Изображения" в окно оглавления</li>
|
||||
<li>настройки конвертирования вынесены в отдельную вкладку</li>
|
||||
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||
<li>улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-12-08',
|
||||
header: '0.9.11 (2020-12-09)',
|
||||
|
||||
@@ -12,7 +12,7 @@ const readerActions = {
|
||||
'setPosition': 'Установить позицию',
|
||||
'search': 'Найти в тексте',
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'splitToPara': 'Обновить с разбиением на параграфы',
|
||||
'convOptions': 'Настроить конвертирование',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
@@ -41,7 +41,7 @@ const toolButtons = [
|
||||
{name: 'setPosition', show: true},
|
||||
{name: 'search', show: true},
|
||||
{name: 'copyText', show: false},
|
||||
{name: 'splitToPara', show: false},
|
||||
{name: 'convOptions', show: true},
|
||||
{name: 'refresh', show: true},
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
@@ -60,8 +60,8 @@ const hotKeys = [
|
||||
{name: 'scrolling', codes: ['Z']},
|
||||
{name: 'setPosition', codes: ['P']},
|
||||
{name: 'search', codes: ['Ctrl+F']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'splitToPara', codes: ['Shift+R']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||
{name: 'refresh', codes: ['R']},
|
||||
{name: 'contents', codes: ['C']},
|
||||
{name: 'libs', codes: ['L']},
|
||||
@@ -252,11 +252,14 @@ const settingDefaults = {
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
enableSitesFilter: true,
|
||||
splitToPara: false,
|
||||
djvuQuality: 20,
|
||||
pdfAsText: true,
|
||||
pdfQuality: 20,
|
||||
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
showDonationDialog2020: true,
|
||||
showLiberamaTopDialog2020: true,
|
||||
showNeedUpdateNotify: true,
|
||||
|
||||
fontShifts: {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.9.11",
|
||||
"version": "0.9.12",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
|
||||
@@ -20,9 +20,12 @@ class ReaderController extends BaseController {
|
||||
const workerId = this.readerWorker.loadBookUrl({
|
||||
url: request.url,
|
||||
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
||||
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
|
||||
skipHtmlCheck: (request.hasOwnProperty('skipHtmlCheck') ? request.skipHtmlCheck : false),
|
||||
isText: (request.hasOwnProperty('isText') ? request.isText : false),
|
||||
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
|
||||
djvuQuality: (request.hasOwnProperty('djvuQuality') ? request.djvuQuality : false),
|
||||
pdfAsText: (request.hasOwnProperty('pdfAsText') ? request.pdfAsText : false),
|
||||
pdfQuality: (request.hasOwnProperty('pdfQuality') ? request.pdfQuality : false),
|
||||
});
|
||||
const state = this.workerState.getState(workerId);
|
||||
return (state ? state : {});
|
||||
|
||||
@@ -103,6 +103,11 @@ class ConvertBase {
|
||||
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
||||
}
|
||||
|
||||
isDataXml(data) {
|
||||
const str = data.slice(0, 100).toString().trim();
|
||||
return (str.indexOf('<?xml version="1.0"') == 0 || str.indexOf('<?xml version=\'1.0\'') == 0 );
|
||||
}
|
||||
|
||||
formatFb2(fb2) {
|
||||
const out = xmlParser.formatXml({
|
||||
FictionBook: {
|
||||
|
||||
@@ -16,12 +16,21 @@ class ConvertDjvu extends ConvertJpegPng {
|
||||
if (!this.check(data, opts))
|
||||
return false;
|
||||
|
||||
const {inputFiles, callback, abort} = opts;
|
||||
let {inputFiles, callback, abort, djvuQuality} = opts;
|
||||
|
||||
djvuQuality = (djvuQuality && djvuQuality <= 100 && djvuQuality >= 10 ? djvuQuality : 20);
|
||||
let jpegQuality = djvuQuality;
|
||||
let tiffQuality = djvuQuality + 30;
|
||||
tiffQuality = (tiffQuality < 85 ? tiffQuality : 85);
|
||||
|
||||
const ddjvuPath = '/usr/bin/ddjvu';
|
||||
if (!await fs.pathExists(ddjvuPath))
|
||||
throw new Error('Внешний конвертер ddjvu не найден');
|
||||
|
||||
const djvusedPath = '/usr/bin/djvused';
|
||||
if (!await fs.pathExists(djvusedPath))
|
||||
throw new Error('Внешний конвертер djvused не найден');
|
||||
|
||||
const tiffsplitPath = '/usr/bin/tiffsplit';
|
||||
if (!await fs.pathExists(tiffsplitPath))
|
||||
throw new Error('Внешний конвертер tiffsplitPath не найден');
|
||||
@@ -36,7 +45,7 @@ class ConvertDjvu extends ConvertJpegPng {
|
||||
|
||||
//конвертируем в tiff
|
||||
let perc = 0;
|
||||
await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
|
||||
await this.execConverter(ddjvuPath, ['-format=tiff', `-quality=${tiffQuality}`, '-verbose', inputFiles.sourceFile, tifFile], () => {
|
||||
perc = (perc < 100 ? perc + 1 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
@@ -53,22 +62,57 @@ class ConvertDjvu extends ConvertJpegPng {
|
||||
await fs.remove(tifFile);
|
||||
|
||||
//конвертируем в jpg
|
||||
await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
|
||||
await this.execConverter(mogrifyPath, ['-quality', jpegQuality, '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
|
||||
perc = (perc < 100 ? perc + 1 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
|
||||
limitSize = 2*this.config.maxUploadFileSize;
|
||||
let jpgFilesSize = 0;
|
||||
//ищем изображения
|
||||
let files = [];
|
||||
await utils.findFiles(async(file) => {
|
||||
if (path.extname(file) == '.jpg')
|
||||
if (path.extname(file) == '.jpg') {
|
||||
jpgFilesSize += (await fs.stat(file)).size;
|
||||
if (jpgFilesSize > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
|
||||
}
|
||||
|
||||
files.push({name: file, base: path.basename(file)});
|
||||
}
|
||||
}, dir);
|
||||
|
||||
files.sort((a, b) => a.base.localeCompare(b.base));
|
||||
|
||||
//схема документа (outline)
|
||||
const djvusedResult = await this.execConverter(djvusedPath, ['-u', '-e', 'print-outline', inputFiles.sourceFile], null, abort);
|
||||
|
||||
const outline = [];
|
||||
const lines = djvusedResult.stdout.match(/\(\s*".*"\s*?"#\d+"/g);
|
||||
if (lines) {
|
||||
lines.forEach(l => {
|
||||
const m = l.match(/"(.*)"\s*?"#(\d+)"/);
|
||||
if (m) {
|
||||
const pageNum = m[2];
|
||||
let s = outline[pageNum];
|
||||
if (!s)
|
||||
s = m[1].trim();
|
||||
else
|
||||
s += `${(s[s.length - 1] != '.' ? '.' : '')} ${m[1].trim()}`;
|
||||
|
||||
outline[pageNum] = s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await utils.sleep(100);
|
||||
return await super.run(data, Object.assign({}, opts, {imageFiles: files.map(f => f.name)}));
|
||||
let i = 0;
|
||||
const imageFiles = files.map(f => {
|
||||
i++;
|
||||
let alt = (outline[i] ? outline[i] : '');
|
||||
return {src: f.name, alt};
|
||||
});
|
||||
return await super.run(data, Object.assign({}, opts, {imageFiles}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,21 +6,37 @@ class ConvertFb2 extends ConvertBase {
|
||||
check(data, opts) {
|
||||
const {dataType} = opts;
|
||||
|
||||
return (dataType && dataType.ext == 'xml' && data.toString().indexOf('<FictionBook') >= 0);
|
||||
return (
|
||||
( (dataType && dataType.ext == 'xml') || this.isDataXml(data) ) &&
|
||||
data.toString().indexOf('<FictionBook') >= 0
|
||||
);
|
||||
}
|
||||
|
||||
async run(data, opts) {
|
||||
let newData = data;
|
||||
let newData = data.slice(0, 1024);
|
||||
|
||||
//Корректируем кодировку, 16-битные кодировки должны стать utf-8
|
||||
//Корректируем кодировку для проверки, 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;
|
||||
|
||||
//Корректируем кодировку всего объема
|
||||
newData = data;
|
||||
if (encoding.indexOf('UTF-16') == 0) {
|
||||
newData = Buffer.from(iconv.decode(newData, encoding));
|
||||
}
|
||||
|
||||
//Корректируем пробелы, всякие файлы попадаются :(
|
||||
if (newData[0] == 32) {
|
||||
newData = Buffer.from(newData.toString().trim());
|
||||
}
|
||||
|
||||
//Окончательно корректируем кодировку
|
||||
return this.checkEncoding(newData);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class ConvertFb3 extends ConvertHtml {
|
||||
.replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<\/subtitle>/g, '</fb2-subtitle>')
|
||||
;
|
||||
return await super.run(Buffer.from(text), {skipCheck: true});
|
||||
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class ConvertHtml extends ConvertBase {
|
||||
const {dataType} = opts;
|
||||
|
||||
//html?
|
||||
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
||||
if ( ( (dataType && (dataType.ext == 'html' || dataType.ext == 'xml')) ) || this.isDataXml(data) )
|
||||
return {isText: false};
|
||||
|
||||
//может это чистый текст?
|
||||
@@ -16,7 +16,7 @@ class ConvertHtml extends ConvertBase {
|
||||
}
|
||||
|
||||
//из буфера обмена?
|
||||
if (data.toString().indexOf('<buffer>') == 0) {
|
||||
if (data.slice(0, 50).toString().indexOf('<buffer>') == 0) {
|
||||
return {isText: false};
|
||||
}
|
||||
|
||||
@@ -24,15 +24,13 @@ class ConvertHtml extends ConvertBase {
|
||||
}
|
||||
|
||||
async run(data, opts) {
|
||||
let isText = false;
|
||||
if (!opts.skipCheck) {
|
||||
let {isText = false, uploadFileName = ''} = opts;
|
||||
if (!opts.skipHtmlCheck) {
|
||||
const checkResult = this.check(data, opts);
|
||||
if (!checkResult)
|
||||
return false;
|
||||
|
||||
isText = checkResult.isText;
|
||||
} else {
|
||||
isText = opts.isText;
|
||||
}
|
||||
|
||||
let titleInfo = {};
|
||||
@@ -242,6 +240,9 @@ class ConvertHtml extends ConvertBase {
|
||||
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
|
||||
});
|
||||
|
||||
if (!title)
|
||||
title = uploadFileName;
|
||||
|
||||
titleInfo['book-title'] = title;
|
||||
if (author)
|
||||
titleInfo.author = {'last-name': author};
|
||||
|
||||
@@ -27,7 +27,7 @@ class ConvertJpegPng extends ConvertBase {
|
||||
} else {
|
||||
const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`;
|
||||
await fs.copy(inputFiles.sourceFile, imageFile);
|
||||
files.push(imageFile);
|
||||
files.push({src: imageFile});
|
||||
}
|
||||
|
||||
//читаем изображения
|
||||
@@ -55,10 +55,9 @@ class ConvertJpegPng extends ConvertBase {
|
||||
|
||||
let images = [];
|
||||
let loading = [];
|
||||
files.forEach(f => {
|
||||
const image = {src: f};
|
||||
images.push(image);
|
||||
loading.push(loadImage(image));
|
||||
files.forEach(img => {
|
||||
images.push(img);
|
||||
loading.push(loadImage(img));
|
||||
});
|
||||
|
||||
await Promise.all(loading);
|
||||
@@ -82,8 +81,14 @@ class ConvertJpegPng extends ConvertBase {
|
||||
const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': image.type}, _t: image.data};
|
||||
binary.push(img);
|
||||
|
||||
const attrs = {'l:href': `#${image.name}`};
|
||||
if (image.alt) {
|
||||
image.alt = (image.alt.length > 256 ? image.alt.substring(0, 256) : image.alt);
|
||||
attrs.alt = image.alt;
|
||||
}
|
||||
|
||||
pars.push({_n: 'p', _t: ''});
|
||||
pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
|
||||
pars.push({_n: 'image', _attrs: attrs});
|
||||
}
|
||||
}
|
||||
pars.push({_n: 'p', _t: ''});
|
||||
|
||||
@@ -15,7 +15,7 @@ class ConvertPdf extends ConvertHtml {
|
||||
}
|
||||
|
||||
async run(notUsed, opts) {
|
||||
if (!this.check(notUsed, opts))
|
||||
if (!opts.pdfAsText || !this.check(notUsed, opts))
|
||||
return false;
|
||||
|
||||
await this.checkExternalConverterPresent();
|
||||
@@ -27,7 +27,6 @@ class ConvertPdf extends ConvertHtml {
|
||||
const outFile = `${outBasename}.xml`;
|
||||
|
||||
const pdftohtmlPath = '/usr/bin/pdftohtml';
|
||||
|
||||
if (!await fs.pathExists(pdftohtmlPath))
|
||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||
|
||||
@@ -342,7 +341,7 @@ class ConvertPdf extends ConvertHtml {
|
||||
|
||||
//console.log(text);
|
||||
await utils.sleep(100);
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
|
||||
return await super.run(Buffer.from(text), {skipHtmlCheck: true, isText: true});
|
||||
}
|
||||
|
||||
async getPdfTitleAndAuthor(pdfFile) {
|
||||
|
||||
115
server/core/Reader/BookConverter/ConvertPdfImages.js
Normal file
115
server/core/Reader/BookConverter/ConvertPdfImages.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const sax = require('../../sax');
|
||||
|
||||
const ConvertJpegPng = require('./ConvertJpegPng');
|
||||
|
||||
class ConvertPdfImages extends ConvertJpegPng {
|
||||
check(data, opts) {
|
||||
const {inputFiles} = opts;
|
||||
|
||||
return this.config.useExternalBookConverter &&
|
||||
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'pdf';
|
||||
}
|
||||
|
||||
async run(data, opts) {
|
||||
if (!this.check(data, opts))
|
||||
return false;
|
||||
|
||||
let {inputFiles, callback, abort, pdfQuality} = opts;
|
||||
|
||||
pdfQuality = (pdfQuality && pdfQuality <= 100 && pdfQuality >= 10 ? pdfQuality : 20);
|
||||
|
||||
const pdftoppmPath = '/usr/bin/pdftoppm';
|
||||
if (!await fs.pathExists(pdftoppmPath))
|
||||
throw new Error('Внешний конвертер pdftoppm не найден');
|
||||
|
||||
const pdftohtmlPath = '/usr/bin/pdftohtml';
|
||||
if (!await fs.pathExists(pdftohtmlPath))
|
||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||
|
||||
const inpFile = inputFiles.sourceFile;
|
||||
const dir = `${inputFiles.filesDir}/`;
|
||||
const outBasename = `${dir}${utils.randomHexString(10)}`;
|
||||
const outFile = `${outBasename}.tmp`;
|
||||
|
||||
//конвертируем в jpeg
|
||||
let perc = 0;
|
||||
await this.execConverter(pdftoppmPath, ['-jpeg', '-jpegopt', `quality=${pdfQuality},progressive=y`, inpFile, outFile], () => {
|
||||
perc = (perc < 100 ? perc + 1 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
|
||||
const limitSize = 2*this.config.maxUploadFileSize;
|
||||
let jpgFilesSize = 0;
|
||||
|
||||
//ищем изображения
|
||||
let files = [];
|
||||
await utils.findFiles(async(file) => {
|
||||
if (path.extname(file) == '.jpg') {
|
||||
jpgFilesSize += (await fs.stat(file)).size;
|
||||
if (jpgFilesSize > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
|
||||
}
|
||||
|
||||
files.push({name: file, base: path.basename(file)});
|
||||
}
|
||||
}, dir);
|
||||
|
||||
files.sort((a, b) => a.base.localeCompare(b.base));
|
||||
|
||||
//схема документа (outline)
|
||||
const outXml = `${outBasename}.xml`;
|
||||
await this.execConverter(pdftohtmlPath, ['-nodrm', '-i', '-c', '-s', '-xml', inpFile, outXml], null, abort);
|
||||
const outline = [];
|
||||
|
||||
let inOutline = 0;
|
||||
let inItem = false;
|
||||
let pageNum = 0;
|
||||
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (inOutline > 0 && inItem && pageNum) {
|
||||
outline[pageNum] = text;
|
||||
}
|
||||
};
|
||||
|
||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (tag == 'outline')
|
||||
inOutline++;
|
||||
|
||||
if (inOutline > 0 && tag == 'item') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
pageNum = (attrs.page && attrs.page.value ? attrs.page.value : 0);
|
||||
inItem = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (tag == 'outline')
|
||||
inOutline--;
|
||||
if (tag == 'item')
|
||||
inItem = false;
|
||||
};
|
||||
|
||||
const dataXml = await fs.readFile(outXml);
|
||||
const buf = this.decode(dataXml).toString();
|
||||
sax.parseSync(buf, {
|
||||
onStartNode, onEndNode, onTextNode
|
||||
});
|
||||
|
||||
|
||||
await utils.sleep(100);
|
||||
//формируем список файлов
|
||||
let i = 0;
|
||||
const imageFiles = files.map(f => {
|
||||
i++;
|
||||
let alt = (outline[i] ? outline[i] : '');
|
||||
return {src: f.name, alt};
|
||||
});
|
||||
return await super.run(data, Object.assign({}, opts, {imageFiles}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConvertPdfImages;
|
||||
@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
|
||||
if (text === false)
|
||||
return false;
|
||||
|
||||
return await super.run(Buffer.from(text), {skipCheck: true});
|
||||
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
|
||||
}
|
||||
|
||||
getTitle(text) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const convertClassFactory = [
|
||||
require('./ConvertEpub'),
|
||||
require('./ConvertDjvu'),
|
||||
require('./ConvertPdf'),
|
||||
require('./ConvertPdfImages'),
|
||||
require('./ConvertRtf'),
|
||||
require('./ConvertDocX'),
|
||||
require('./ConvertFb3'),
|
||||
|
||||
Reference in New Issue
Block a user