Compare commits
219 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 | ||
|
|
6074c4b7bd | ||
|
|
9906dd43c7 | ||
|
|
17699f66f8 | ||
|
|
80a29e654d | ||
|
|
4184fda247 | ||
|
|
7460ff7055 | ||
|
|
3137b86cee | ||
|
|
b2ca84bb7e | ||
|
|
7d692dd730 | ||
|
|
8850a89aa7 | ||
|
|
57b01dd204 | ||
|
|
8aa1da36b6 | ||
|
|
2dbe29d632 | ||
|
|
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 | ||
|
|
d8e765a04f | ||
|
|
40ff572f94 | ||
|
|
cc4275dc03 | ||
|
|
b387f4a0db | ||
|
|
1a096031c4 | ||
|
|
83a60b4091 | ||
|
|
b292407ec2 | ||
|
|
952c337b76 | ||
|
|
e947b887fe | ||
|
|
bd1e5485d7 | ||
|
|
e095c3318b | ||
|
|
d75a08b519 | ||
|
|
d55a616fe0 | ||
|
|
2146cb3576 | ||
|
|
ae260e74f6 | ||
|
|
355410c03c | ||
|
|
718ad51fac | ||
|
|
4242a8679f | ||
|
|
4ff9ff699b | ||
|
|
7a76673274 | ||
|
|
bd03ca5136 | ||
|
|
b3e1e4b909 | ||
|
|
4bb22183df | ||
|
|
e72f8f4245 | ||
|
|
5b52f48bce | ||
|
|
f07a157a2a | ||
|
|
ca40854106 | ||
|
|
d6a8209b31 | ||
|
|
731e1f1f15 | ||
|
|
b4a2a8fb98 | ||
|
|
5a3e4ee5ca | ||
|
|
ab2cf0aeec | ||
|
|
9de6a02b30 | ||
|
|
9fb7892bfe | ||
|
|
546f4556f6 | ||
|
|
471de104bc | ||
|
|
d30be1536d | ||
|
|
6c0678ed61 | ||
|
|
4883b8a190 | ||
|
|
14742ed4ad | ||
|
|
1d8bd56862 | ||
|
|
94b8f9fe1c | ||
|
|
27412211a5 | ||
|
|
f8c4960079 | ||
|
|
b2e0bcf995 | ||
|
|
fcf6639d38 | ||
|
|
d540cb91a9 | ||
|
|
f69cc6f1b1 | ||
|
|
607f2ff407 | ||
|
|
ba6bf8c091 | ||
|
|
7e4c938dfd | ||
|
|
7f36d55320 | ||
|
|
d9634a134c | ||
|
|
4f8868d4b1 | ||
|
|
956546585c | ||
|
|
3ca0a92442 | ||
|
|
213f7e48c9 | ||
|
|
8b66fd522d | ||
|
|
fdf5009999 | ||
|
|
bbdba0ef16 | ||
|
|
a63602df7a | ||
|
|
587120f984 |
@@ -66,7 +66,7 @@ class Reader {
|
|||||||
await utils.sleep(refreshPause);
|
await utils.sleep(refreshPause);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||||
throw new Error('Слишком долгое время ожидания');
|
throw new Error('Слишком долгое время ожидания');
|
||||||
}
|
}
|
||||||
//проверка воркера
|
//проверка воркера
|
||||||
@@ -181,9 +181,8 @@ class Reader {
|
|||||||
maxUploadFileSize = 10*1024*1024;
|
maxUploadFileSize = 10*1024*1024;
|
||||||
if (file.size > maxUploadFileSize)
|
if (file.size > maxUploadFileSize)
|
||||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file, file.name);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="fit row">
|
<div class="fit row">
|
||||||
<Notify ref="notify"/>
|
<Notify ref="notify"/>
|
||||||
<StdDialog ref="stdDialog"/>
|
<StdDialog ref="stdDialog"/>
|
||||||
<keep-alive>
|
<keep-alive v-if="showPage">
|
||||||
<router-view class="col"></router-view>
|
<router-view class="col"></router-view>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,8 +12,11 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
import Notify from './share/Notify.vue';
|
import Notify from './share/Notify.vue';
|
||||||
import StdDialog from './share/StdDialog.vue';
|
import StdDialog from './share/StdDialog.vue';
|
||||||
|
|
||||||
|
import miscApi from '../api/misc';
|
||||||
import * as utils from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
@@ -30,6 +33,8 @@ export default @Component({
|
|||||||
|
|
||||||
})
|
})
|
||||||
class App extends Vue {
|
class App extends Vue {
|
||||||
|
showPage = false;
|
||||||
|
|
||||||
itemRuText = {
|
itemRuText = {
|
||||||
'/cardindex': 'Картотека',
|
'/cardindex': 'Картотека',
|
||||||
'/reader': 'Читалка',
|
'/reader': 'Читалка',
|
||||||
@@ -42,7 +47,6 @@ class App extends Vue {
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
|
||||||
this.state = this.$store.state;
|
this.state = this.$store.state;
|
||||||
this.uistate = this.$store.state.uistate;
|
this.uistate = this.$store.state.uistate;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
@@ -116,18 +120,28 @@ class App extends Vue {
|
|||||||
this.$root.notify = this.$refs.notify;
|
this.$root.notify = this.$refs.notify;
|
||||||
this.$root.stdDialog = this.$refs.stdDialog;
|
this.$root.stdDialog = this.$refs.stdDialog;
|
||||||
|
|
||||||
this.dispatch('config/loadConfig');
|
|
||||||
this.$watch('apiError', function(newError) {
|
|
||||||
if (newError) {
|
|
||||||
let mes = newError.message;
|
|
||||||
if (newError.response && newError.response.config)
|
|
||||||
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
|
||||||
this.$root.notify.error(mes, 'Ошибка API');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setAppTitle();
|
this.setAppTitle();
|
||||||
(async() => {
|
(async() => {
|
||||||
|
//загрузим конфиг сревера
|
||||||
|
try {
|
||||||
|
const config = await miscApi.loadConfig();
|
||||||
|
this.commit('config/setConfig', config);
|
||||||
|
this.showPage = true;
|
||||||
|
} catch(e) {
|
||||||
|
//проверим, не получен ли конфиг ранее
|
||||||
|
if (!this.mode) {
|
||||||
|
this.$root.notify.error(e.message, 'Ошибка API');
|
||||||
|
} else {
|
||||||
|
//вероятно, работаем в оффлайне
|
||||||
|
this.showPage = true;
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//запросим persistent storage
|
||||||
|
if (navigator.storage && navigator.storage.persist) {
|
||||||
|
navigator.storage.persist();
|
||||||
|
}
|
||||||
await this.routerReady();
|
await this.routerReady();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
})();
|
})();
|
||||||
@@ -248,7 +262,7 @@ body, html, #app {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dborder {
|
.dborder {
|
||||||
border: 2px solid yellow !important;
|
border: 2px solid magenta !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-rotate {
|
.icon-rotate {
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
<template>
|
||||||
|
<Window ref="window" width="600px" height="95%" @close="close">
|
||||||
|
<template slot="header">
|
||||||
|
Настроить закладки
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="col column fit">
|
||||||
|
<div class="row items-center top-panel bg-grey-3">
|
||||||
|
<q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search">
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col row">
|
||||||
|
<div class="left-panel column items-center no-wrap bg-grey-3">
|
||||||
|
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<div class="space"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col fit tree">
|
||||||
|
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||||
|
<q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" />
|
||||||
|
</div>
|
||||||
|
<q-tree
|
||||||
|
class="q-my-xs"
|
||||||
|
:nodes="nodes"
|
||||||
|
node-key="key"
|
||||||
|
tick-strategy="leaf"
|
||||||
|
:selected.sync="selected"
|
||||||
|
:ticked.sync="ticked"
|
||||||
|
:expanded.sync="expanded"
|
||||||
|
selected-color="black"
|
||||||
|
:filter="search"
|
||||||
|
no-nodes-label="Закладок пока нет"
|
||||||
|
no-results-label="Ничего не найдено"
|
||||||
|
>
|
||||||
|
<template v-slot:default-header="p">
|
||||||
|
<div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div>
|
||||||
|
</template>
|
||||||
|
</q-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import Window from '../../share/Window.vue';
|
||||||
|
import * as lu from '../linkUtils';
|
||||||
|
import rstore from '../../../store/modules/reader';
|
||||||
|
|
||||||
|
const BookmarkSettingsProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
libs: Object,
|
||||||
|
addBookmarkVisible: Boolean,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
ticked: function() {
|
||||||
|
this.checkAllTicked();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
class BookmarkSettings extends BookmarkSettingsProps {
|
||||||
|
search = '';
|
||||||
|
selected = '';
|
||||||
|
ticked = [];
|
||||||
|
expanded = [];
|
||||||
|
tickAll = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.afterInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.$refs.window.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodes() {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
const expanded = [];
|
||||||
|
this.links = {};
|
||||||
|
this.libs.groups.forEach(group => {
|
||||||
|
const rkey = `r-${group.r}`;
|
||||||
|
const g = {label: group.r, key: rkey, children: []};
|
||||||
|
this.links[rkey] = {l: group.r, c: ''};
|
||||||
|
|
||||||
|
group.list.forEach(link => {
|
||||||
|
const key = link.l;
|
||||||
|
g.children.push({
|
||||||
|
label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l),
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
this.links[key] = link;
|
||||||
|
if (link.l == this.libs.startLink && expanded.indexOf(rkey) < 0) {
|
||||||
|
expanded.push(rkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push(g);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.afterInit) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.expanded = expanded;
|
||||||
|
});
|
||||||
|
this.afterInit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeTickAll() {
|
||||||
|
if (this.tickAll) {
|
||||||
|
const newTicked = [];
|
||||||
|
for (const key of Object.keys(this.links)) {
|
||||||
|
if (key.indexOf('r-') != 0)
|
||||||
|
newTicked.push(key);
|
||||||
|
}
|
||||||
|
this.ticked = newTicked;
|
||||||
|
} else {
|
||||||
|
this.ticked = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAllTicked() {
|
||||||
|
const ticked = new Set(this.ticked);
|
||||||
|
|
||||||
|
let newTickAll = !!(this.nodes.length);
|
||||||
|
for (const key of Object.keys(this.links)) {
|
||||||
|
if (key.indexOf('r-') != 0 && !ticked.has(key))
|
||||||
|
newTickAll = false;
|
||||||
|
}
|
||||||
|
this.tickAll = newTickAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSearch() {
|
||||||
|
this.search = '';
|
||||||
|
this.$refs.search.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
openSelected() {
|
||||||
|
if (!this.selected)
|
||||||
|
return;
|
||||||
|
if (this.selected.indexOf('r-') === 0) {//rootLink
|
||||||
|
this.$emit('do-action', {action: 'setRootLink', data: this.links[this.selected].l});
|
||||||
|
} else {//selectedLink
|
||||||
|
this.$emit('do-action', {action: 'setSelectedLink', data: this.links[this.selected].l});
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
editBookmark() {
|
||||||
|
this.$emit('do-action', {action: 'editBookmark', data: {link: this.links[this.selected].l, desc: this.links[this.selected].c}});
|
||||||
|
}
|
||||||
|
|
||||||
|
addBookmark() {
|
||||||
|
this.$emit('do-action', {action: 'addBookmark'});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delBookmark() {
|
||||||
|
const newLibs = _.cloneDeep(this.libs);
|
||||||
|
|
||||||
|
if (await this.$root.stdDialog.confirm(`Подтвердите удаление ${this.ticked.length} закладок:`, ' ')) {
|
||||||
|
const ticked = new Set(this.ticked);
|
||||||
|
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
|
||||||
|
const g = newLibs.groups[i];
|
||||||
|
for (let j = g.list.length - 1; j >= 0; j--) {
|
||||||
|
if (ticked.has(g.list[j].l)) {
|
||||||
|
delete g.list[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.list = g.list.filter(v => v);
|
||||||
|
if (!g.list.length)
|
||||||
|
delete newLibs.groups[i];
|
||||||
|
else {
|
||||||
|
const item = lu.getListItemByLink(g.list, g.s);
|
||||||
|
if (!item)
|
||||||
|
g.s = g.list[0].l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newLibs.groups = newLibs.groups.filter(v => v);
|
||||||
|
this.ticked = [];
|
||||||
|
this.selected = '';
|
||||||
|
this.$emit('do-action', {action: 'setLibs', data: newLibs});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBookmark(down = false) {
|
||||||
|
const newLibs = _.cloneDeep(this.libs);
|
||||||
|
|
||||||
|
const ticked = new Set(this.ticked);
|
||||||
|
let moved = false;
|
||||||
|
let prevFull = false;
|
||||||
|
if (!down) {
|
||||||
|
for (let i = 0; i < newLibs.groups.length; i++) {
|
||||||
|
const g = newLibs.groups[i];
|
||||||
|
let count = 0;
|
||||||
|
for (let j = 0; j < g.list.length; j++) {
|
||||||
|
if (ticked.has(g.list[j].l)) {
|
||||||
|
if (j > 0 && !ticked.has(g.list[j - 1].l)) {
|
||||||
|
[g.list[j], g.list[j - 1]] = [g.list[j - 1], g.list[j]];
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == g.list.length && !prevFull && i > 0) {
|
||||||
|
const gs = newLibs.groups;
|
||||||
|
[gs[i], gs[i - 1]] = [gs[i - 1], gs[i]];
|
||||||
|
moved = true;
|
||||||
|
} else
|
||||||
|
prevFull = (count == g.list.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
|
||||||
|
const g = newLibs.groups[i];
|
||||||
|
let count = 0;
|
||||||
|
for (let j = g.list.length - 1; j >= 0; j--) {
|
||||||
|
if (ticked.has(g.list[j].l)) {
|
||||||
|
if (j < g.list.length - 1 && !ticked.has(g.list[j + 1].l)) {
|
||||||
|
[g.list[j], g.list[j + 1]] = [g.list[j + 1], g.list[j]];
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == g.list.length && !prevFull && i < newLibs.groups.length - 1) {
|
||||||
|
const gs = newLibs.groups;
|
||||||
|
[gs[i], gs[i + 1]] = [gs[i + 1], gs[i]];
|
||||||
|
moved = true;
|
||||||
|
} else
|
||||||
|
prevFull = (count == g.list.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved)
|
||||||
|
this.$emit('do-action', {action: 'setLibs', data: newLibs});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultBookmarks() {
|
||||||
|
const result = await this.$root.stdDialog.prompt(`Введите 'да' для сброса всех закладок в предустановленные значения:`, ' ', {
|
||||||
|
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.value && result.value.toLowerCase() == 'да') {
|
||||||
|
this.$emit('do-action', {action: 'setLibs', data: _.cloneDeep(
|
||||||
|
Object.assign({helpShowed: true}, rstore.libsDefaults)
|
||||||
|
)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.afterInit = false;
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (this.addBookmarkVisible)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (event.type == 'keydown' && event.key == 'Escape') {
|
||||||
|
this.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.top-panel {
|
||||||
|
height: 50px;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
padding: 0 10px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 60px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
padding: 0px 10px 10px 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
text-shadow: 0 0 20px yellow, 0 0 15px yellow, 0 0 10px yellow, 0 0 10px yellow, 0 0 5px yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-tick-all {
|
||||||
|
border-bottom: 1px solid #bbbbbb;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
padding: 5px 5px 2px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
min-height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,6 +9,18 @@
|
|||||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
|
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||||
|
<q-icon name="la la-plus" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||||
|
</span>
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||||
|
<q-icon name="la la-minus" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||||
|
</span>
|
||||||
|
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||||
|
<q-icon name="la la-question-circle" size="16px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-show="ready" class="col column" style="min-width: 600px">
|
<div v-show="ready" class="col column" style="min-width: 600px">
|
||||||
@@ -23,12 +35,12 @@
|
|||||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
|
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px" disabled>
|
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки (пока недоступно)</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:selected>
|
<template v-slot:selected>
|
||||||
<div style="overflow: hidden; white-space: nowrap;">{{ removeProtocol(rootLink) }}</div>
|
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
|
||||||
</template>
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
|
|
||||||
@@ -44,23 +56,30 @@
|
|||||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(libs.startLink)" size="12px">
|
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px">
|
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl">Открыть
|
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<div class="col fit" style="position: relative;">
|
<div class="col fit" ref="frameBox" style="position: relative;">
|
||||||
<iframe v-if="frameVisible" class="fit" ref="frame" :src="frameSrc" frameborder="0"></iframe>
|
<div ref="frameWrap" class="overflow-hidden">
|
||||||
|
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
|
||||||
|
</div>
|
||||||
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
|
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,7 +87,8 @@
|
|||||||
<template slot="header">
|
<template slot="header">
|
||||||
<div class="row items-center">
|
<div class="row items-center">
|
||||||
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
|
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
|
||||||
Добавить закладку
|
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
|
||||||
|
<div v-else>Добавить закладку</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -96,7 +116,30 @@
|
|||||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog ref="options" v-model="optionsVisible">
|
||||||
|
<template slot="header">
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
|
||||||
|
Опции
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="q-mx-md column">
|
||||||
|
<q-checkbox v-model="closeAfterSubmit" size="36px" label="Закрыть окно при отправке ссылки в читалку" />
|
||||||
|
<q-checkbox v-model="openInFrameOnEnter" size="36px" label="Открывать ссылку во фрейме при нажатии 'Enter'" />
|
||||||
|
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template slot="footer">
|
||||||
|
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
|
||||||
|
@do-action="doAction" @close="closeBookmarkSettings">
|
||||||
|
</BookmarkSettings>
|
||||||
</Window>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -108,58 +151,102 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
import Window from '../share/Window.vue';
|
import Window from '../share/Window.vue';
|
||||||
import Dialog from '../share/Dialog.vue';
|
import Dialog from '../share/Dialog.vue';
|
||||||
|
import BookmarkSettings from './BookmarkSettings/BookmarkSettings.vue';
|
||||||
|
|
||||||
import rstore from '../../store/modules/reader';
|
import rstore from '../../store/modules/reader';
|
||||||
import * as utils from '../../share/utils';
|
import * as utils from '../../share/utils';
|
||||||
|
import * as lu from './linkUtils';
|
||||||
|
|
||||||
const proxySubst = {
|
const proxySubst = {
|
||||||
'http://flibusta.is': 'http://b.liberama.top:23480',
|
'http://flibusta.is': 'http://b.liberama.top:23480',
|
||||||
|
'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
components: {
|
components: {
|
||||||
Window,
|
Window,
|
||||||
Dialog
|
Dialog,
|
||||||
|
BookmarkSettings
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
libs: function() {
|
libs: function() {
|
||||||
this.loadLibs();
|
this.loadLibs();
|
||||||
},
|
},
|
||||||
rootLink: function() {
|
|
||||||
this.updateSelectedLink();
|
|
||||||
this.updateStartLink();
|
|
||||||
},
|
|
||||||
selectedLink: function() {
|
|
||||||
this.updateStartLink();
|
|
||||||
},
|
|
||||||
defaultRootLink: function() {
|
defaultRootLink: function() {
|
||||||
this.updateBookmarkLink();
|
this.updateBookmarkLink();
|
||||||
}
|
},
|
||||||
|
bookUrl: function(newValue) {
|
||||||
|
const value = lu.addProtocol(newValue);
|
||||||
|
const subst = this.makeProxySubst(value, true);
|
||||||
|
if (value != subst) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bookUrl = subst;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bookmarkLink: function(newValue) {
|
||||||
|
const value = lu.addProtocol(newValue);
|
||||||
|
const subst = this.makeProxySubst(value, true);
|
||||||
|
if (value != subst) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.bookmarkLink = subst;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeAfterSubmit: function(newValue) {
|
||||||
|
this.commitProp('closeAfterSubmit', newValue);
|
||||||
|
},
|
||||||
|
openInFrameOnEnter: function(newValue) {
|
||||||
|
this.commitProp('openInFrameOnEnter', newValue);
|
||||||
|
},
|
||||||
|
openInFrameOnAdd: function(newValue) {
|
||||||
|
this.commitProp('openInFrameOnAdd', newValue);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
class ExternalLibs extends Vue {
|
class ExternalLibs extends Vue {
|
||||||
ready = false;
|
ready = false;
|
||||||
frameVisible = false;
|
frameVisible = false;
|
||||||
startLink = '';
|
|
||||||
rootLink = '';
|
rootLink = '';
|
||||||
selectedLink = '';
|
selectedLink = '';
|
||||||
frameSrc = '';
|
frameSrc = '';
|
||||||
bookUrl = '';
|
bookUrl = '';
|
||||||
libs = {};
|
libs = {};
|
||||||
fullScreenActive = false;
|
fullScreenActive = false;
|
||||||
addBookmarkVisible = false;
|
|
||||||
transparentLayoutVisible = false;
|
transparentLayoutVisible = false;
|
||||||
|
|
||||||
|
addBookmarkVisible = false;
|
||||||
|
optionsVisible = false;
|
||||||
|
|
||||||
|
addBookmarkMode = '';
|
||||||
bookmarkLink = '';
|
bookmarkLink = '';
|
||||||
bookmarkDesc = '';
|
bookmarkDesc = '';
|
||||||
defaultRootLink = '';
|
defaultRootLink = '';
|
||||||
|
|
||||||
|
bookmarkSettingsActive = false;
|
||||||
|
|
||||||
|
closeAfterSubmit = false;
|
||||||
|
openInFrameOnEnter = false;
|
||||||
|
openInFrameOnAdd = false;
|
||||||
|
frameScale = 1;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
this.oldStartLink = '';
|
||||||
|
this.justOpened = true;
|
||||||
this.$root.addKeyHook(this.keyHook);
|
this.$root.addKeyHook(this.keyHook);
|
||||||
|
|
||||||
|
this.$root.$on('resize', async() => {
|
||||||
|
await utils.sleep(200);
|
||||||
|
this.frameResize();
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.debouncedGoToLink = _.debounce((link) => {
|
||||||
|
this.goToLink(link);
|
||||||
|
}, 100, {'maxWait':200});
|
||||||
//this.commit = this.$store.commit;
|
//this.commit = this.$store.commit;
|
||||||
//this.commit('reader/setLibs', rstore.libsDefaults);
|
//this.commit('reader/setLibs', rstore.libsDefaults);
|
||||||
}
|
}
|
||||||
@@ -252,8 +339,6 @@ class ExternalLibs extends Vue {
|
|||||||
} else if (d.type == 'libs') {
|
} else if (d.type == 'libs') {
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
this.libs = _.cloneDeep(d.data);
|
this.libs = _.cloneDeep(d.data);
|
||||||
if (!this.frameSrc)
|
|
||||||
this.goToLink(this.libs.startLink);
|
|
||||||
} else if (d.type == 'notify') {
|
} else if (d.type == 'notify') {
|
||||||
this.$root.notify.success(d.data, '', {position: 'bottom-right'});
|
this.$root.notify.success(d.data, '', {position: 'bottom-right'});
|
||||||
}
|
}
|
||||||
@@ -278,11 +363,44 @@ class ExternalLibs extends Vue {
|
|||||||
this.sendMessage({type: 'libs', data: libs});
|
this.sendMessage({type: 'libs', data: libs});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitProp(prop, value) {
|
||||||
|
let libs = _.cloneDeep(this.libs);
|
||||||
|
libs[prop] = value;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
loadLibs() {
|
loadLibs() {
|
||||||
const libs = this.libs;
|
const libs = this.libs;
|
||||||
this.startLink = (libs.comment ? libs.comment + ' ': '') + this.removeProtocol(libs.startLink);
|
|
||||||
this.rootLink = this.getOrigin(libs.startLink);
|
if (!libs.helpShowed) {
|
||||||
this.updateSelectedLink();
|
this.showHelp();
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(1000);
|
||||||
|
this.commitProp('helpShowed', true);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedLink = libs.startLink;
|
||||||
|
this.closeAfterSubmit = libs.closeAfterSubmit || false;
|
||||||
|
this.openInFrameOnEnter = libs.openInFrameOnEnter || false;
|
||||||
|
this.openInFrameOnAdd = libs.openInFrameOnAdd || false;
|
||||||
|
|
||||||
|
this.frameScale = 1;
|
||||||
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
|
||||||
|
if (index >= 0)
|
||||||
|
this.frameScale = this.libs.groups[index].frameScale || 1;
|
||||||
|
|
||||||
|
this.updateStartLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
doAction(event) {
|
||||||
|
switch (event.action) {
|
||||||
|
case 'setLibs': this.commitLibs(event.data); break;
|
||||||
|
case 'setRootLink': this.rootLink = event.data; this.rootLinkInput(); break;
|
||||||
|
case 'setSelectedLink': this.selectedLink = event.data; this.selectedLinkInput(); break;
|
||||||
|
case 'editBookmark': this.addBookmark('edit', event.data.link, event.data.desc); break;
|
||||||
|
case 'addBookmark': this.addBookmark('add'); break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get mode() {
|
get mode() {
|
||||||
@@ -290,33 +408,61 @@ class ExternalLibs extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get header() {
|
get header() {
|
||||||
let result = (this.ready ? 'Библиотека' : 'Загрузка...');
|
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
|
||||||
if (this.ready && this.startLink) {
|
if (this.ready && this.selectedLink) {
|
||||||
result += ` | ${this.startLink}`;
|
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||||
}
|
}
|
||||||
this.$root.$emit('set-app-title', result);
|
this.$root.$emit('set-app-title', result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedLink() {
|
get rootLinkWithoutProtocol() {
|
||||||
if (!this.ready)
|
return lu.removeProtocol(this.rootLink);
|
||||||
return;
|
|
||||||
const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
|
|
||||||
if (index >= 0)
|
|
||||||
this.selectedLink = this.libs.groups[index].s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStartLink() {
|
updateSelectedLinkByRoot() {
|
||||||
if (!this.ready)
|
if (!this.ready)
|
||||||
return;
|
return;
|
||||||
const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
|
|
||||||
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||||
|
if (index >= 0)
|
||||||
|
this.selectedLink = this.libs.groups[index].s;
|
||||||
|
else
|
||||||
|
this.selectedLink = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStartLink(force) {
|
||||||
|
if (!this.ready)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
try {
|
||||||
|
this.rootLink = lu.getOrigin(this.selectedLink);
|
||||||
|
index = lu.getRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||||
|
} catch(e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
let libs = _.cloneDeep(this.libs);
|
let libs = _.cloneDeep(this.libs);
|
||||||
libs.groups[index].s = this.selectedLink;
|
const com = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
|
||||||
libs.startLink = this.selectedLink;
|
if (libs.groups[index].s != this.selectedLink ||
|
||||||
libs.comment = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
|
libs.startLink != this.selectedLink ||
|
||||||
this.goToLink(this.selectedLink);
|
libs.comment != com) {
|
||||||
this.commitLibs(libs);
|
libs.groups[index].s = this.selectedLink;
|
||||||
|
libs.startLink = this.selectedLink;
|
||||||
|
libs.comment = com;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force || this.oldStartLink != libs.startLink) {
|
||||||
|
this.oldStartLink = libs.startLink;
|
||||||
|
this.debouncedGoToLink(this.selectedLink);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.rootLink = '';
|
||||||
|
this.selectedLink = '';
|
||||||
|
this.debouncedGoToLink(this.selectedLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +472,7 @@ class ExternalLibs extends Vue {
|
|||||||
return result;
|
return result;
|
||||||
|
|
||||||
this.libs.groups.forEach(group => {
|
this.libs.groups.forEach(group => {
|
||||||
result.push({label: this.removeProtocol(group.r), value: group.r});
|
result.push({label: lu.removeProtocol(group.r), value: group.r});
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -336,7 +482,7 @@ class ExternalLibs extends Vue {
|
|||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
rstore.libsDefaults.groups.forEach(group => {
|
rstore.libsDefaults.groups.forEach(group => {
|
||||||
result.push({label: this.removeProtocol(group.r), value: group.r});
|
result.push({label: lu.removeProtocol(group.r), value: group.r});
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -347,10 +493,10 @@ class ExternalLibs extends Vue {
|
|||||||
if (!this.ready)
|
if (!this.ready)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.libs.groups[index].list.forEach(link => {
|
this.libs.groups[index].list.forEach(link => {
|
||||||
result.push({label: (link.c ? link.c + ' ': '') + this.removeOrigin(link.l), value: link.l});
|
result.push({label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l), value: link.l});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,66 +505,69 @@ class ExternalLibs extends Vue {
|
|||||||
|
|
||||||
openBookUrlInFrame() {
|
openBookUrlInFrame() {
|
||||||
if (this.bookUrl) {
|
if (this.bookUrl) {
|
||||||
this.goToLink(this.addProtocol(this.bookUrl));
|
this.goToLink(lu.addProtocol(this.bookUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
goToLink(link) {
|
goToLink(link) {
|
||||||
if (!this.ready)
|
if (!this.ready || !link)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
this.frameVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.frameSrc = this.makeProxySubst(link);
|
this.frameSrc = this.makeProxySubst(link);
|
||||||
|
|
||||||
this.frameVisible = false;
|
this.frameVisible = false;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.frameVisible = true;
|
this.frameVisible = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.frame.contentWindow.focus();
|
if (this.$refs.frame) {
|
||||||
|
this.$refs.frame.contentWindow.focus();
|
||||||
|
this.frameResize();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addProtocol(url) {
|
frameResize() {
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0))
|
this.$refs.frameWrap.style = 'width: 1px; height: 1px;';
|
||||||
return 'http://' + url;
|
this.$nextTick(() => {
|
||||||
return url;
|
if (this.$refs.frame) {
|
||||||
|
const w = this.$refs.frameBox.offsetWidth;
|
||||||
|
const h = this.$refs.frameBox.offsetHeight;
|
||||||
|
const normalSize = `width: ${w}px; height: ${h}px;`;
|
||||||
|
this.$refs.frameWrap.style = normalSize;
|
||||||
|
if (this.frameScale != 1) {
|
||||||
|
const s = this.frameScale;
|
||||||
|
this.$refs.frame.style = `width: ${w/s}px; height: ${h/s}px; transform: scale(${s}); transform-origin: 0 0;`;
|
||||||
|
} else {
|
||||||
|
this.$refs.frame.style = normalSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeProtocol(url) {
|
changeScale(delta) {
|
||||||
return url.replace(/(^\w+:|^)\/\//, '');
|
if ((this.frameScale > 0.1 && delta <= 0) || (this.frameScale < 5 && delta >= 0)) {
|
||||||
}
|
this.frameScale = _.round(this.frameScale + delta, 1);
|
||||||
|
|
||||||
getOrigin(url) {
|
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
|
||||||
const parsed = new URL(url);
|
if (index >= 0) {
|
||||||
return parsed.origin;
|
let libs = _.cloneDeep(this.libs);
|
||||||
}
|
libs.groups[index].frameScale = this.frameScale;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
}
|
||||||
|
|
||||||
removeOrigin(url) {
|
this.frameResize();
|
||||||
const parsed = new URL(url);
|
this.$root.notify.success(`Масштаб изменен: ${(this.frameScale*100).toFixed(0)}%`, '', {position: 'bottom-right'});
|
||||||
const result = url.substring(parsed.origin.length);
|
|
||||||
return (result ? result : '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
getRootIndexByUrl(groups, url) {
|
|
||||||
if (!this.ready)
|
|
||||||
return -1;
|
|
||||||
const origin = this.getOrigin(url);
|
|
||||||
for (let i = 0; i < groups.length; i++) {
|
|
||||||
if (groups[i].r == origin)
|
|
||||||
return i;
|
|
||||||
}
|
}
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
getListItemByLink(list, link) {
|
|
||||||
for (const item of list) {
|
|
||||||
if (item.l == link)
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommentByLink(list, link) {
|
getCommentByLink(list, link) {
|
||||||
const item = this.getListItemByLink(list, link);
|
const item = lu.getListItemByLink(list, link);
|
||||||
return (item ? item.c : '');
|
return (item ? item.c : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +575,7 @@ class ExternalLibs extends Vue {
|
|||||||
for (const [key, value] of Object.entries(proxySubst)) {
|
for (const [key, value] of Object.entries(proxySubst)) {
|
||||||
if (reverse && value == url.substring(0, value.length)) {
|
if (reverse && value == url.substring(0, value.length)) {
|
||||||
return key + url.substring(value.length);
|
return key + url.substring(value.length);
|
||||||
} else if (key == url.substring(0, key.length)) {
|
} else if (!reverse && key == url.substring(0, key.length)) {
|
||||||
return value + url.substring(key.length);
|
return value + url.substring(key.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,29 +589,37 @@ class ExternalLibs extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rootLinkInput() {
|
rootLinkInput() {
|
||||||
this.updateSelectedLink();
|
this.updateSelectedLinkByRoot();
|
||||||
this.updateStartLink();
|
this.updateStartLink(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedLinkInput() {
|
selectedLinkInput() {
|
||||||
this.updateStartLink();
|
this.updateStartLink(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
submitUrl() {
|
submitUrl() {
|
||||||
if (this.bookUrl) {
|
if (this.bookUrl) {
|
||||||
this.sendMessage({type: 'submitUrl', data: {
|
this.sendMessage({type: 'submitUrl', data: {
|
||||||
url: this.makeProxySubst(this.addProtocol(this.bookUrl), true),
|
url: this.bookUrl,
|
||||||
force: true
|
force: true
|
||||||
}});
|
}});
|
||||||
this.bookUrl = '';
|
this.bookUrl = '';
|
||||||
if (this.libs.closeAfterSubmit)
|
if (this.closeAfterSubmit)
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addBookmark() {
|
addBookmark(mode = 'add', link = '', desc = '') {
|
||||||
this.bookmarkLink = (this.bookUrl ? this.makeProxySubst(this.addProtocol(this.bookUrl), true) : '');
|
|
||||||
this.bookmarkDesc = '';
|
if (mode == 'edit') {
|
||||||
|
this.editBookmarkLink = this.bookmarkLink = link;
|
||||||
|
this.editBookmarkDesc = this.bookmarkDesc = desc;
|
||||||
|
} else {
|
||||||
|
this.bookmarkLink = this.bookUrl;
|
||||||
|
this.bookmarkDesc = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addBookmarkMode = mode;
|
||||||
this.addBookmarkVisible = true;
|
this.addBookmarkVisible = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.bookmarkLink.focus();
|
this.$refs.bookmarkLink.focus();
|
||||||
@@ -471,7 +628,7 @@ class ExternalLibs extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateBookmarkLink() {
|
updateBookmarkLink() {
|
||||||
const index = this.getRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
|
const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.bookmarkLink = rstore.libsDefaults.groups[index].s;
|
this.bookmarkLink = rstore.libsDefaults.groups[index].s;
|
||||||
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
|
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
|
||||||
@@ -494,8 +651,9 @@ class ExternalLibs extends Vue {
|
|||||||
|
|
||||||
bookmarkDescKeyDown(event) {
|
bookmarkDescKeyDown(event) {
|
||||||
if (event.key == 'Enter') {
|
if (event.key == 'Enter') {
|
||||||
this.okAddBookmark();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
this.okAddBookmark();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,55 +661,82 @@ class ExternalLibs extends Vue {
|
|||||||
if (!this.bookmarkLink)
|
if (!this.bookmarkLink)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const link = this.addProtocol(this.bookmarkLink);
|
const link = (this.addBookmarkMode == 'edit' ? lu.addProtocol(this.editBookmarkLink) : lu.addProtocol(this.bookmarkLink));
|
||||||
let index = -1;
|
let index = -1;
|
||||||
try {
|
try {
|
||||||
index = this.getRootIndexByUrl(this.libs.groups, link);
|
index = lu.getRootIndexByUrl(this.libs.groups, link);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
|
await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let libs = _.cloneDeep(this.libs);
|
||||||
|
|
||||||
|
//добавление
|
||||||
//есть группа в закладках
|
//есть группа в закладках
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const item = this.getListItemByLink(this.libs.groups[index].list, link);
|
const item = lu.getListItemByLink(libs.groups[index].list, link);
|
||||||
|
|
||||||
if (!item || item.c != this.bookmarkDesc) {
|
|
||||||
//добавляем
|
|
||||||
let libs = _.cloneDeep(this.libs);
|
|
||||||
|
|
||||||
|
//редактирование
|
||||||
|
if (item && this.addBookmarkMode == 'edit') {
|
||||||
|
if (item) {
|
||||||
|
//редактируем
|
||||||
|
item.l = link;
|
||||||
|
item.c = this.bookmarkDesc;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
} else {
|
||||||
|
await this.$root.stdDialog.alert('Не удалось отредактировать закладку', 'Ошибка');
|
||||||
|
}
|
||||||
|
} else if (!item) {
|
||||||
|
//добавляем
|
||||||
if (libs.groups[index].list.length >= 100) {
|
if (libs.groups[index].list.length >= 100) {
|
||||||
await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
|
await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
||||||
|
|
||||||
|
if (this.openInFrameOnAdd) {
|
||||||
|
libs.startLink = link;
|
||||||
|
libs.comment = this.bookmarkDesc;
|
||||||
|
}
|
||||||
|
|
||||||
this.commitLibs(libs);
|
this.commitLibs(libs);
|
||||||
|
} else if (item.c != this.bookmarkDesc) {
|
||||||
|
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
|
||||||
|
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
|
||||||
|
item.c = this.bookmarkDesc;
|
||||||
|
this.commitLibs(libs);
|
||||||
|
} else
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await this.$root.stdDialog.alert('Такая закладка уже существует', ' ');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {//нет группы в закладках
|
} else {//нет группы в закладках
|
||||||
let libs = _.cloneDeep(this.libs);
|
|
||||||
|
|
||||||
if (libs.groups.length >= 100) {
|
if (libs.groups.length >= 100) {
|
||||||
await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
|
await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//добавляем сначала группу
|
//добавляем сначала группу
|
||||||
libs.groups.push({r: this.getOrigin(link), s: link, list: []});
|
libs.groups.push({r: lu.getOrigin(link), s: link, list: []});
|
||||||
|
|
||||||
index = this.getRootIndexByUrl(libs.groups, link);
|
index = lu.getSafeRootIndexByUrl(libs.groups, link);
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
|
||||||
|
|
||||||
|
if (this.openInFrameOnAdd) {
|
||||||
|
libs.startLink = link;
|
||||||
|
libs.comment = this.bookmarkDesc;
|
||||||
|
}
|
||||||
|
|
||||||
this.commitLibs(libs);
|
this.commitLibs(libs);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addBookmarkVisible = false;
|
this.addBookmarkVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkSettings() {
|
|
||||||
}
|
|
||||||
|
|
||||||
fullScreenToggle() {
|
fullScreenToggle() {
|
||||||
this.fullScreenActive = !this.fullScreenActive;
|
this.fullScreenActive = !this.fullScreenActive;
|
||||||
if (this.fullScreenActive) {
|
if (this.fullScreenActive) {
|
||||||
@@ -579,19 +764,65 @@ class ExternalLibs extends Vue {
|
|||||||
|
|
||||||
bookUrlKeyDown(event) {
|
bookUrlKeyDown(event) {
|
||||||
if (event.key == 'Enter') {
|
if (event.key == 'Enter') {
|
||||||
this.submitUrl();
|
if (!this.openInFrameOnEnter) {
|
||||||
|
this.submitUrl();
|
||||||
|
} else {
|
||||||
|
if (this.bookUrl)
|
||||||
|
this.goToLink(this.bookUrl);
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook() {
|
bookmarkSettings() {
|
||||||
|
this.bookmarkSettingsActive = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.bookmarkSettings.init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBookmarkSettings() {
|
||||||
|
this.bookmarkSettingsActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showHelp() {
|
||||||
|
this.$root.stdDialog.alert(`
|
||||||
|
<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>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
|
||||||
|
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Приятного пользования ;-)
|
||||||
|
</p>
|
||||||
|
`, 'Справка', {iconName: 'la la-info-circle'});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
if (this.$root.rootRoute() == '/external-libs') {
|
if (this.$root.rootRoute() == '/external-libs') {
|
||||||
if (this.$refs.dialogAddBookmark.active)
|
if (this.$root.stdDialog.active)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (this.bookmarkSettingsActive && this.$refs.bookmarkSettings.keyHook(event))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (this.addBookmarkVisible || this.optionsVisible)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (event.type == 'keydown' && event.key == 'F4') {
|
if (event.type == 'keydown' && event.key == 'F4') {
|
||||||
this.addBookmark()
|
this.addBookmark();
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == 'keydown' && event.key == 'Escape' &&
|
if (event.type == 'keydown' && event.key == 'Escape' &&
|
||||||
@@ -599,12 +830,11 @@ class ExternalLibs extends Vue {
|
|||||||
(document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu)
|
(document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu)
|
||||||
) {
|
) {
|
||||||
this.close();
|
this.close();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
48
client/components/ExternalLibs/linkUtils.js
Normal file
48
client/components/ExternalLibs/linkUtils.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export function addProtocol(url) {
|
||||||
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0))
|
||||||
|
return 'http://' + url;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeProtocol(url) {
|
||||||
|
return url.replace(/(^\w+:|^)\/\//, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrigin(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeOrigin(url) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const result = url.substring(parsed.origin.length);
|
||||||
|
return (result ? result : '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRootIndexByUrl(groups, url) {
|
||||||
|
const origin = getOrigin(url);
|
||||||
|
for (let i = 0; i < groups.length; i++) {
|
||||||
|
if (groups[i].r == origin)
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSafeRootIndexByUrl(groups, url) {
|
||||||
|
let index = -1;
|
||||||
|
try {
|
||||||
|
index = getRootIndexByUrl(groups, url);
|
||||||
|
} catch(e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getListItemByLink(list, link) {
|
||||||
|
for (const item of list) {
|
||||||
|
if (item.l == link)
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
436
client/components/Reader/ContentsPage/ContentsPage.vue
Normal file
436
client/components/Reader/ContentsPage/ContentsPage.vue
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
<template>
|
||||||
|
<Window width="600px" ref="window" @close="close">
|
||||||
|
<template slot="header">
|
||||||
|
Оглавление/закладки
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="bg-grey-3 row">
|
||||||
|
<q-tabs
|
||||||
|
v-model="selectedTab"
|
||||||
|
active-color="black"
|
||||||
|
active-bg-color="white"
|
||||||
|
indicator-color="white"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
inline-label
|
||||||
|
class="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>
|
||||||
|
|
||||||
|
<div class="q-mb-sm"/>
|
||||||
|
|
||||||
|
<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 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
||||||
|
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
|
||||||
|
<div 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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
Раздел находится в разработке
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
//import _ from 'lodash';
|
||||||
|
|
||||||
|
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 ContentsPageProps {
|
||||||
|
selectedTab = 'contents';
|
||||||
|
contents = [];
|
||||||
|
images = [];
|
||||||
|
imageSrc = [];
|
||||||
|
imageLoaded = [];
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(currentBook, parsed) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
|
//закладки
|
||||||
|
|
||||||
|
//проверим, надо ли обновлять списки
|
||||||
|
if (this.parsed == parsed) {
|
||||||
|
this.updateBookPosSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//далее формирование оглавления
|
||||||
|
this.parsed = parsed;
|
||||||
|
this.contents = [];
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
const pc = parsed.contents;
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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: []});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 = getIndentStyle(sub.inset + 1);
|
||||||
|
const ls = getLabelStyle(cont.inset + 1);
|
||||||
|
const p = parsed.para[sub.paraIndex];
|
||||||
|
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[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
|
||||||
|
|
||||||
|
i++;
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('do-action', {action: 'contents'});
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-panel {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 90%;
|
||||||
|
padding: 0 10px 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item, .subitem, .item-book-pos, .subitem-book-pos {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover, .subitem:hover {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subitems-transition {
|
||||||
|
height: 0;
|
||||||
|
transition: height 0.2s linear;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
<span class="greeting"><b>{{ title }}</b></span>
|
<span class="greeting"><b>{{ title }}</b></span>
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<span class="greeting">Добро пожаловать!</span>
|
<span class="greeting">Добро пожаловать!</span>
|
||||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</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>rtf, doc, docx, pdf, epub, mobi</b></span>
|
<span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
|
|||||||
@@ -39,15 +39,19 @@
|
|||||||
<q-icon name="la la-copy" size="32px"/>
|
<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>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
|
<button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
|
||||||
<q-icon name="la la-retweet" size="32px"/>
|
<q-icon name="la la-magic" size="32px"/>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
<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}"/>
|
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
|
<button ref="contents" v-show="showToolButton['contents']" class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')" v-ripple>
|
||||||
|
<q-icon name="la la-list" size="32px"/>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['contents'] }}</q-tooltip>
|
||||||
|
</button>
|
||||||
<button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
|
<button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
|
||||||
<q-icon name="la la-sitemap" size="32px"/>
|
<q-icon name="la la-sitemap" size="32px"/>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
|
||||||
@@ -89,71 +93,15 @@
|
|||||||
@stop-text-search="stopTextSearch">
|
@stop-text-search="stopTextSearch">
|
||||||
</SearchPage>
|
</SearchPage>
|
||||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
||||||
<LibsPage v-show="libsActive" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
||||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
||||||
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
||||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
|
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||||
|
|
||||||
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
||||||
<template slot="header">
|
|
||||||
Что нового:
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
|
||||||
|
|
||||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
|
||||||
<span slot="footer">
|
|
||||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
|
||||||
</span>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog ref="dialog2" v-model="donationVisible">
|
|
||||||
<template slot="header">
|
|
||||||
Здравствуйте, уважаемые читатели!
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div style="word-break: normal">
|
|
||||||
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
|
||||||
|
|
||||||
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
|
||||||
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
|
||||||
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>непрерывно улучшаемой</li>
|
|
||||||
<li>без рекламы</li>
|
|
||||||
<li>без регистрации</li>
|
|
||||||
<li>Open Source</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
Автор также обращается с просьбой о помощи в распространении
|
|
||||||
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
|
||||||
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
|
|
||||||
<br><br>
|
|
||||||
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
|
||||||
<br>
|
|
||||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
|
||||||
</span>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -178,13 +126,17 @@ import SettingsPage from './SettingsPage/SettingsPage.vue';
|
|||||||
import HelpPage from './HelpPage/HelpPage.vue';
|
import HelpPage from './HelpPage/HelpPage.vue';
|
||||||
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
||||||
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
||||||
import Dialog from '../share/Dialog.vue';
|
import ContentsPage from './ContentsPage/ContentsPage.vue';
|
||||||
|
|
||||||
|
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
||||||
|
|
||||||
import bookManager from './share/bookManager';
|
import bookManager from './share/bookManager';
|
||||||
import rstore from '../../store/modules/reader';
|
import rstore from '../../store/modules/reader';
|
||||||
import readerApi from '../../api/reader';
|
import readerApi from '../../api/reader';
|
||||||
import * as utils from '../../share/utils';
|
import miscApi from '../../api/misc';
|
||||||
|
|
||||||
import {versionHistory} from './versionHistory';
|
import {versionHistory} from './versionHistory';
|
||||||
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -201,7 +153,9 @@ export default @Component({
|
|||||||
HelpPage,
|
HelpPage,
|
||||||
ClickMapPage,
|
ClickMapPage,
|
||||||
ServerStorage,
|
ServerStorage,
|
||||||
Dialog,
|
ContentsPage,
|
||||||
|
|
||||||
|
ReaderDialogs,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
bookPos: function(newValue) {
|
bookPos: function(newValue) {
|
||||||
@@ -258,6 +212,7 @@ class Reader extends Vue {
|
|||||||
settingsActive = false;
|
settingsActive = false;
|
||||||
helpActive = false;
|
helpActive = false;
|
||||||
clickMapActive = false;
|
clickMapActive = false;
|
||||||
|
contentsActive = false;
|
||||||
|
|
||||||
bookPos = null;
|
bookPos = null;
|
||||||
allowUrlParamBookPos = false;
|
allowUrlParamBookPos = false;
|
||||||
@@ -277,7 +232,6 @@ class Reader extends Vue {
|
|||||||
this.rstore = rstore;
|
this.rstore = rstore;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
|
||||||
this.reader = this.$store.state.reader;
|
this.reader = this.$store.state.reader;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
|
|
||||||
@@ -298,11 +252,11 @@ class Reader extends Vue {
|
|||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.paramPosIgnore = false;
|
this.paramPosIgnore = false;
|
||||||
}
|
}
|
||||||
}, 500, {'maxWait':5000});
|
}, 500, {maxWait: 5000});
|
||||||
|
|
||||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||||
this.debouncedSetRecentBook(newValue);
|
this.debouncedSetRecentBook(newValue);
|
||||||
}, 15000, {'maxWait':20000});
|
}, 15000, {maxWait: 20000});
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
@@ -331,10 +285,25 @@ class Reader extends Vue {
|
|||||||
this.checkActivateDonateHelpPage();
|
this.checkActivateDonateHelpPage();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
//проверим состояние Settings, и обновим, если надо
|
||||||
|
const newSettings = rstore.addDefaultsToSettings(this.settings);
|
||||||
|
if (newSettings) {
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
|
|
||||||
await this.showWhatsNew();
|
await this.$refs.dialogs.init();
|
||||||
await this.showDonation();
|
})();
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
this.isFirstNeedUpdateNotify = true;
|
||||||
|
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
|
||||||
|
while (true) {// eslint-disable-line no-constant-condition
|
||||||
|
await this.checkNewVersionAvailable();
|
||||||
|
await utils.sleep(3600*1000); //каждый час
|
||||||
|
}
|
||||||
|
//дальше кода нет
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,10 +314,13 @@ class Reader extends Vue {
|
|||||||
this.showClickMapPage = settings.showClickMapPage;
|
this.showClickMapPage = settings.showClickMapPage;
|
||||||
this.clickControl = settings.clickControl;
|
this.clickControl = settings.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
|
||||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
this.enableSitesFilter = settings.enableSitesFilter;
|
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.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||||
this.$root.readerActionByKeyEvent = (event) => {
|
this.$root.readerActionByKeyEvent = (event) => {
|
||||||
@@ -358,6 +330,30 @@ class Reader extends Vue {
|
|||||||
this.updateHeaderMinWidth();
|
this.updateHeaderMinWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkNewVersionAvailable() {
|
||||||
|
if (!this.checkingNewVersion && this.showNeedUpdateNotify) {
|
||||||
|
this.checkingNewVersion = true;
|
||||||
|
try {
|
||||||
|
await utils.sleep(15*1000); //подождем 15 секунд, чтобы прогрузился ServiceWorker при выходе новой версии
|
||||||
|
const config = await miscApi.loadConfig();
|
||||||
|
this.commit('config/setConfig', config);
|
||||||
|
|
||||||
|
let againMes = '';
|
||||||
|
if (this.isFirstNeedUpdateNotify) {
|
||||||
|
againMes = ' еще один раз';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.version != this.clientVersion)
|
||||||
|
this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
this.checkingNewVersion = false;
|
||||||
|
}
|
||||||
|
this.isFirstNeedUpdateNotify = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateHeaderMinWidth() {
|
updateHeaderMinWidth() {
|
||||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
||||||
if (this.$refs.buttons)
|
if (this.$refs.buttons)
|
||||||
@@ -410,64 +406,6 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async showWhatsNew() {
|
|
||||||
await utils.sleep(2000);
|
|
||||||
|
|
||||||
const whatsNew = versionHistory[0];
|
|
||||||
if (this.showWhatsNewDialog &&
|
|
||||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
|
||||||
whatsNew.header != this.whatsNewContentHash) {
|
|
||||||
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
|
||||||
this.whatsNewVisible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showDonation() {
|
|
||||||
await utils.sleep(3000);
|
|
||||||
const today = utils.formatDate(new Date(), 'coDate');
|
|
||||||
|
|
||||||
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
|
||||||
this.donationVisible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
donationDialogDisable() {
|
|
||||||
this.donationVisible = false;
|
|
||||||
if (this.showDonationDialog2020) {
|
|
||||||
const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
|
|
||||||
this.commit('reader/setSettings', newSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
donationDialogRemind() {
|
|
||||||
this.donationVisible = false;
|
|
||||||
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
|
||||||
}
|
|
||||||
|
|
||||||
openDonate() {
|
|
||||||
this.donationVisible = false;
|
|
||||||
this.donateToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyLink(link) {
|
|
||||||
const result = await utils.copyTextToClipboard(link);
|
|
||||||
if (result)
|
|
||||||
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
|
||||||
else
|
|
||||||
this.$root.notify.error('Копирование не удалось');
|
|
||||||
}
|
|
||||||
|
|
||||||
openVersionHistory() {
|
|
||||||
this.whatsNewVisible = false;
|
|
||||||
this.versionHistoryToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
whatsNewDisable() {
|
|
||||||
this.whatsNewVisible = false;
|
|
||||||
const whatsNew = versionHistory[0];
|
|
||||||
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
|
||||||
}
|
|
||||||
|
|
||||||
get routeParamPos() {
|
get routeParamPos() {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
const q = this.$route.query;
|
const q = this.$route.query;
|
||||||
@@ -497,6 +435,16 @@ class Reader extends Vue {
|
|||||||
return this.$store.state.config.mode;
|
return this.$store.state.config.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get version() {
|
||||||
|
return this.$store.state.config.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientVersion() {
|
||||||
|
let v = versionHistory[0].header;
|
||||||
|
v = v.split(' ')[0];
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
let result = '';
|
let result = '';
|
||||||
const path = this.$route.fullPath;
|
const path = this.$route.fullPath;
|
||||||
@@ -571,14 +519,6 @@ class Reader extends Vue {
|
|||||||
return this.$store.state.reader.settings;
|
return this.$store.state.reader.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
get whatsNewContentHash() {
|
|
||||||
return this.$store.state.reader.whatsNewContentHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
get donationRemindDate() {
|
|
||||||
return this.$store.state.reader.donationRemindDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction(pos) {
|
addAction(pos) {
|
||||||
let a = this.actionList;
|
let a = this.actionList;
|
||||||
if (!a.length || a[a.length - 1] != pos) {
|
if (!a.length || a[a.length - 1] != pos) {
|
||||||
@@ -611,6 +551,7 @@ class Reader extends Vue {
|
|||||||
this.stopScrolling();
|
this.stopScrolling();
|
||||||
this.stopSearch();
|
this.stopSearch();
|
||||||
this.helpActive = false;
|
this.helpActive = false;
|
||||||
|
this.contentsActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loaderToggle() {
|
loaderToggle() {
|
||||||
@@ -703,12 +644,6 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshBookSplitToPara() {
|
|
||||||
if (this.mostRecentBook()) {
|
|
||||||
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recentBooksClose() {
|
recentBooksClose() {
|
||||||
this.recentBooksActive = false;
|
this.recentBooksActive = false;
|
||||||
}
|
}
|
||||||
@@ -724,6 +659,21 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentsPageToggle() {
|
||||||
|
this.contentsActive = !this.contentsActive;
|
||||||
|
const page = this.$refs.page;
|
||||||
|
if (this.contentsActive && this.activePage == 'TextPage' && page.parsed) {
|
||||||
|
this.closeAllWindows();
|
||||||
|
this.contentsActive = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.contentsPage.init(this.mostRecentBook(), page.parsed);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.contentsActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
libsClose() {
|
libsClose() {
|
||||||
if (this.libsActive)
|
if (this.libsActive)
|
||||||
this.libsToogle();
|
this.libsToogle();
|
||||||
@@ -757,6 +707,12 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convOptionsToggle() {
|
||||||
|
this.settingsToggle();
|
||||||
|
if (this.settingsActive)
|
||||||
|
this.$refs.settingsPage.selectedTab = 'convert';
|
||||||
|
}
|
||||||
|
|
||||||
helpToggle() {
|
helpToggle() {
|
||||||
this.helpActive = !this.helpActive;
|
this.helpActive = !this.helpActive;
|
||||||
if (this.helpActive) {
|
if (this.helpActive) {
|
||||||
@@ -784,9 +740,8 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshBook() {
|
refreshBook() {
|
||||||
if (this.mostRecentBook()) {
|
const mrb = this.mostRecentBook();
|
||||||
this.loadBook({url: this.mostRecentBook().url, force: true});
|
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
undoAction() {
|
undoAction() {
|
||||||
@@ -826,8 +781,9 @@ class Reader extends Vue {
|
|||||||
case 'scrolling':
|
case 'scrolling':
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
case 'splitToPara':
|
case 'convOptions':
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
|
case 'contents':
|
||||||
case 'libs':
|
case 'libs':
|
||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
@@ -856,9 +812,9 @@ class Reader extends Vue {
|
|||||||
case 'scrolling':
|
case 'scrolling':
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
|
case 'contents':
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
case 'splitToPara':
|
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
if (!this.mostRecentBookReactive)
|
if (!this.mostRecentBookReactive)
|
||||||
@@ -939,8 +895,12 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
let url = encodeURI(decodeURI(opts.url));
|
let url = encodeURI(decodeURI(opts.url));
|
||||||
|
|
||||||
|
//TODO: убрать конвертирование 'file://' после 06.2021
|
||||||
|
if (url.length == 71 && url.indexOf('file://') == 0)
|
||||||
|
url = url.replace(/^file/, 'disk');
|
||||||
|
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||||
(url.indexOf('file://') != 0))
|
(url.indexOf('disk://') != 0))
|
||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
|
|
||||||
// уже просматривается сейчас
|
// уже просматривается сейчас
|
||||||
@@ -971,6 +931,7 @@ class Reader extends Vue {
|
|||||||
wasOpened = (wasOpened ? wasOpened : {});
|
wasOpened = (wasOpened ? wasOpened : {});
|
||||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||||
|
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
||||||
|
|
||||||
let book = null;
|
let book = null;
|
||||||
|
|
||||||
@@ -1015,9 +976,13 @@ class Reader extends Vue {
|
|||||||
if (!book) {
|
if (!book) {
|
||||||
book = await readerApi.loadBook({
|
book = await readerApi.loadBook({
|
||||||
url,
|
url,
|
||||||
skipCheck: (opts.skipCheck ? true : false),
|
uploadFileName,
|
||||||
isText: (opts.isText ? true : false),
|
enableSitesFilter: this.enableSitesFilter,
|
||||||
enableSitesFilter: this.enableSitesFilter
|
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||||
|
isText: (this.splitToPara ? true : false),
|
||||||
|
djvuQuality: this.djvuQuality,
|
||||||
|
pdfAsText: this.pdfAsText,
|
||||||
|
pdfQuality: this.pdfQuality,
|
||||||
},
|
},
|
||||||
(state) => {
|
(state) => {
|
||||||
progress.setState(state);
|
progress.setState(state);
|
||||||
@@ -1033,7 +998,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.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(bookPos);
|
||||||
this.updateRoute(true);
|
this.updateRoute(true);
|
||||||
@@ -1051,6 +1016,8 @@ class Reader extends Vue {
|
|||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
} finally {
|
||||||
|
this.checkNewVersionAvailable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1070,7 +1037,7 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
|
|
||||||
await this.loadBook({url});
|
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
@@ -1141,12 +1108,15 @@ class Reader extends Vue {
|
|||||||
case 'copyText':
|
case 'copyText':
|
||||||
this.copyTextToggle();
|
this.copyTextToggle();
|
||||||
break;
|
break;
|
||||||
case 'splitToPara':
|
case 'convOptions':
|
||||||
this.refreshBookSplitToPara();
|
this.convOptionsToggle();
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
break;
|
break;
|
||||||
|
case 'contents':
|
||||||
|
this.contentsPageToggle();
|
||||||
|
break;
|
||||||
case 'libs':
|
case 'libs':
|
||||||
this.libsToogle();
|
this.libsToogle();
|
||||||
break;
|
break;
|
||||||
@@ -1222,9 +1192,12 @@ class Reader extends Vue {
|
|||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
let result = false;
|
let result = false;
|
||||||
if (this.$root.rootRoute() == '/reader') {
|
if (this.$root.rootRoute() == '/reader') {
|
||||||
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
|
if (this.$root.stdDialog.active)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
result = this.$refs.dialogs.keyHook(event);
|
||||||
|
|
||||||
if (!result && this.helpActive)
|
if (!result && this.helpActive)
|
||||||
result = this.$refs.helpPage.keyHook(event);
|
result = this.$refs.helpPage.keyHook(event);
|
||||||
|
|
||||||
@@ -1243,6 +1216,9 @@ class Reader extends Vue {
|
|||||||
if (!result && this.copyTextActive)
|
if (!result && this.copyTextActive)
|
||||||
result = this.$refs.copyTextPage.keyHook(event);
|
result = this.$refs.copyTextPage.keyHook(event);
|
||||||
|
|
||||||
|
if (!result && this.contentsActive)
|
||||||
|
result = this.$refs.contentsPage.keyHook(event);
|
||||||
|
|
||||||
if (!result && this.$refs.page && this.$refs.page.keyHook)
|
if (!result && this.$refs.page && this.$refs.page.keyHook)
|
||||||
result = this.$refs.page.keyHook(event);
|
result = this.$refs.page.keyHook(event);
|
||||||
|
|
||||||
@@ -1333,16 +1309,4 @@ class Reader extends Vue {
|
|||||||
.clear {
|
.clear {
|
||||||
color: rgba(0,0,0,0);
|
color: rgba(0,0,0,0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
|
||||||
color: blue;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 120%;
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
199
client/components/Reader/ReaderDialogs/ReaderDialogs.vue
Normal file
199
client/components/Reader/ReaderDialogs/ReaderDialogs.vue
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||||
|
<template slot="header">
|
||||||
|
Что нового:
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||||
|
|
||||||
|
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||||
|
<span slot="footer">
|
||||||
|
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||||
|
</span>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog ref="dialog2" v-model="donationVisible">
|
||||||
|
<template slot="header">
|
||||||
|
Здравствуйте, уважаемые читатели!
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
||||||
|
|
||||||
|
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
||||||
|
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
||||||
|
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>непрерывно улучшаемой</li>
|
||||||
|
<li>без рекламы</li>
|
||||||
|
<li>без регистрации</li>
|
||||||
|
<li>Open Source</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Автор также обращается с просьбой о помощи в распространении
|
||||||
|
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
||||||
|
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
||||||
|
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
|
||||||
|
<br><br>
|
||||||
|
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||||
|
<br>
|
||||||
|
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||||
|
</span>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import Dialog from '../../share/Dialog.vue';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
import {versionHistory} from '../versionHistory';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Dialog
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
settings: function() {
|
||||||
|
this.loadSettings();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class ReaderDialogs extends Vue {
|
||||||
|
whatsNewVisible = false;
|
||||||
|
whatsNewContent = '';
|
||||||
|
donationVisible = false;
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.commit = this.$store.commit;
|
||||||
|
this.loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.showWhatsNew();
|
||||||
|
await this.showDonation();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings() {
|
||||||
|
const settings = this.settings;
|
||||||
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
|
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||||
|
}
|
||||||
|
|
||||||
|
async showWhatsNew() {
|
||||||
|
const whatsNew = versionHistory[0];
|
||||||
|
if (this.showWhatsNewDialog &&
|
||||||
|
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||||
|
whatsNew.header != this.whatsNewContentHash) {
|
||||||
|
await utils.sleep(2000);
|
||||||
|
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
||||||
|
this.whatsNewVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showDonation() {
|
||||||
|
const today = utils.formatDate(new Date(), 'coDate');
|
||||||
|
|
||||||
|
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
this.donationVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donationDialogDisable() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
if (this.showDonationDialog2020) {
|
||||||
|
this.commit('reader/setSettings', { showDonationDialog2020: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donationDialogRemind() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
openDonate() {
|
||||||
|
this.donationVisible = false;
|
||||||
|
this.$emit('donate-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyLink(link) {
|
||||||
|
const result = await utils.copyTextToClipboard(link);
|
||||||
|
if (result)
|
||||||
|
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
||||||
|
else
|
||||||
|
this.$root.notify.error('Копирование не удалось');
|
||||||
|
}
|
||||||
|
|
||||||
|
openVersionHistory() {
|
||||||
|
this.whatsNewVisible = false;
|
||||||
|
this.$emit('version-history-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
whatsNewDisable() {
|
||||||
|
this.whatsNewVisible = false;
|
||||||
|
const whatsNew = versionHistory[0];
|
||||||
|
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
||||||
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return this.$store.state.reader.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
get whatsNewContentHash() {
|
||||||
|
return this.$store.state.reader.whatsNewContentHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
get donationRemindDate() {
|
||||||
|
return this.$store.state.reader.donationRemindDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook() {
|
||||||
|
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 120%;
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
<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 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>
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import _ from 'lodash';
|
//import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
@@ -112,6 +112,7 @@ class RecentBooksPage extends Vue {
|
|||||||
pagination = {};
|
pagination = {};
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
this.firstInit = true;
|
||||||
this.pagination = {rowsPerPage: 0};
|
this.pagination = {rowsPerPage: 0};
|
||||||
|
|
||||||
this.columns = [
|
this.columns = [
|
||||||
@@ -167,26 +168,11 @@ class RecentBooksPage extends Vue {
|
|||||||
this.initing = true;
|
this.initing = true;
|
||||||
|
|
||||||
|
|
||||||
if (!bookManager.loaded) {
|
if (this.firstInit) {//для отзывчивости
|
||||||
await this.updateTableData(10);
|
await this.updateTableData(20);
|
||||||
//для отзывчивости
|
this.firstInit = false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
await utils.sleep(50);
|
||||||
await this.updateTableData();
|
await this.updateTableData();
|
||||||
this.initing = false;
|
this.initing = false;
|
||||||
})();
|
})();
|
||||||
@@ -222,30 +208,11 @@ class RecentBooksPage extends Vue {
|
|||||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fb2 = (book.fb2 ? book.fb2 : {});
|
const bt = utils.getBookTitle(book.fb2);
|
||||||
|
|
||||||
let title = fb2.bookTitle;
|
let title = bt.bookTitle;
|
||||||
if (title)
|
title = (title ? `"${title}"`: '');
|
||||||
title = `"${title}"`;
|
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
|
||||||
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));
|
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
num,
|
num,
|
||||||
@@ -256,9 +223,10 @@ class RecentBooksPage extends Vue {
|
|||||||
author,
|
author,
|
||||||
title: `${title}${perc}${textLen}`,
|
title: `${title}${perc}${textLen}`,
|
||||||
},
|
},
|
||||||
descString: `${author}${title}${perc}${textLen}`,
|
descString: `${author}${title}${perc}${textLen}`,//для сортировки
|
||||||
url: book.url,
|
url: book.url,
|
||||||
path: book.path,
|
path: book.path,
|
||||||
|
fullTitle: bt.fullTitle,
|
||||||
key: book.key,
|
key: book.key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -291,13 +259,18 @@ class RecentBooksPage extends Vue {
|
|||||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBook(fb2path) {
|
async downloadBook(fb2path, fullTitle) {
|
||||||
try {
|
try {
|
||||||
await readerApi.checkCachedBook(fb2path);
|
await readerApi.checkCachedBook(fb2path);
|
||||||
|
|
||||||
const d = this.$refs.download;
|
const d = this.$refs.download;
|
||||||
d.href = fb2path;
|
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();
|
d.click();
|
||||||
} catch (e) {
|
} 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) {
|
async handleDel(key) {
|
||||||
await bookManager.delRecentBook({key});
|
await bookManager.delRecentBook({key});
|
||||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||||
@@ -331,7 +296,7 @@ class RecentBooksPage extends Vue {
|
|||||||
|
|
||||||
isUrl(url) {
|
isUrl(url) {
|
||||||
if (url)
|
if (url)
|
||||||
return (url.indexOf('file://') != 0);
|
return (url.indexOf('disk://') != 0);
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,8 +216,15 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error(message) {
|
error(message) {
|
||||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
if (this.showServerStorageMessages && !this.offlineModeActive) {
|
||||||
this.$root.notify.error(message);
|
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) {
|
async loadSettings(force = false, doNotifySuccess = true) {
|
||||||
@@ -505,12 +512,12 @@ class ServerStorage extends Vue {
|
|||||||
|
|
||||||
const md = newRecentMod.data;
|
const md = newRecentMod.data;
|
||||||
if (md.key && result[md.key])
|
if (md.key && result[md.key])
|
||||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, true);
|
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||||
|
|
||||||
if (!bookManager.loaded) {
|
/*if (!bookManager.loaded) {
|
||||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||||
while (!bookManager.loaded) await utils.sleep(100);
|
while (!bookManager.loaded) await utils.sleep(100);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (newRecent.rev != this.cachedRecent.rev)
|
if (newRecent.rev != this.cachedRecent.rev)
|
||||||
await this.setCachedRecent(newRecent);
|
await this.setCachedRecent(newRecent);
|
||||||
@@ -569,7 +576,7 @@ class ServerStorage extends Vue {
|
|||||||
|
|
||||||
let applyMod = this.cachedRecentMod.data;
|
let applyMod = this.cachedRecentMod.data;
|
||||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true);
|
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||||
|
|
||||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
needSaveRecentPatch = true;
|
needSaveRecentPatch = true;
|
||||||
@@ -580,8 +587,8 @@ class ServerStorage extends Vue {
|
|||||||
let newRecent = {};
|
let newRecent = {};
|
||||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
//ждем весь bm.recent
|
//ждем весь bm.recent
|
||||||
while (!bookManager.loaded)
|
/*while (!bookManager.loaded)
|
||||||
await utils.sleep(100);
|
await utils.sleep(100);*/
|
||||||
|
|
||||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
<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="keys" icon="la la-gamepad" label="Управление" />
|
||||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
||||||
|
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||||
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
||||||
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
||||||
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
||||||
@@ -53,6 +54,10 @@
|
|||||||
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
||||||
@@include('./include/PageMoveTab.inc');
|
@@include('./include/PageMoveTab.inc');
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Конвертирование ------------------------------------------------------------->
|
||||||
|
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
||||||
|
@@include('./include/ConvertTab.inc');
|
||||||
|
</div>
|
||||||
<!-- Прочее ---------------------------------------------------------------------->
|
<!-- Прочее ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
||||||
@@include('./include/OthersTab.inc');
|
@@include('./include/OthersTab.inc');
|
||||||
@@ -108,8 +113,10 @@ export default @Component({
|
|||||||
},
|
},
|
||||||
vertShift: function(newValue) {
|
vertShift: function(newValue) {
|
||||||
const font = (this.webFontName ? this.webFontName : this.fontName);
|
const font = (this.webFontName ? this.webFontName : this.fontName);
|
||||||
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
|
if (this.fontShifts[font] != newValue) {
|
||||||
this.fontVertShift = newValue;
|
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
|
||||||
|
this.fontVertShift = newValue;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fontName: function(newValue) {
|
fontName: function(newValue) {
|
||||||
const font = (this.webFontName ? this.webFontName : newValue);
|
const font = (this.webFontName ? this.webFontName : newValue);
|
||||||
@@ -185,10 +192,18 @@ class SettingsPage extends Vue {
|
|||||||
settingsChanged() {
|
settingsChanged() {
|
||||||
if (_.isEqual(this.form, this.settings))
|
if (_.isEqual(this.form, this.settings))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.form = Object.assign({}, this.settings);
|
this.form = Object.assign({}, this.settings);
|
||||||
|
if (!this.unwatch)
|
||||||
|
this.unwatch = {};
|
||||||
|
|
||||||
for (let prop in rstore.settingDefaults) {
|
for (let prop in rstore.settingDefaults) {
|
||||||
|
if (this.unwatch && this.unwatch[prop])
|
||||||
|
this.unwatch[prop]();
|
||||||
|
|
||||||
this[prop] = this.form[prop];
|
this[prop] = this.form[prop];
|
||||||
this.$watch(prop, (newValue) => {
|
|
||||||
|
this.unwatch[prop] = this.$watch(prop, (newValue) => {
|
||||||
this.form = Object.assign({}, this.form, {[prop]: newValue});
|
this.form = Object.assign({}, this.form, {[prop]: newValue});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -208,6 +223,10 @@ class SettingsPage extends Vue {
|
|||||||
return this.$store.state.config.mode;
|
return this.$store.state.config.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isExternalConverter() {
|
||||||
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
|
}
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
return this.$store.state.reader.settings;
|
return this.$store.state.reader.settings;
|
||||||
}
|
}
|
||||||
@@ -397,7 +416,7 @@ class SettingsPage extends Vue {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.currentProfile}' необратимо.` +
|
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` +
|
||||||
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
|
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
|
||||||
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
|
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
|
||||||
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||||
@@ -534,7 +553,7 @@ class SettingsPage extends Vue {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-1 {
|
.label-1, .label-7 {
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +565,7 @@ class SettingsPage extends Vue {
|
|||||||
width: 100px;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -104,8 +104,12 @@ class UserHotKeys extends UserHotKeysProps {
|
|||||||
this.updateTableData();
|
this.updateTableData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
updateTableData() {
|
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 search = this.search.toLowerCase();
|
||||||
const codesIncludeSearch = (action) => {
|
const codesIncludeSearch = (action) => {
|
||||||
|
|||||||
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>
|
||||||
@@ -36,7 +36,18 @@
|
|||||||
Показывать уведомление "Что нового"
|
Показывать уведомление "Что нового"
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
Показывать уведомления "Что нового"<br>
|
Показывать уведомления "Что нового"<br>
|
||||||
при каждом выходе новой версии читалки
|
при появлении новой версии читалки
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
|
||||||
|
Показывать уведомление о новой версии
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Напоминать о необходимости обновления страницы<br>
|
||||||
|
при появлении новой версии читалки
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,22 +65,6 @@
|
|||||||
<!---------------------------------------------->
|
<!---------------------------------------------->
|
||||||
<div class="part-header">Другое</div>
|
<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="item row">
|
||||||
<div class="label-6">Обработка</div>
|
<div class="label-6">Обработка</div>
|
||||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
||||||
style="width: 275px"
|
style="width: 275px"
|
||||||
dropdown-icon="la la-angle-down la-sm"
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
outlined dense emit-value map-options
|
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,9 +77,15 @@ export default class DrawHelper {
|
|||||||
let j = 0;
|
let j = 0;
|
||||||
//формируем строку
|
//формируем строку
|
||||||
for (const part of line.parts) {
|
for (const part of line.parts) {
|
||||||
let tOpen = (part.style.bold ? '<b>' : '');
|
let tOpen = '';
|
||||||
|
tOpen += (part.style.bold ? '<b>' : '');
|
||||||
tOpen += (part.style.italic ? '<i>' : '');
|
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>' : '');
|
tClose += (part.style.bold ? '</b>' : '');
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
@@ -154,12 +160,13 @@ export default class DrawHelper {
|
|||||||
return out;
|
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 pad = 3;
|
||||||
const fh = h - 2*pad;
|
const fh = h - 2*pad;
|
||||||
const fh2 = fh/2;
|
const fh2 = fh/2;
|
||||||
|
|
||||||
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
|
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 w1 = this.measureTextFont(t1, font) + fh2;
|
||||||
const read = (bookPos + 1)/textLength;
|
const read = (bookPos + 1)/textLength;
|
||||||
const t2 = `${(read*100).toFixed(2)}%`;
|
const t2 = `${(read*100).toFixed(2)}%`;
|
||||||
@@ -182,7 +189,7 @@ export default class DrawHelper {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
|
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
|
||||||
|
|
||||||
let out = `<div class="layout" style="` +
|
let out = `<div class="layout" style="` +
|
||||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||||
@@ -200,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.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>';
|
out += '</div>';
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import Component from 'vue-class-component';
|
|||||||
import {loadCSS} from 'fg-loadcss';
|
import {loadCSS} from 'fg-loadcss';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import {sleep} from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import bookManager from '../share/bookManager';
|
import bookManager from '../share/bookManager';
|
||||||
import DrawHelper from './DrawHelper';
|
import DrawHelper from './DrawHelper';
|
||||||
import rstore from '../../../store/modules/reader';
|
import rstore from '../../../store/modules/reader';
|
||||||
@@ -134,7 +134,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
this.$root.$on('resize', async() => {
|
this.$root.$on('resize', async() => {
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
await sleep(500);
|
await utils.sleep(500);
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
let close = null;
|
let close = null;
|
||||||
(async() => {
|
(async() => {
|
||||||
await sleep(500);
|
await utils.sleep(500);
|
||||||
if (this.fontsLoading)
|
if (this.fontsLoading)
|
||||||
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
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;
|
let i = 0;
|
||||||
const t = this.parsed.testText;
|
const t = this.parsed.testText;
|
||||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
||||||
await sleep(100);
|
await utils.sleep(100);
|
||||||
|
|
||||||
if (this.parsed === parsed) {
|
if (this.parsed === parsed) {
|
||||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||||
@@ -366,7 +366,6 @@ class TextPage extends Vue {
|
|||||||
this.updateLayout();
|
this.updateLayout();
|
||||||
this.book = null;
|
this.book = null;
|
||||||
this.meta = null;
|
this.meta = null;
|
||||||
this.fb2 = null;
|
|
||||||
this.parsed = null;
|
this.parsed = null;
|
||||||
|
|
||||||
this.linesUp = null;
|
this.linesUp = null;
|
||||||
@@ -383,7 +382,7 @@ class TextPage extends Vue {
|
|||||||
try {
|
try {
|
||||||
//подождем ленивый парсинг
|
//подождем ленивый парсинг
|
||||||
this.stopLazyParse = true;
|
this.stopLazyParse = true;
|
||||||
while (this.doingLazyParse) await sleep(10);
|
while (this.doingLazyParse) await utils.sleep(10);
|
||||||
|
|
||||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||||
if (!isParsed) {
|
if (!isParsed) {
|
||||||
@@ -392,21 +391,9 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
this.book = await bookManager.getBook(this.lastBook);
|
this.book = await bookManager.getBook(this.lastBook);
|
||||||
this.meta = bookManager.metaOnly(this.book);
|
this.meta = bookManager.metaOnly(this.book);
|
||||||
this.fb2 = this.meta.fb2;
|
const bt = utils.getBookTitle(this.meta.fb2);
|
||||||
|
|
||||||
let authorNames = [];
|
this.title = bt.fullTitle;
|
||||||
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.$root.$emit('set-app-title', this.title);
|
this.$root.$emit('set-app-title', this.title);
|
||||||
|
|
||||||
@@ -493,7 +480,7 @@ class TextPage extends Vue {
|
|||||||
let wait = (timeout + 201)/100;
|
let wait = (timeout + 201)/100;
|
||||||
while (wait > 0 && !this[stopPropertyName]) {
|
while (wait > 0 && !this[stopPropertyName]) {
|
||||||
wait--;
|
wait--;
|
||||||
await sleep(100);
|
await utils.sleep(100);
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
})().catch(reject); });
|
})().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.stopScrolling = false;
|
||||||
this.doingScrolling = true;
|
this.doingScrolling = true;
|
||||||
@@ -520,7 +507,7 @@ class TextPage extends Vue {
|
|||||||
this.page1 = this.page2;
|
this.page1 = this.page2;
|
||||||
this.toggleLayout = true;
|
this.toggleLayout = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
await sleep(50);
|
await utils.sleep(50);
|
||||||
|
|
||||||
this.cachedPos = -1;
|
this.cachedPos = -1;
|
||||||
this.draw();
|
this.draw();
|
||||||
@@ -557,7 +544,7 @@ class TextPage extends Vue {
|
|||||||
page.style.transform = 'none';
|
page.style.transform = 'none';
|
||||||
page.offsetHeight;
|
page.offsetHeight;
|
||||||
|
|
||||||
while (this.doingScrolling) await sleep(10);
|
while (this.doingScrolling) await utils.sleep(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
@@ -735,8 +722,24 @@ class TextPage extends Vue {
|
|||||||
message = this.statusBarMessage;
|
message = this.statusBarMessage;
|
||||||
if (!message)
|
if (!message)
|
||||||
message = this.title;
|
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,
|
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;
|
this.bookPosSeen = lines[i].end;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -766,7 +769,7 @@ class TextPage extends Vue {
|
|||||||
for (let i = 0; i < this.parsed.para.length; i++) {
|
for (let i = 0; i < this.parsed.para.length; i++) {
|
||||||
j++;
|
j++;
|
||||||
if (j > 1) {
|
if (j > 1) {
|
||||||
await sleep(1);
|
await utils.sleep(1);
|
||||||
j = 0;
|
j = 0;
|
||||||
}
|
}
|
||||||
if (this.stopLazyParse)
|
if (this.stopLazyParse)
|
||||||
@@ -788,7 +791,7 @@ class TextPage extends Vue {
|
|||||||
async refreshTime() {
|
async refreshTime() {
|
||||||
if (!this.timeRefreshing) {
|
if (!this.timeRefreshing) {
|
||||||
this.timeRefreshing = true;
|
this.timeRefreshing = true;
|
||||||
await sleep(60*1000);
|
await utils.sleep(60*1000);
|
||||||
|
|
||||||
if (this.book && this.parsed.textLength) {
|
if (this.book && this.parsed.textLength) {
|
||||||
this.debouncedDrawStatusBar();
|
this.debouncedDrawStatusBar();
|
||||||
@@ -904,9 +907,8 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
||||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
this.commit('reader/setSettings', {fontSize: newSize});
|
||||||
this.commit('reader/setSettings', newSettings);
|
await utils.sleep(50);
|
||||||
await sleep(50);
|
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -915,9 +917,8 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
||||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
this.commit('reader/setSettings', {fontSize: newSize});
|
||||||
this.commit('reader/setSettings', newSettings);
|
await utils.sleep(50);
|
||||||
await sleep(50);
|
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -926,9 +927,8 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
||||||
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
|
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||||
this.commit('reader/setSettings', newSettings);
|
await utils.sleep(50);
|
||||||
await sleep(50);
|
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,9 +937,8 @@ class TextPage extends Vue {
|
|||||||
if (!this.settingsChanging) {
|
if (!this.settingsChanging) {
|
||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
||||||
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
|
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||||
this.commit('reader/setSettings', newSettings);
|
await utils.sleep(50);
|
||||||
await sleep(50);
|
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -953,7 +952,7 @@ class TextPage extends Vue {
|
|||||||
let delay = 400;
|
let delay = 400;
|
||||||
while (this.repDoing) {
|
while (this.repDoing) {
|
||||||
this.handleClick(pointX, pointY);
|
this.handleClick(pointX, pointY);
|
||||||
await sleep(delay);
|
await utils.sleep(delay);
|
||||||
if (delay > 15)
|
if (delay > 15)
|
||||||
delay *= 0.8;
|
delay *= 0.8;
|
||||||
}
|
}
|
||||||
@@ -1070,7 +1069,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
onStatusBarClick() {
|
onStatusBarClick() {
|
||||||
const url = this.meta.url;
|
const url = this.meta.url;
|
||||||
if (url && url.indexOf('file://') != 0) {
|
if (url && url.indexOf('disk://') != 0) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else {
|
} else {
|
||||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import he from 'he';
|
import he from 'he';
|
||||||
import sax from '../../../../server/core/sax';
|
import sax from '../../../../server/core/sax';
|
||||||
import {sleep} from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxImageLineCount = 100;
|
const maxImageLineCount = 100;
|
||||||
|
|
||||||
@@ -46,11 +46,23 @@ export default class BookParser {
|
|||||||
let isFirstSection = true;
|
let isFirstSection = true;
|
||||||
let isFirstTitlePara = false;
|
let isFirstTitlePara = false;
|
||||||
|
|
||||||
|
//изображения
|
||||||
this.binary = {};
|
this.binary = {};
|
||||||
let binaryId = '';
|
let binaryId = '';
|
||||||
let binaryType = '';
|
let binaryType = '';
|
||||||
let dimPromises = [];
|
let dimPromises = [];
|
||||||
|
|
||||||
|
//оглавление
|
||||||
|
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 paraIndex = -1;
|
||||||
let paraOffset = 0;
|
let paraOffset = 0;
|
||||||
let para = []; /*array of
|
let para = []; /*array of
|
||||||
@@ -80,7 +92,7 @@ export default class BookParser {
|
|||||||
i.onerror = reject;
|
i.onerror = reject;
|
||||||
|
|
||||||
i.src = `data:${binaryType};base64,${data}`;
|
i.src = `data:${binaryType};base64,${data}`;
|
||||||
await sleep(30*1000);
|
await utils.sleep(30*1000);
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
reject('Не удалось получить размер изображения');
|
reject('Не удалось получить размер изображения');
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
@@ -102,7 +114,7 @@ export default class BookParser {
|
|||||||
i.onerror = reject;
|
i.onerror = reject;
|
||||||
|
|
||||||
i.src = src;
|
i.src = src;
|
||||||
await sleep(30*1000);
|
await utils.sleep(30*1000);
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
reject('Не удалось получить размер изображения');
|
reject('Не удалось получить размер изображения');
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
@@ -118,6 +130,12 @@ export default class BookParser {
|
|||||||
addIndex: (addIndex ? addIndex : 0),
|
addIndex: (addIndex ? addIndex : 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (inSubtitle) {
|
||||||
|
curSubtitle.title += '<p>';
|
||||||
|
} else if (inTitle) {
|
||||||
|
curTitle.title += '<p>';
|
||||||
|
}
|
||||||
|
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
paraOffset += p.length;
|
||||||
};
|
};
|
||||||
@@ -129,6 +147,7 @@ export default class BookParser {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prevParaIndex = paraIndex;
|
||||||
let p = para[paraIndex];
|
let p = para[paraIndex];
|
||||||
paraOffset -= p.length;
|
paraOffset -= p.length;
|
||||||
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
|
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
|
||||||
@@ -143,6 +162,11 @@ export default class BookParser {
|
|||||||
p.offset = paraOffset;
|
p.offset = paraOffset;
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
|
|
||||||
|
if (curTitle.paraIndex == prevParaIndex)
|
||||||
|
curTitle.paraIndex = paraIndex;
|
||||||
|
if (curSubtitle.paraIndex == prevParaIndex)
|
||||||
|
curSubtitle.paraIndex = paraIndex;
|
||||||
|
|
||||||
//уберем начальный пробел
|
//уберем начальный пробел
|
||||||
p.length = 0;
|
p.length = 0;
|
||||||
p.text = p.text.substr(1);
|
p.text = p.text.substr(1);
|
||||||
@@ -151,6 +175,13 @@ export default class BookParser {
|
|||||||
p.length += len;
|
p.length += len;
|
||||||
p.text += text;
|
p.text += text;
|
||||||
|
|
||||||
|
|
||||||
|
if (inSubtitle) {
|
||||||
|
curSubtitle.title += text;
|
||||||
|
} else if (inTitle) {
|
||||||
|
curTitle.title += text;
|
||||||
|
}
|
||||||
|
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
paraOffset += p.length;
|
||||||
};
|
};
|
||||||
@@ -160,11 +191,12 @@ export default class BookParser {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
tag = elemName;
|
tag = elemName;
|
||||||
path += '/' + elemName;
|
path += '/' + tag;
|
||||||
|
|
||||||
if (tag == 'binary') {
|
if (tag == 'binary') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
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')
|
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
||||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||||
}
|
}
|
||||||
@@ -173,31 +205,80 @@ export default class BookParser {
|
|||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
const 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
|
if (href[0] == '#') {//local
|
||||||
|
imageNum++;
|
||||||
|
|
||||||
if (inPara && !this.showInlineImagesInCenter && !center)
|
if (inPara && !this.showInlineImagesInCenter && !center)
|
||||||
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
|
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||||
else
|
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)
|
if (inPara && this.showInlineImagesInCenter)
|
||||||
newParagraph(' ', 1);
|
newParagraph(' ', 1);
|
||||||
} else {//external
|
} else {//external
|
||||||
|
imageNum++;
|
||||||
|
|
||||||
dimPromises.push(getExternalImageDimensions(href));
|
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});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
if (path == '/fictionbook/description/title-info/author') {
|
||||||
if (!fb2.author)
|
if (!fb2.author)
|
||||||
fb2.author = [];
|
fb2.author = [];
|
||||||
|
|
||||||
fb2.author.push({});
|
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 (path.indexOf('/fictionbook/body') == 0) {
|
||||||
if (tag == 'body') {
|
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)
|
if (!isFirstBody)
|
||||||
newParagraph(' ', 1);
|
newParagraph(' ', 1);
|
||||||
isFirstBody = false;
|
isFirstBody = false;
|
||||||
|
bodyIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'title') {
|
if (tag == 'title') {
|
||||||
@@ -205,15 +286,20 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = true;
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
|
|
||||||
|
inTitle = true;
|
||||||
|
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||||
|
this.contents.push(curTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'section') {
|
if (tag == 'section') {
|
||||||
if (!isFirstSection)
|
if (!isFirstSection)
|
||||||
newParagraph(' ', 1);
|
newParagraph(' ', 1);
|
||||||
isFirstSection = false;
|
isFirstSection = false;
|
||||||
|
sectionLevel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||||
growParagraph(`<${tag}>`, 0);
|
growParagraph(`<${tag}>`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +317,18 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = true;
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
|
|
||||||
|
if (curTitle.paraIndex < 0) {
|
||||||
|
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||||
|
this.contents.push(curTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
inSubtitle = true;
|
||||||
|
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||||
|
curTitle.subtitles.push(curSubtitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'epigraph') {
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = true;
|
italic = true;
|
||||||
space += 1;
|
space += 1;
|
||||||
}
|
}
|
||||||
@@ -260,9 +355,14 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = false;
|
isFirstTitlePara = false;
|
||||||
bold = false;
|
bold = false;
|
||||||
center = false;
|
center = false;
|
||||||
|
inTitle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'section') {
|
||||||
|
sectionLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||||
growParagraph(`</${tag}>`, 0);
|
growParagraph(`</${tag}>`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,11 +374,14 @@ export default class BookParser {
|
|||||||
isFirstTitlePara = false;
|
isFirstTitlePara = false;
|
||||||
bold = false;
|
bold = false;
|
||||||
center = false;
|
center = false;
|
||||||
|
inSubtitle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'epigraph') {
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = false;
|
italic = false;
|
||||||
space -= 1;
|
space -= 1;
|
||||||
|
if (tag == 'annotation')
|
||||||
|
newParagraph(' ', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'stanza') {
|
if (tag == 'stanza') {
|
||||||
@@ -372,7 +475,7 @@ export default class BookParser {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onProgress = async(prog) => {
|
const onProgress = async(prog) => {
|
||||||
await sleep(1);
|
await utils.sleep(1);
|
||||||
callback(prog);
|
callback(prog);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -394,11 +497,20 @@ export default class BookParser {
|
|||||||
this.textLength = paraOffset;
|
this.textLength = paraOffset;
|
||||||
|
|
||||||
callback(100);
|
callback(100);
|
||||||
await sleep(10);
|
await utils.sleep(10);
|
||||||
|
|
||||||
return {fb2};
|
return {fb2};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageHrefToId(id) {
|
||||||
|
let local = false;
|
||||||
|
if (id[0] == '#') {
|
||||||
|
id = id.substr(1);
|
||||||
|
local = true;
|
||||||
|
}
|
||||||
|
return {id, local};
|
||||||
|
}
|
||||||
|
|
||||||
findParaIndex(bookPos) {
|
findParaIndex(bookPos) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
//дихотомия
|
//дихотомия
|
||||||
@@ -423,7 +535,7 @@ export default class BookParser {
|
|||||||
|
|
||||||
splitToStyle(s) {
|
splitToStyle(s) {
|
||||||
let result = [];/*array of {
|
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},
|
image: {local: Boolean, inline: Boolean, id: String},
|
||||||
text: String,
|
text: String,
|
||||||
}*/
|
}*/
|
||||||
@@ -446,6 +558,12 @@ export default class BookParser {
|
|||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
style.italic = true;
|
style.italic = true;
|
||||||
break;
|
break;
|
||||||
|
case 'sup':
|
||||||
|
style.sup = true;
|
||||||
|
break;
|
||||||
|
case 'sub':
|
||||||
|
style.sub = true;
|
||||||
|
break;
|
||||||
case 'center':
|
case 'center':
|
||||||
style.center = true;
|
style.center = true;
|
||||||
break;
|
break;
|
||||||
@@ -458,28 +576,21 @@ export default class BookParser {
|
|||||||
case 'image': {
|
case 'image': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
image = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
image.inline = false;
|
||||||
if (id[0] == '#') {
|
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
image = {local, inline: false, id};
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'image-inline': {
|
case 'image-inline': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
const img = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
img.inline = true;
|
||||||
if (id[0] == '#') {
|
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
result.push({
|
result.push({
|
||||||
style: Object.assign({}, style),
|
style: Object.assign({}, style),
|
||||||
image: {local, inline: true, id},
|
image: img,
|
||||||
text: ''
|
text: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -496,6 +607,12 @@ export default class BookParser {
|
|||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
style.italic = false;
|
style.italic = false;
|
||||||
break;
|
break;
|
||||||
|
case 'sup':
|
||||||
|
style.sup = false;
|
||||||
|
break;
|
||||||
|
case 'sub':
|
||||||
|
style.sub = false;
|
||||||
|
break;
|
||||||
case 'center':
|
case 'center':
|
||||||
style.center = false;
|
style.center = false;
|
||||||
break;
|
break;
|
||||||
@@ -700,6 +817,7 @@ export default class BookParser {
|
|||||||
paraIndex,
|
paraIndex,
|
||||||
w: imageWidth,
|
w: imageWidth,
|
||||||
h: imageHeight,
|
h: imageHeight,
|
||||||
|
num: part.image.num
|
||||||
}});
|
}});
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
line = {begin: line.end + 1, parts: []};
|
line = {begin: line.end + 1, parts: []};
|
||||||
@@ -710,7 +828,7 @@ export default class BookParser {
|
|||||||
line.last = true;
|
line.last = true;
|
||||||
line.parts.push({style, text: ' ',
|
line.parts.push({style, text: ' ',
|
||||||
image: {local: part.image.local, inline: false, id: part.image.id,
|
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;
|
continue;
|
||||||
@@ -722,7 +840,7 @@ export default class BookParser {
|
|||||||
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
||||||
imgW += bin.w*imgH/bin.h;
|
imgW += bin.w*imgH/bin.h;
|
||||||
line.parts.push({style, text: '',
|
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}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,25 @@ import _ from 'lodash';
|
|||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
|
||||||
const maxDataSize = 300*1024*1024;//compressed bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
|
|
||||||
|
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
name: 'bmMetaStore'
|
name: 'bmMetaStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//локальный кэш самих книг, ограничение maxDataSize
|
||||||
const bmDataStore = localForage.createInstance({
|
const bmDataStore = localForage.createInstance({
|
||||||
name: 'bmDataStore'
|
name: 'bmDataStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
const bmRecentStore = localForage.createInstance({
|
//список недавно открытых книг
|
||||||
|
const bmRecentStoreOld = localForage.createInstance({
|
||||||
name: 'bmRecentStore'
|
name: 'bmRecentStore'
|
||||||
});
|
});
|
||||||
|
const bmRecentStoreNew = localForage.createInstance({
|
||||||
|
name: 'bmRecentStoreNew'
|
||||||
|
});
|
||||||
|
|
||||||
class BookManager {
|
class BookManager {
|
||||||
async init(settings) {
|
async init(settings) {
|
||||||
@@ -25,15 +31,80 @@ class BookManager {
|
|||||||
|
|
||||||
this.eventListeners = [];
|
this.eventListeners = [];
|
||||||
this.books = {};
|
this.books = {};
|
||||||
this.recent = {};
|
|
||||||
|
|
||||||
this.recentLast = await bmRecentStore.getItem('recent-last');
|
this.recent = {};
|
||||||
if (this.recentLast) {
|
this.saveRecent = _.debounce(() => {
|
||||||
this.recent[this.recentLast.key] = this.recentLast;
|
bmRecentStoreNew.setItem('recent', this.recent);
|
||||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
}, 300, {maxWait: 800});
|
||||||
if (_.isObject(meta)) {
|
|
||||||
this.books[meta.key] = meta;
|
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;
|
this.recentChanged = true;
|
||||||
@@ -41,9 +112,7 @@ class BookManager {
|
|||||||
this.loadStored();//no await
|
this.loadStored();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
//Долгая асинхронная загрузка из хранилища.
|
//Ленивая асинхронная загрузка bmMetaStore
|
||||||
//Хранение в отдельных записях дает относительно
|
|
||||||
//нормальное поведение при нескольких вкладках с читалкой в браузере.
|
|
||||||
async loadStored() {
|
async loadStored() {
|
||||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||||
await utils.sleep(2000);
|
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.cleanBooks();
|
||||||
await this.cleanRecentBooks();
|
|
||||||
|
|
||||||
this.recentChanged = true;
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.emit('load-stored-finish');
|
this.emit('load-stored-finish');
|
||||||
}
|
}
|
||||||
@@ -125,7 +169,7 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deflateWithProgress(data, callback) {
|
async deflateWithProgress(data, callback) {
|
||||||
const chunkSize = 128*1024;
|
const chunkSize = 512*1024;
|
||||||
const deflator = new utils.pako.Deflate({level: 5});
|
const deflator = new utils.pako.Deflate({level: 5});
|
||||||
|
|
||||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
@@ -159,7 +203,7 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async inflateWithProgress(data, callback) {
|
async inflateWithProgress(data, callback) {
|
||||||
const chunkSize = 64*1024;
|
const chunkSize = 512*1024;
|
||||||
const inflator = new utils.pako.Inflate({to: 'string'});
|
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||||
|
|
||||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
@@ -238,7 +282,7 @@ class BookManager {
|
|||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
if (!book && !this.loaded) {
|
if (!book && !this.loaded) {
|
||||||
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||||
if (book)
|
if (book)
|
||||||
this.books[meta.key] = book;
|
this.books[meta.key] = book;
|
||||||
}
|
}
|
||||||
@@ -254,7 +298,7 @@ class BookManager {
|
|||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||||
if (result)
|
if (result)
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
@@ -328,51 +372,71 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-- recent --------------------------------------------------------------
|
//-- 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) {
|
async setRecentBook(value) {
|
||||||
const result = this.metaOnly(value);
|
let result = this.metaOnly(value);
|
||||||
result.touchTime = Date.now();
|
result.touchTime = Date.now();
|
||||||
result.deleted = 0;
|
result.deleted = 0;
|
||||||
|
|
||||||
if (this.recent[result.key] && this.recent[result.key].deleted) {
|
if (this.recent[result.key]) {
|
||||||
//восстановим из небытия пользовательские данные
|
result = Object.assign({}, this.recent[result.key], result);
|
||||||
if (!result.bookPos)
|
|
||||||
result.bookPos = this.recent[result.key].bookPos;
|
|
||||||
if (!result.bookPosSeen)
|
|
||||||
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recent[result.key] = result;
|
await this.recentSetLastKey(result.key);
|
||||||
|
await this.recentSetItem(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);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentBook(value) {
|
async getRecentBook(value) {
|
||||||
let result = this.recent[value.key];
|
return this.recent[value.key];
|
||||||
if (!result) {
|
|
||||||
result = await bmRecentStore.getItem(value.key);
|
|
||||||
if (result)
|
|
||||||
this.recent[value.key] = result;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value) {
|
||||||
this.recent[value.key].deleted = 1;
|
const item = this.recent[value.key];
|
||||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
item.deleted = 1;
|
||||||
|
|
||||||
if (this.recentLast.key == value.key) {
|
if (this.recentLastKey == value.key) {
|
||||||
this.recentLast = null;
|
await this.recentSetLastKey(null);
|
||||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.recentSetItem(item);
|
||||||
this.emit('recent-deleted', value.key);
|
this.emit('recent-deleted', value.key);
|
||||||
this.emit('recent-changed', value.key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
@@ -380,24 +444,49 @@ class BookManager {
|
|||||||
|
|
||||||
let isDel = false;
|
let isDel = false;
|
||||||
for (let i = 1000; i < sorted.length; i++) {
|
for (let i = 1000; i < sorted.length; i++) {
|
||||||
await bmRecentStore.removeItem(sorted[i].key);
|
|
||||||
delete this.recent[sorted[i].key];
|
delete this.recent[sorted[i].key];
|
||||||
await bmRecentStore.removeItem(sorted[i].key);
|
|
||||||
isDel = true;
|
isDel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortedRecentCached = null;
|
this.sortedRecentCached = null;
|
||||||
|
|
||||||
if (isDel)
|
if (isDel)
|
||||||
this.emit('recent-changed');
|
await this.recentSetItem();
|
||||||
return isDel;
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
async convertFileToDiskPrefix() {
|
||||||
if (this.recentLast) {
|
let isConverted = false;
|
||||||
return this.recentLast;
|
|
||||||
|
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 max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
@@ -408,10 +497,11 @@ class BookManager {
|
|||||||
result = book;
|
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');
|
this.emit('recent-changed');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -442,24 +532,12 @@ class BookManager {
|
|||||||
delete mergedRecent[i];
|
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.recent = mergedRecent;
|
||||||
|
|
||||||
this.recentLast = null;
|
await this.recentSetLastKey(null);
|
||||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
await this.recentSetItem(null, true);
|
||||||
|
|
||||||
this.recentChanged = true;
|
|
||||||
this.emit('set-recent');
|
this.emit('set-recent');
|
||||||
this.emit('recent-changed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(listener) {
|
addEventListener(listener) {
|
||||||
|
|||||||
@@ -1,4 +1,87 @@
|
|||||||
export const versionHistory = [
|
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)',
|
||||||
|
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)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлено окно "Оглавление/закладки"</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-11-11',
|
||||||
|
header: '0.9.7 (2020-11-12)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-11-05',
|
||||||
|
header: '0.9.6 (2020-11-06)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>завершена работа над новым окном "Библиотека"</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
showUntil: '2020-10-31',
|
showUntil: '2020-10-31',
|
||||||
header: '0.9.5 (2020-11-01)',
|
header: '0.9.5 (2020-11-01)',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="active">
|
<q-dialog v-model="active" no-route-dismiss>
|
||||||
<div class="column bg-white no-wrap">
|
<div class="column bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
@@ -32,7 +32,7 @@ const DialogProps = Vue.extend({
|
|||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<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>
|
<slot></slot>
|
||||||
|
|
||||||
<!--------------------------------------------------->
|
<!--------------------------------------------------->
|
||||||
<div v-show="type == 'alert'" class="bg-white no-wrap">
|
<div v-show="type == 'alert'" class="bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
<div v-html="caption"></div>
|
<div v-html="caption"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="close-icon column justify-center items-center">
|
<div class="close-icon column justify-center items-center">
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div v-show="type == 'confirm'" class="bg-white no-wrap">
|
<div v-show="type == 'confirm'" class="bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
<div v-html="caption"></div>
|
<div v-html="caption"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="close-icon column justify-center items-center">
|
<div class="close-icon column justify-center items-center">
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
<div v-html="caption"></div>
|
<div v-html="caption"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="close-icon column justify-center items-center">
|
<div class="close-icon column justify-center items-center">
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
|
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||||
<div v-html="caption"></div>
|
<div v-html="caption"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="close-icon column justify-center items-center">
|
<div class="close-icon column justify-center items-center">
|
||||||
@@ -127,6 +127,7 @@ class StdDialog extends Vue {
|
|||||||
inputValue = '';
|
inputValue = '';
|
||||||
error = '';
|
error = '';
|
||||||
iconColor = '';
|
iconColor = '';
|
||||||
|
iconName = '';
|
||||||
hotKeyCode = '';
|
hotKeyCode = '';
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@@ -144,12 +145,18 @@ class StdDialog extends Vue {
|
|||||||
this.inputValidator = null;
|
this.inputValidator = null;
|
||||||
this.inputValue = '';
|
this.inputValue = '';
|
||||||
this.error = '';
|
this.error = '';
|
||||||
|
this.showed = false;
|
||||||
|
|
||||||
this.iconColor = 'text-warning';
|
this.iconColor = 'text-warning';
|
||||||
if (opts && opts.color) {
|
if (opts && opts.color) {
|
||||||
this.iconColor = `text-${opts.color}`;
|
this.iconColor = `text-${opts.color}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.iconName = 'las la-exclamation-circle';
|
||||||
|
if (opts && opts.iconName) {
|
||||||
|
this.iconName = opts.iconName;
|
||||||
|
}
|
||||||
|
|
||||||
this.hotKeyCode = '';
|
this.hotKeyCode = '';
|
||||||
if (opts && opts.hotKeyCode) {
|
if (opts && opts.hotKeyCode) {
|
||||||
this.hotKeyCode = opts.hotKeyCode;
|
this.hotKeyCode = opts.hotKeyCode;
|
||||||
@@ -161,6 +168,7 @@ class StdDialog extends Vue {
|
|||||||
this.hideTrigger();
|
this.hideTrigger();
|
||||||
this.hideTrigger = null;
|
this.hideTrigger = null;
|
||||||
}
|
}
|
||||||
|
this.showed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
@@ -170,6 +178,7 @@ class StdDialog extends Vue {
|
|||||||
this.validate(this.inputValue);
|
this.validate(this.inputValue);
|
||||||
this.$refs.input.focus();
|
this.$refs.input.focus();
|
||||||
}
|
}
|
||||||
|
this.showed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(value) {
|
validate(value) {
|
||||||
@@ -276,7 +285,7 @@ class StdDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (this.active) {
|
if (this.active && this.showed) {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
if (this.type == 'hotKey') {
|
if (this.type == 'hotKey') {
|
||||||
if (event.type == 'keydown') {
|
if (event.type == 'keydown') {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export default @Component({
|
|||||||
class Window extends Vue {
|
class Window extends Vue {
|
||||||
init() {
|
init() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
this.$refs.main.style.top = 0;
|
||||||
|
this.$refs.main.style.left = 0;
|
||||||
|
|
||||||
this.$refs.windowBox.style.height = this.height;
|
this.$refs.windowBox.style.height = this.height;
|
||||||
this.$refs.windowBox.style.width = this.width;
|
this.$refs.windowBox.style.width = this.width;
|
||||||
if (this.maxWidth)
|
if (this.maxWidth)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import router from './router';
|
|||||||
import store from './store';
|
import store from './store';
|
||||||
import './quasar';
|
import './quasar';
|
||||||
|
|
||||||
|
import vueSanitize from 'vue-sanitize';
|
||||||
|
Vue.use(vueSanitize);
|
||||||
|
|
||||||
import App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
//Vue.config.productionTip = false;
|
//Vue.config.productionTip = false;
|
||||||
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {QSlider} from 'quasar/src/components/slider';
|
|||||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||||
import {QSeparator} from 'quasar/src/components/separator';
|
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 {QTooltip} from 'quasar/src/components/tooltip';
|
||||||
import {QSpinner} from 'quasar/src/components/spinner';
|
import {QSpinner} from 'quasar/src/components/spinner';
|
||||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||||
@@ -31,6 +31,8 @@ import {QColor} from 'quasar/src/components/color';
|
|||||||
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||||
import {QDialog} from 'quasar/src/components/dialog';
|
import {QDialog} from 'quasar/src/components/dialog';
|
||||||
import {QChip} from 'quasar/src/components/chip';
|
import {QChip} from 'quasar/src/components/chip';
|
||||||
|
import {QTree} from 'quasar/src/components/tree';
|
||||||
|
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
//QLayout,
|
//QLayout,
|
||||||
@@ -47,7 +49,7 @@ const components = {
|
|||||||
QTabs, QTab,
|
QTabs, QTab,
|
||||||
//QTabPanels, QTabPanel,
|
//QTabPanels, QTabPanel,
|
||||||
QSeparator,
|
QSeparator,
|
||||||
QList, QItem, QItemSection, QItemLabel,
|
//QList, QItem, QItemSection, QItemLabel,
|
||||||
QTooltip,
|
QTooltip,
|
||||||
QSpinner,
|
QSpinner,
|
||||||
QTable, QTh, QTr, QTd,
|
QTable, QTh, QTr, QTd,
|
||||||
@@ -57,6 +59,8 @@ const components = {
|
|||||||
QPopupProxy,
|
QPopupProxy,
|
||||||
QDialog,
|
QDialog,
|
||||||
QChip,
|
QChip,
|
||||||
|
QTree,
|
||||||
|
//QExpansionItem,
|
||||||
};
|
};
|
||||||
|
|
||||||
//directives
|
//directives
|
||||||
|
|||||||
@@ -103,47 +103,107 @@ export function fromBase64(data) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getObjDiff(oldObj, newObj) {
|
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||||
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
const {
|
||||||
|
exclude = [],
|
||||||
|
excludeAdd = [],
|
||||||
|
excludeDel = [],
|
||||||
|
} = opts;
|
||||||
|
|
||||||
for (const key of Object.keys(oldObj)) {
|
const ex = new Set(exclude);
|
||||||
if (newObj.hasOwnProperty(key)) {
|
const exAdd = new Set(excludeAdd);
|
||||||
if (!_.isEqual(oldObj[key], newObj[key])) {
|
const exDel = new Set(excludeDel);
|
||||||
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
|
||||||
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
|
const makeObjDiff = (oldObj, newObj, keyPath) => {
|
||||||
} else {
|
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
||||||
result.change[key] = _.cloneDeep(newObj[key]);
|
|
||||||
|
keyPath = `${keyPath}${keyPath ? '/' : ''}`;
|
||||||
|
|
||||||
|
for (const key of Object.keys(oldObj)) {
|
||||||
|
const kp = `${keyPath}${key}`;
|
||||||
|
|
||||||
|
if (newObj.hasOwnProperty(key)) {
|
||||||
|
if (ex.has(kp))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_.isEqual(oldObj[key], newObj[key])) {
|
||||||
|
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
||||||
|
result.change[key] = makeObjDiff(oldObj[key], newObj[key], kp);
|
||||||
|
} else {
|
||||||
|
result.change[key] = _.cloneDeep(newObj[key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (exDel.has(kp))
|
||||||
|
continue;
|
||||||
|
result.del.push(key);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result.del.push(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(newObj)) {
|
||||||
|
const kp = `${keyPath}${key}`;
|
||||||
|
if (exAdd.has(kp))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!oldObj.hasOwnProperty(key)) {
|
||||||
|
result.add[key] = _.cloneDeep(newObj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of Object.keys(newObj)) {
|
return makeObjDiff(oldObj, newObj, '');
|
||||||
if (!oldObj.hasOwnProperty(key)) {
|
|
||||||
result.add[key] = _.cloneDeep(newObj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isObjDiff(diff) {
|
export function isObjDiff(diff) {
|
||||||
return (_.isObject(diff) && diff.__isDiff);
|
return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEmptyObjDiff(diff) {
|
export function isEmptyObjDiff(diff) {
|
||||||
return (!_.isObject(diff) || !diff.__isDiff ||
|
return (!isObjDiff(diff) ||
|
||||||
(!Object.keys(diff.change).length &&
|
!(Object.keys(diff.change).length ||
|
||||||
!Object.keys(diff.add).length &&
|
Object.keys(diff.add).length ||
|
||||||
!diff.del.length
|
diff.del.length
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyObjDiff(obj, diff, isAddChanged) {
|
export function isEmptyObjDiffDeep(diff, opts = {}) {
|
||||||
const result = _.cloneDeep(obj);
|
if (!isObjDiff(diff))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isApplyChange = true,
|
||||||
|
isApplyAdd = true,
|
||||||
|
isApplyDel = true,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
let notEmptyDeep = false;
|
||||||
|
const change = diff.change;
|
||||||
|
for (const key of Object.keys(change)) {
|
||||||
|
if (_.isObject(change[key]))
|
||||||
|
notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
|
||||||
|
else if (isApplyChange)
|
||||||
|
notEmptyDeep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(
|
||||||
|
notEmptyDeep ||
|
||||||
|
(isApplyAdd && Object.keys(diff.add).length) ||
|
||||||
|
(isApplyDel && diff.del.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyObjDiff(obj, diff, opts = {}) {
|
||||||
|
const {
|
||||||
|
isAddChanged = false,
|
||||||
|
isApplyChange = true,
|
||||||
|
isApplyAdd = true,
|
||||||
|
isApplyDel = true,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
let result = _.cloneDeep(obj);
|
||||||
if (!diff.__isDiff)
|
if (!diff.__isDiff)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
@@ -151,21 +211,28 @@ export function applyObjDiff(obj, diff, isAddChanged) {
|
|||||||
for (const key of Object.keys(change)) {
|
for (const key of Object.keys(change)) {
|
||||||
if (result.hasOwnProperty(key)) {
|
if (result.hasOwnProperty(key)) {
|
||||||
if (_.isObject(change[key])) {
|
if (_.isObject(change[key])) {
|
||||||
result[key] = applyObjDiff(result[key], change[key], isAddChanged);
|
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||||
} else {
|
} else {
|
||||||
result[key] = _.cloneDeep(change[key]);
|
if (isApplyChange)
|
||||||
|
result[key] = _.cloneDeep(change[key]);
|
||||||
}
|
}
|
||||||
} else if (isAddChanged) {
|
} else if (isAddChanged) {
|
||||||
result[key] = _.cloneDeep(change[key]);
|
result[key] = _.cloneDeep(change[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of Object.keys(diff.add)) {
|
if (isApplyAdd) {
|
||||||
result[key] = _.cloneDeep(diff.add[key]);
|
for (const key of Object.keys(diff.add)) {
|
||||||
|
result[key] = _.cloneDeep(diff.add[key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of diff.del) {
|
if (isApplyDel && diff.del.length) {
|
||||||
delete result[key];
|
for (const key of diff.del) {
|
||||||
|
delete result[key];
|
||||||
|
}
|
||||||
|
if (_.isArray(result))
|
||||||
|
result = result.filter(v => v);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -237,3 +304,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
|
|||||||
}
|
}
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
@@ -10,18 +10,7 @@ const state = {
|
|||||||
const getters = {};
|
const getters = {};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
const actions = {
|
const actions = {};
|
||||||
async loadConfig({ commit, state }) {
|
|
||||||
commit('setApiError', null, { root: true });
|
|
||||||
commit('setConfig', {});
|
|
||||||
try {
|
|
||||||
const config = await miscApi.loadConfig();
|
|
||||||
commit('setConfig', config);
|
|
||||||
} catch (e) {
|
|
||||||
commit('setApiError', e, { root: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// mutations
|
// mutations
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
const readerActions = {
|
const readerActions = {
|
||||||
'help': 'Вызвать cправку',
|
'help': 'Вызвать cправку',
|
||||||
'loader': 'На страницу загрузки',
|
'loader': 'На страницу загрузки',
|
||||||
@@ -10,10 +12,11 @@ const readerActions = {
|
|||||||
'setPosition': 'Установить позицию',
|
'setPosition': 'Установить позицию',
|
||||||
'search': 'Найти в тексте',
|
'search': 'Найти в тексте',
|
||||||
'copyText': 'Скопировать текст со страницы',
|
'copyText': 'Скопировать текст со страницы',
|
||||||
'splitToPara': 'Обновить с разбиением на параграфы',
|
'convOptions': 'Настроить конвертирование',
|
||||||
'refresh': 'Принудительно обновить книгу',
|
'refresh': 'Принудительно обновить книгу',
|
||||||
'offlineMode': 'Автономный режим (без интернета)',
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
'libs': 'Библиотека',
|
'contents': 'Оглавление/закладки',
|
||||||
|
'libs': 'Сетевая библиотека',
|
||||||
'recentBooks': 'Открыть недавние',
|
'recentBooks': 'Открыть недавние',
|
||||||
'switchToolbar': 'Показать/скрыть панель управления',
|
'switchToolbar': 'Показать/скрыть панель управления',
|
||||||
'donate': '',
|
'donate': '',
|
||||||
@@ -38,8 +41,9 @@ const toolButtons = [
|
|||||||
{name: 'setPosition', show: true},
|
{name: 'setPosition', show: true},
|
||||||
{name: 'search', show: true},
|
{name: 'search', show: true},
|
||||||
{name: 'copyText', show: false},
|
{name: 'copyText', show: false},
|
||||||
{name: 'splitToPara', show: false},
|
{name: 'convOptions', show: true},
|
||||||
{name: 'refresh', show: true},
|
{name: 'refresh', show: true},
|
||||||
|
{name: 'contents', show: true},
|
||||||
{name: 'libs', show: true},
|
{name: 'libs', show: true},
|
||||||
{name: 'recentBooks', show: true},
|
{name: 'recentBooks', show: true},
|
||||||
{name: 'offlineMode', show: false},
|
{name: 'offlineMode', show: false},
|
||||||
@@ -56,12 +60,13 @@ const hotKeys = [
|
|||||||
{name: 'scrolling', codes: ['Z']},
|
{name: 'scrolling', codes: ['Z']},
|
||||||
{name: 'setPosition', codes: ['P']},
|
{name: 'setPosition', codes: ['P']},
|
||||||
{name: 'search', codes: ['Ctrl+F']},
|
{name: 'search', codes: ['Ctrl+F']},
|
||||||
{name: 'copyText', codes: ['Ctrl+C']},
|
{name: 'copyText', codes: ['Ctrl+C']},
|
||||||
{name: 'splitToPara', codes: ['Shift+R']},
|
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||||
{name: 'refresh', codes: ['R']},
|
{name: 'refresh', codes: ['R']},
|
||||||
{name: 'offlineMode', codes: ['O']},
|
{name: 'contents', codes: ['C']},
|
||||||
{name: 'libs', codes: ['L']},
|
{name: 'libs', codes: ['L']},
|
||||||
{name: 'recentBooks', codes: ['X']},
|
{name: 'recentBooks', codes: ['X']},
|
||||||
|
{name: 'offlineMode', codes: ['O']},
|
||||||
|
|
||||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||||
{name: 'bookBegin', codes: ['Home']},
|
{name: 'bookBegin', codes: ['Home']},
|
||||||
@@ -246,39 +251,22 @@ const settingDefaults = {
|
|||||||
compactTextPerc: 0,
|
compactTextPerc: 0,
|
||||||
imageHeightLines: 100,
|
imageHeightLines: 100,
|
||||||
imageFitWidth: true,
|
imageFitWidth: true,
|
||||||
|
enableSitesFilter: true,
|
||||||
|
splitToPara: false,
|
||||||
|
djvuQuality: 20,
|
||||||
|
pdfAsText: true,
|
||||||
|
pdfQuality: 20,
|
||||||
|
|
||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
showDonationDialog2020: true,
|
showDonationDialog2020: true,
|
||||||
enableSitesFilter: true,
|
showNeedUpdateNotify: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
userHotKeys: {},
|
userHotKeys: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const libsDefaults = {
|
|
||||||
startLink: 'http://flibusta.is',
|
|
||||||
comment: 'Флибуста | Книжное братство',
|
|
||||||
closeAfterSubmit: false,
|
|
||||||
groups: [
|
|
||||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
|
||||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
|
||||||
]},
|
|
||||||
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
|
|
||||||
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
|
|
||||||
]},
|
|
||||||
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
|
|
||||||
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
|
|
||||||
]},
|
|
||||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
|
||||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
|
||||||
]},
|
|
||||||
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
|
|
||||||
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
|
|
||||||
]},
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||||
for (const font of webFonts)
|
for (const font of webFonts)
|
||||||
@@ -288,6 +276,47 @@ for (const button of toolButtons)
|
|||||||
for (const hotKey of hotKeys)
|
for (const hotKey of hotKeys)
|
||||||
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
||||||
|
|
||||||
|
const excludeDiffHotKeys = [];
|
||||||
|
for (const hotKey of hotKeys)
|
||||||
|
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
|
||||||
|
|
||||||
|
function addDefaultsToSettings(settings) {
|
||||||
|
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
|
||||||
|
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
|
||||||
|
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const libsDefaults = {
|
||||||
|
startLink: 'http://flibusta.is',
|
||||||
|
comment: 'Флибуста | Книжное братство',
|
||||||
|
closeAfterSubmit: false,
|
||||||
|
openInFrameOnEnter: false,
|
||||||
|
openInFrameOnAdd: false,
|
||||||
|
groups: [
|
||||||
|
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||||
|
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||||
|
]},
|
||||||
|
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
||||||
|
{l: 'https://flibs.in', c: 'Flibs'},
|
||||||
|
]},
|
||||||
|
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||||
|
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||||
|
]},
|
||||||
|
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
|
||||||
|
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
|
||||||
|
]},
|
||||||
|
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
|
||||||
|
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
|
||||||
|
]},
|
||||||
|
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
|
||||||
|
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
const state = {
|
const state = {
|
||||||
toolBarActive: true,
|
toolBarActive: true,
|
||||||
@@ -341,7 +370,13 @@ const mutations = {
|
|||||||
state.currentProfile = value;
|
state.currentProfile = value;
|
||||||
},
|
},
|
||||||
setSettings(state, value) {
|
setSettings(state, value) {
|
||||||
state.settings = Object.assign({}, state.settings, value);
|
const newSettings = Object.assign({}, state.settings, value);
|
||||||
|
const added = addDefaultsToSettings(newSettings);
|
||||||
|
if (added) {
|
||||||
|
state.settings = added;
|
||||||
|
} else {
|
||||||
|
state.settings = newSettings;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setSettingsRev(state, value) {
|
setSettingsRev(state, value) {
|
||||||
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
||||||
@@ -361,6 +396,7 @@ export default {
|
|||||||
fonts,
|
fonts,
|
||||||
webFonts,
|
webFonts,
|
||||||
settingDefaults,
|
settingDefaults,
|
||||||
|
addDefaultsToSettings,
|
||||||
libsDefaults,
|
libsDefaults,
|
||||||
|
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/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"
|
||||||
|
|||||||
@@ -126,3 +126,18 @@ server {
|
|||||||
proxy_set_header Referer "";
|
proxy_set_header Referer "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 23580;
|
||||||
|
server_name fw_proxy;
|
||||||
|
|
||||||
|
valid_referers liberama.top b.liberama.top;
|
||||||
|
|
||||||
|
if ($invalid_referer) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://fantasy-worlds.org;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,15 +27,19 @@ sudo chown www-data.www-data /home/liberama
|
|||||||
|
|
||||||
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
### 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 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 converters
|
### external converters
|
||||||
```
|
```
|
||||||
|
sudo apt install rar
|
||||||
sudo apt install libreoffice
|
sudo apt install libreoffice
|
||||||
sudo apt install poppler-utils
|
sudo apt install poppler-utils
|
||||||
|
sudo apt install djvulibre-bin
|
||||||
|
sudo apt install libtiff-tools
|
||||||
|
sudo apt install graphicsmagick-imagemagick-compat
|
||||||
```
|
```
|
||||||
|
|
||||||
### nginx, server config
|
### nginx, server config
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if ! pgrep -x "liberama" > /dev/null ; then
|
if ! pgrep -x "liberama" > /dev/null ; then
|
||||||
sudo -H -u www-data /home/liberama/liberama
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
||||||
else
|
else
|
||||||
echo "Process 'liberama' already running"
|
echo "Process 'liberama' already running"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/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
|
sudo service cron start
|
||||||
|
|||||||
190
package-lock.json
generated
190
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.9.5",
|
"version": "0.9.11",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -36,12 +36,6 @@
|
|||||||
"pkg-up": "^2.0.0"
|
"pkg-up": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
|
||||||
"version": "1.0.30001062",
|
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
|
|
||||||
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.446",
|
"version": "1.3.446",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
||||||
@@ -321,12 +315,6 @@
|
|||||||
"pkg-up": "^2.0.0"
|
"pkg-up": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
|
||||||
"version": "1.0.30001062",
|
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
|
|
||||||
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.446",
|
"version": "1.3.446",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
||||||
@@ -1603,12 +1591,6 @@
|
|||||||
"pkg-up": "^2.0.0"
|
"pkg-up": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
|
||||||
"version": "1.0.30001062",
|
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
|
|
||||||
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.446",
|
"version": "1.3.446",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
||||||
@@ -4017,9 +3999,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30000989",
|
"version": "1.0.30001154",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001154.tgz",
|
||||||
"integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==",
|
"integrity": "sha512-y9DvdSti8NnYB9Be92ddMZQrcOe04kcQtcxtBx4NkB04+qZ+JUWotnXBJTmxlKudhxNTQ3RRknMwNU2YQl/Org==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"caseless": {
|
"caseless": {
|
||||||
@@ -4373,6 +4355,11 @@
|
|||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"colorette": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
|
||||||
|
},
|
||||||
"combined-stream": {
|
"combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -4759,12 +4746,6 @@
|
|||||||
"pkg-up": "^2.0.0"
|
"pkg-up": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
|
||||||
"version": "1.0.30001062",
|
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
|
|
||||||
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.446",
|
"version": "1.3.446",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
|
||||||
@@ -8696,6 +8677,11 @@
|
|||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"klona": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
|
||||||
|
},
|
||||||
"last-call-webpack-plugin": {
|
"last-call-webpack-plugin": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
|
||||||
@@ -8790,6 +8776,30 @@
|
|||||||
"immediate": "~3.0.5"
|
"immediate": "~3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"line-column": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=",
|
||||||
|
"requires": {
|
||||||
|
"isarray": "^1.0.0",
|
||||||
|
"isobject": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||||
|
},
|
||||||
|
"isobject": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
|
||||||
|
"requires": {
|
||||||
|
"isarray": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"loader-runner": {
|
"loader-runner": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
|
||||||
@@ -9372,6 +9382,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
|
||||||
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
|
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
|
||||||
},
|
},
|
||||||
|
"nanoid": {
|
||||||
|
"version": "3.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
|
||||||
|
"integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w=="
|
||||||
|
},
|
||||||
"nanomatch": {
|
"nanomatch": {
|
||||||
"version": "1.2.13",
|
"version": "1.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||||
@@ -10010,6 +10025,11 @@
|
|||||||
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
|
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
|
||||||
|
},
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -10104,6 +10124,21 @@
|
|||||||
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
||||||
"dev": true
|
"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": {
|
"pify": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||||
@@ -13306,6 +13341,97 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
|
"sanitize-html": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-yb+I+K25cn6PzsRJb+kGdlOBC3SkvEwJrr/Xl+6YR42oDZDeu61yWeEMSEi0hCKlRSYlWWE4tUnF2Ds+c/M1Vg==",
|
||||||
|
"requires": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^4.1.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"klona": "^2.0.3",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^3.0.0",
|
||||||
|
"entities": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domelementtype": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA=="
|
||||||
|
},
|
||||||
|
"domhandler": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domutils": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-NKbgaM8ZJOecTZsIzW5gSuplsX2IWW2mIK7xVr8hTQF2v1CJWTmLZ1HOCh5sH+IzVPAGE5IucooOkvwBRAdowA==",
|
||||||
|
"requires": {
|
||||||
|
"dom-serializer": "^1.0.1",
|
||||||
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||||
|
},
|
||||||
|
"escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.0.1",
|
||||||
|
"domhandler": "^3.0.0",
|
||||||
|
"domutils": "^2.0.0",
|
||||||
|
"entities": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-plain-object": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"version": "8.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.4.tgz",
|
||||||
|
"integrity": "sha512-LfqcwgMq9LOd8pX7K2+r2HPitlIGC5p6PoZhVELlqhh2YGDVcXKpkCseqan73Hrdik6nBd2OvoDPUaP/oMj9hQ==",
|
||||||
|
"requires": {
|
||||||
|
"colorette": "^1.2.1",
|
||||||
|
"line-column": "^1.0.2",
|
||||||
|
"nanoid": "^3.1.15",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sax": {
|
"sax": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||||
@@ -15193,6 +15319,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",
|
||||||
"integrity": "sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA=="
|
"integrity": "sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA=="
|
||||||
},
|
},
|
||||||
|
"vue-sanitize": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-sanitize/-/vue-sanitize-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-MhrfAaAqIQ8JXWfdlhK6f+H2WnZ7KQwkOgLNeUyfACI2bq/3HPZ2zeUQDathnLdsXMXAa8pJehOgPYtlaG9Jew==",
|
||||||
|
"requires": {
|
||||||
|
"sanitize-html": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vue-style-loader": {
|
"vue-style-loader": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.9.5",
|
"version": "0.9.12",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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: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: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 .",
|
"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",
|
"multer": "^1.4.2",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
|
"pidusage": "^2.0.21",
|
||||||
"quasar": "^1.14.3",
|
"quasar": "^1.14.3",
|
||||||
"safe-buffer": "^5.2.0",
|
"safe-buffer": "^5.2.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"unbzip2-stream": "^1.3.3",
|
"unbzip2-stream": "^1.3.3",
|
||||||
"vue": "github:bookpauk/vue",
|
"vue": "github:bookpauk/vue",
|
||||||
"vue-router": "^3.1.6",
|
"vue-router": "^3.1.6",
|
||||||
|
"vue-sanitize": "^0.2.1",
|
||||||
"vuex": "^3.1.2",
|
"vuex": "^3.1.2",
|
||||||
"vuex-persistedstate": "^2.7.1",
|
"vuex-persistedstate": "^2.7.1",
|
||||||
"webdav": "^2.10.2",
|
"webdav": "^2.10.2",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module.exports = {
|
|||||||
loggingEnabled: true,
|
loggingEnabled: true,
|
||||||
|
|
||||||
maxUploadFileSize: 50*1024*1024,//50Мб
|
maxUploadFileSize: 50*1024*1024,//50Мб
|
||||||
maxTempPublicDirSize: 512*1024*1024,//512Мб
|
maxTempPublicDirSize: 512*1024*1024,//512Мб + 20% квота если проблема с remoteWebDavStorage
|
||||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||||
|
|
||||||
useExternalBookConverter: false,
|
useExternalBookConverter: false,
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ class ReaderController extends BaseController {
|
|||||||
const workerId = this.readerWorker.loadBookUrl({
|
const workerId = this.readerWorker.loadBookUrl({
|
||||||
url: request.url,
|
url: request.url,
|
||||||
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
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),
|
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);
|
const state = this.workerState.getState(workerId);
|
||||||
return (state ? state : {});
|
return (state ? state : {});
|
||||||
|
|||||||
@@ -136,8 +136,9 @@ class WebSocketController {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 3*60*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||||
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ class FileDecompressor {
|
|||||||
constructor(limitFileSize = 0) {
|
constructor(limitFileSize = 0) {
|
||||||
this.detector = new FileDetector();
|
this.detector = new FileDetector();
|
||||||
this.limitFileSize = limitFileSize;
|
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) {
|
async decompressNested(filename, outputDir) {
|
||||||
@@ -30,7 +37,11 @@ class FileDecompressor {
|
|||||||
files: []
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +105,11 @@ class FileDecompressor {
|
|||||||
async decompress(fileExt, filename, outputDir) {
|
async decompress(fileExt, filename, outputDir) {
|
||||||
let files = [];
|
let files = [];
|
||||||
|
|
||||||
|
if (fileExt == 'rar' && this.rarExists) {
|
||||||
|
files = await this.unRar(filename, outputDir);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
switch (fileExt) {
|
switch (fileExt) {
|
||||||
case 'zip':
|
case 'zip':
|
||||||
files = await this.unZip(filename, outputDir);
|
files = await this.unZip(filename, outputDir);
|
||||||
@@ -119,17 +135,16 @@ class FileDecompressor {
|
|||||||
try {
|
try {
|
||||||
return await zip.unpack(filename, outputDir, {
|
return await zip.unpack(filename, outputDir, {
|
||||||
limitFileSize: this.limitFileSize,
|
limitFileSize: this.limitFileSize,
|
||||||
limitFileCount: 1000,
|
limitFileCount: 10000,
|
||||||
decodeEntryNameCallback: (nameRaw) => {
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
return utils.bufferRemoveZeroes(nameRaw);
|
return utils.bufferRemoveZeroes(nameRaw);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fs.emptyDir(outputDir);
|
fs.emptyDir(outputDir);
|
||||||
return await zip.unpack(filename, outputDir, {
|
return await zip.unpack(filename, outputDir, {
|
||||||
limitFileSize: this.limitFileSize,
|
limitFileSize: this.limitFileSize,
|
||||||
limitFileCount: 1000,
|
limitFileCount: 10000,
|
||||||
decodeEntryNameCallback: (nameRaw) => {
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
||||||
const enc = textUtils.getEncodingLite(nameRaw);
|
const enc = textUtils.getEncodingLite(nameRaw);
|
||||||
@@ -156,7 +171,7 @@ class FileDecompressor {
|
|||||||
|
|
||||||
if (this.limitFileSize) {
|
if (this.limitFileSize) {
|
||||||
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
||||||
reject('Файл слишком большой');
|
reject(new Error('Файл слишком большой'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +237,39 @@ class FileDecompressor {
|
|||||||
|
|
||||||
inputStream.pipe(stream).pipe(outputStream);
|
inputStream.pipe(stream).pipe(outputStream);
|
||||||
})().catch(reject); });
|
})().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) {
|
async gzipBuffer(buf) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class FileDetector {
|
|||||||
|
|
||||||
fromBuffer(buffer, callback) {
|
fromBuffer(buffer, callback) {
|
||||||
let result = null;
|
let result = null;
|
||||||
|
//console.log(buffer);
|
||||||
const invalidSignaturesList = this.validateSigantures();
|
const invalidSignaturesList = this.validateSigantures();
|
||||||
if (invalidSignaturesList.length) {
|
if (invalidSignaturesList.length) {
|
||||||
return callback(invalidSignaturesList);
|
return callback(invalidSignaturesList);
|
||||||
|
|||||||
@@ -676,7 +676,9 @@
|
|||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
{ "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) {
|
get(onPlaceChange) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.destroyed)
|
if (this.destroyed)
|
||||||
reject('destroyed');
|
reject(new Error('destroyed'));
|
||||||
|
|
||||||
const take = () => {
|
const take = () => {
|
||||||
if (this.freed <= 0)
|
if (this.freed <= 0)
|
||||||
@@ -73,7 +73,7 @@ class LimitedQueue {
|
|||||||
if (onPlaceChange)
|
if (onPlaceChange)
|
||||||
onPlaceChange(this.listeners.length);
|
onPlaceChange(this.listeners.length);
|
||||||
} else {
|
} 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 LimitedQueue = require('../../LimitedQueue');
|
||||||
const textUtils = require('./textUtils');
|
const textUtils = require('./textUtils');
|
||||||
const utils = require('../../utils');
|
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 {
|
class ConvertBase {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@@ -14,7 +15,6 @@ class ConvertBase {
|
|||||||
|
|
||||||
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
|
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
|
||||||
this.sofficePath = '/usr/bin/soffice';
|
this.sofficePath = '/usr/bin/soffice';
|
||||||
this.pdfToHtmlPath = '/usr/bin/pdftohtml';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {// eslint-disable-line no-unused-vars
|
async run(data, opts) {// eslint-disable-line no-unused-vars
|
||||||
@@ -27,9 +27,6 @@ class ConvertBase {
|
|||||||
|
|
||||||
if (!await fs.pathExists(this.sofficePath))
|
if (!await fs.pathExists(this.sofficePath))
|
||||||
throw new Error('Внешний конвертер LibreOffice не найден');
|
throw new Error('Внешний конвертер LibreOffice не найден');
|
||||||
|
|
||||||
if (!await fs.pathExists(this.pdfToHtmlPath))
|
|
||||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execConverter(path, args, onData, abort) {
|
async execConverter(path, args, onData, abort) {
|
||||||
@@ -42,22 +39,38 @@ class ConvertBase {
|
|||||||
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abort = (abort ? abort : () => false);
|
||||||
|
const myAbort = () => {
|
||||||
|
return q.abort() || abort();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (myAbort())
|
||||||
|
throw new Error('abort');
|
||||||
|
|
||||||
const result = await utils.spawnProcess(path, {
|
const result = await utils.spawnProcess(path, {
|
||||||
killAfter: 600,
|
killAfter: 3600,//1 час
|
||||||
args,
|
args,
|
||||||
onData: (data) => {
|
onData: (data) => {
|
||||||
q.resetTimeout();
|
if (queue.freed > 0)
|
||||||
|
q.resetTimeout();
|
||||||
onData(data);
|
onData(data);
|
||||||
},
|
},
|
||||||
abort
|
//будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
|
||||||
|
onUsage: (stats) => {
|
||||||
|
if (queue.freed > 0 && stats.cpu >= 10) {
|
||||||
|
q.resetTimeout();
|
||||||
|
onData('.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUsageInterval: 10,
|
||||||
|
abort: myAbort
|
||||||
});
|
});
|
||||||
if (result.code != 0) {
|
if (result.code != 0) {
|
||||||
let error = result.code;
|
const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||||
if (this.config.branch == 'development')
|
|
||||||
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
|
||||||
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e.status == 'killed') {
|
if (e.status == 'killed') {
|
||||||
throw new Error('Слишком долгое ожидание конвертера');
|
throw new Error('Слишком долгое ожидание конвертера');
|
||||||
@@ -90,62 +103,20 @@ class ConvertBase {
|
|||||||
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFb2(fb2) {
|
isDataXml(data) {
|
||||||
let out = '<?xml version="1.0" encoding="utf-8"?>';
|
const str = data.slice(0, 100).toString().trim();
|
||||||
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">';
|
return (str.indexOf('<?xml version="1.0"') == 0 || str.indexOf('<?xml version=\'1.0\'') == 0 );
|
||||||
out += this.formatFb2Node(fb2);
|
|
||||||
out += '</FictionBook>';
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFb2Node(node, name) {
|
formatFb2(fb2) {
|
||||||
let out = '';
|
const out = xmlParser.formatXml({
|
||||||
|
FictionBook: {
|
||||||
if (Array.isArray(node)) {
|
_attrs: {xmlns: 'http://www.gribuser.ru/xml/fictionbook/2.0', 'xmlns:l': 'http://www.w3.org/1999/xlink'},
|
||||||
for (const n of node) {
|
_a: [fb2],
|
||||||
out += this.formatFb2Node(n);
|
|
||||||
}
|
}
|
||||||
} else if (typeof node == 'string') {
|
}, 'utf-8', this.repSpaces);
|
||||||
if (name)
|
|
||||||
out += `<${name}>${this.repSpaces(node)}</${name}>`;
|
|
||||||
else
|
|
||||||
out += this.repSpaces(node);
|
|
||||||
} else {
|
|
||||||
if (node._n)
|
|
||||||
name = node._n;
|
|
||||||
|
|
||||||
let attrs = '';
|
return out.replace(/<p>\s*?<\/p>/g, '<empty-line/>');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 ConvertJpegPng = require('./ConvertJpegPng');
|
||||||
|
|
||||||
|
class ConvertDjvu extends ConvertJpegPng {
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 не найден');
|
||||||
|
|
||||||
|
const mogrifyPath = '/usr/bin/mogrify';
|
||||||
|
if (!await fs.pathExists(mogrifyPath))
|
||||||
|
throw new Error('Внешний конвертер mogrifyPath не найден');
|
||||||
|
|
||||||
|
const dir = `${inputFiles.filesDir}/`;
|
||||||
|
const baseFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
|
||||||
|
const tifFile = `${baseFile}.tif`;
|
||||||
|
|
||||||
|
//конвертируем в tiff
|
||||||
|
let perc = 0;
|
||||||
|
await this.execConverter(ddjvuPath, ['-format=tiff', `-quality=${tiffQuality}`, '-verbose', inputFiles.sourceFile, tifFile], () => {
|
||||||
|
perc = (perc < 100 ? perc + 1 : 40);
|
||||||
|
callback(perc);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
const tifFileSize = (await fs.stat(tifFile)).size;
|
||||||
|
let limitSize = 4*this.config.maxUploadFileSize;
|
||||||
|
if (tifFileSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| tifFileSize: ${tifFileSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//разбиваем на файлы
|
||||||
|
await this.execConverter(tiffsplitPath, [tifFile, dir], null, abort);
|
||||||
|
|
||||||
|
await fs.remove(tifFile);
|
||||||
|
|
||||||
|
//конвертируем в jpg
|
||||||
|
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') {
|
||||||
|
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);
|
||||||
|
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 = ConvertDjvu;
|
||||||
@@ -1,34 +1,71 @@
|
|||||||
const ConvertBase = require('./ConvertBase');
|
const ConvertBase = require('./ConvertBase');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
|
const textUtils = require('./textUtils');
|
||||||
|
|
||||||
class ConvertFb2 extends ConvertBase {
|
class ConvertFb2 extends ConvertBase {
|
||||||
check(data, opts) {
|
check(data, opts) {
|
||||||
const {dataType} = 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) {
|
async run(data, opts) {
|
||||||
if (!this.check(data, opts))
|
let newData = data.slice(0, 1024);
|
||||||
|
|
||||||
|
//Корректируем кодировку для проверки, 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 false;
|
||||||
|
|
||||||
return this.checkEncoding(data);
|
//Корректируем кодировку всего объема
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEncoding(data) {
|
checkEncoding(data) {
|
||||||
let result = 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) {
|
if (left >= 0) {
|
||||||
const right = data.indexOf('?>', left);
|
const right = data.indexOf('?>', left);
|
||||||
if (right >= 0) {
|
if (right >= 0) {
|
||||||
const head = data.slice(left, right + 2).toString();
|
const head = data.slice(left, right + 2).toString();
|
||||||
const m = head.match(/encoding="(.*?)"/);
|
const m = head.match(/encoding=['"](.*?)['"]/);
|
||||||
if (m) {
|
if (m) {
|
||||||
let encoding = m[1].toLowerCase();
|
let encoding = m[1].toLowerCase();
|
||||||
if (encoding != 'utf-8') {
|
if (encoding != 'utf-8') {
|
||||||
result = iconv.decode(data, encoding);
|
//encoding может не соответсвовать реальной кодировке файла, поэтому:
|
||||||
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
|
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');
|
const ConvertHtml = require('./ConvertHtml');
|
||||||
|
|
||||||
class ConvertDocX extends ConvertHtml {
|
class ConvertFb3 extends ConvertHtml {
|
||||||
async check(data, opts) {
|
async check(data, opts) {
|
||||||
const {inputFiles} = opts;
|
const {inputFiles} = opts;
|
||||||
if (this.config.useExternalBookConverter &&
|
if (this.config.useExternalBookConverter &&
|
||||||
@@ -39,13 +39,14 @@ class ConvertDocX extends ConvertHtml {
|
|||||||
const title = this.getTitle(text)
|
const title = this.getTitle(text)
|
||||||
.replace(/<\/?p>/g, '')
|
.replace(/<\/?p>/g, '')
|
||||||
;
|
;
|
||||||
text = `<title>${title}</title>` + text
|
text = `<fb2-title>${title}</fb2-title>` + text
|
||||||
.replace(/<title>/g, '<br><b>')
|
.replace(/<title>/g, '<br><b>')
|
||||||
.replace(/<\/title>/g, '</b><br>')
|
.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), {skipHtmlCheck: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ConvertDocX;
|
module.exports = ConvertFb3;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
const {dataType} = opts;
|
const {dataType} = opts;
|
||||||
|
|
||||||
//html?
|
//html?
|
||||||
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
if ( ( (dataType && (dataType.ext == 'html' || dataType.ext == 'xml')) ) || this.isDataXml(data) )
|
||||||
return {isText: false};
|
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};
|
return {isText: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,17 +24,14 @@ class ConvertHtml extends ConvertBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {
|
async run(data, opts) {
|
||||||
let isText = false;
|
let {isText = false, uploadFileName = ''} = opts;
|
||||||
if (!opts.skipCheck) {
|
if (!opts.skipHtmlCheck) {
|
||||||
const checkResult = this.check(data, opts);
|
const checkResult = this.check(data, opts);
|
||||||
if (!checkResult)
|
if (!checkResult)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
isText = checkResult.isText;
|
isText = checkResult.isText;
|
||||||
} else {
|
|
||||||
isText = opts.isText;
|
|
||||||
}
|
}
|
||||||
let {cutTitle} = opts;
|
|
||||||
|
|
||||||
let titleInfo = {};
|
let titleInfo = {};
|
||||||
let desc = {_n: 'description', 'title-info': titleInfo};
|
let desc = {_n: 'description', 'title-info': titleInfo};
|
||||||
@@ -44,12 +41,17 @@ class ConvertHtml extends ConvertBase {
|
|||||||
let fb2 = [desc, body, binary];
|
let fb2 = [desc, body, binary];
|
||||||
|
|
||||||
let title = '';
|
let title = '';
|
||||||
|
let author = '';
|
||||||
let inTitle = false;
|
let inTitle = false;
|
||||||
|
let inSectionTitle = false;
|
||||||
|
let inAuthor = false;
|
||||||
let inSubTitle = false;
|
let inSubTitle = false;
|
||||||
let inImage = false;
|
let inImage = false;
|
||||||
let image = {};
|
let image = {};
|
||||||
let bold = false;
|
let bold = false;
|
||||||
let italic = false;
|
let italic = false;
|
||||||
|
let superscript = false;
|
||||||
|
let subscript = false;
|
||||||
let begining = true;
|
let begining = true;
|
||||||
|
|
||||||
let spaceCounter = [];
|
let spaceCounter = [];
|
||||||
@@ -62,7 +64,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const growParagraph = (text) => {
|
const growParagraph = (text) => {
|
||||||
if (!pars.length)
|
if (!pars.length || pars[pars.length - 1]._n != 'p')
|
||||||
newParagraph();
|
newParagraph();
|
||||||
|
|
||||||
const l = pars.length;
|
const l = pars.length;
|
||||||
@@ -94,12 +96,16 @@ class ConvertHtml extends ConvertBase {
|
|||||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
text = this.escapeEntities(text);
|
text = this.escapeEntities(text);
|
||||||
|
|
||||||
if (!cutCounter && !(cutTitle && inTitle)) {
|
if (!(cutCounter || inTitle || inSectionTitle || inSubTitle)) {
|
||||||
let tOpen = '';
|
let tOpen = '';
|
||||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||||
tOpen += (bold ? '<strong>' : '');
|
tOpen += (bold ? '<strong>' : '');
|
||||||
tOpen += (italic ? '<emphasis>' : '');
|
tOpen += (italic ? '<emphasis>' : '');
|
||||||
|
tOpen += (superscript ? '<sup>' : '');
|
||||||
|
tOpen += (subscript ? '<sub>' : '');
|
||||||
let tClose = ''
|
let tClose = ''
|
||||||
|
tClose += (subscript ? '</sub>' : '');
|
||||||
|
tClose += (superscript ? '</sup>' : '');
|
||||||
tClose += (italic ? '</emphasis>' : '');
|
tClose += (italic ? '</emphasis>' : '');
|
||||||
tClose += (bold ? '</strong>' : '');
|
tClose += (bold ? '</strong>' : '');
|
||||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||||
@@ -110,12 +116,22 @@ class ConvertHtml extends ConvertBase {
|
|||||||
if (inTitle && !title)
|
if (inTitle && !title)
|
||||||
title = text;
|
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) {
|
if (inImage) {
|
||||||
image._t = text;
|
image._t = text;
|
||||||
binary.push(image);
|
binary.push(image);
|
||||||
|
|
||||||
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
|
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
|
||||||
newParagraph();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -140,15 +156,27 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = true;
|
bold = true;
|
||||||
break;
|
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;
|
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;
|
inSubTitle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +184,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
inImage = true;
|
inImage = true;
|
||||||
const attrs = sax.getAttrsSync(tail);
|
const attrs = sax.getAttrsSync(tail);
|
||||||
image = {_n: 'binary', _attrs: {id: attrs.name.value, 'content-type': attrs.type.value}, _t: ''};
|
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
|
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
@@ -179,12 +207,26 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = false;
|
bold = false;
|
||||||
break;
|
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;
|
inTitle = false;
|
||||||
|
|
||||||
if (tag == 'subtitle')
|
if (tag == 'fb2-author') {
|
||||||
|
inAuthor = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'fb2-section-title') {
|
||||||
|
inSectionTitle = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'fb2-subtitle')
|
||||||
inSubTitle = false;
|
inSubTitle = false;
|
||||||
|
|
||||||
if (tag == 'fb2-image')
|
if (tag == 'fb2-image')
|
||||||
@@ -195,10 +237,20 @@ class ConvertHtml extends ConvertBase {
|
|||||||
|
|
||||||
sax.parseSync(buf, {
|
sax.parseSync(buf, {
|
||||||
onStartNode, onEndNode, onTextNode,
|
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'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!title)
|
||||||
|
title = uploadFileName;
|
||||||
|
|
||||||
titleInfo['book-title'] = title;
|
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)) {
|
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
@@ -218,7 +270,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
//если разброс не слишком большой, выделяем параграфы
|
//если разброс не слишком большой, выделяем параграфы
|
||||||
if (d < 10 && spaceCounter.length) {
|
if (d < 20 && spaceCounter.length) {
|
||||||
total /= 20;
|
total /= 20;
|
||||||
i = spaceCounter.length - 1;
|
i = spaceCounter.length - 1;
|
||||||
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
||||||
@@ -228,56 +280,49 @@ class ConvertHtml extends ConvertBase {
|
|||||||
if (parIndent > 2) parIndent--;
|
if (parIndent > 2) parIndent--;
|
||||||
|
|
||||||
let newPars = [];
|
let newPars = [];
|
||||||
|
let curPar = {};
|
||||||
const newPar = () => {
|
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) {
|
for (const par of pars) {
|
||||||
if (par._n != 'p') {
|
if (par._n != 'p') {
|
||||||
newPars.push(par);
|
newPars.push(par);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i > 0)
|
newPar();
|
||||||
newPar();
|
|
||||||
i++;
|
|
||||||
|
|
||||||
let j = 0;
|
|
||||||
const lines = par._t.split('\n');
|
const lines = par._t.split('\n');
|
||||||
for (let line of lines) {
|
for (let j = 0; j < lines.length; j++) {
|
||||||
line = repCrLfTab(line);
|
const line = repCrLfTab(lines[j]);
|
||||||
|
|
||||||
let l = 0;
|
let l = 0;
|
||||||
while (l < line.length && line[l] == ' ') {
|
while (l < line.length && line[l] == ' ') {
|
||||||
l++;
|
l++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l >= parIndent || line == '') {
|
if (j > 0 &&
|
||||||
if (j > 0)
|
(l >= parIndent ||
|
||||||
newPar();
|
(j < lines.length - 1 && line == '')
|
||||||
j++;
|
)
|
||||||
|
) {
|
||||||
|
newPar();
|
||||||
}
|
}
|
||||||
growPar(line.trim() + ' ');
|
|
||||||
|
curPar._t += line.trim() + ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.section._a[0] = newPars;
|
body.section._a[0] = newPars;
|
||||||
} else {
|
|
||||||
body.section._a[0] = pars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
|
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
|
||||||
bold = false;
|
bold = false;
|
||||||
italic = false;
|
italic = false;
|
||||||
|
superscript = false;
|
||||||
|
subscript = false;
|
||||||
inSubTitle = false;
|
inSubTitle = false;
|
||||||
pars = body.section._a[0];
|
pars = body.section._a[0];
|
||||||
for (let i = 0; i < pars.length; i++) {
|
for (let i = 0; i < pars.length; i++) {
|
||||||
@@ -297,7 +342,11 @@ class ConvertHtml extends ConvertBase {
|
|||||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||||
tOpen += (bold ? '<strong>' : '');
|
tOpen += (bold ? '<strong>' : '');
|
||||||
tOpen += (italic ? '<emphasis>' : '');
|
tOpen += (italic ? '<emphasis>' : '');
|
||||||
|
tOpen += (superscript ? '<sup>' : '');
|
||||||
|
tOpen += (subscript ? '<sub>' : '');
|
||||||
let tClose = ''
|
let tClose = ''
|
||||||
|
tClose += (subscript ? '</sub>' : '');
|
||||||
|
tClose += (superscript ? '</sup>' : '');
|
||||||
tClose += (italic ? '</emphasis>' : '');
|
tClose += (italic ? '</emphasis>' : '');
|
||||||
tClose += (bold ? '</strong>' : '');
|
tClose += (bold ? '</strong>' : '');
|
||||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||||
@@ -313,6 +362,10 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = true;
|
bold = true;
|
||||||
if (tag == 'emphasis')
|
if (tag == 'emphasis')
|
||||||
italic = true;
|
italic = true;
|
||||||
|
if (tag == 'sup')
|
||||||
|
superscript = true;
|
||||||
|
if (tag == 'sub')
|
||||||
|
subscript = true;
|
||||||
if (tag == 'subtitle')
|
if (tag == 'subtitle')
|
||||||
inSubTitle = true;
|
inSubTitle = true;
|
||||||
}
|
}
|
||||||
@@ -322,6 +375,10 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = false;
|
bold = false;
|
||||||
if (tag == 'emphasis')
|
if (tag == 'emphasis')
|
||||||
italic = false;
|
italic = false;
|
||||||
|
if (tag == 'sup')
|
||||||
|
superscript = false;
|
||||||
|
if (tag == 'sub')
|
||||||
|
subscript = false;
|
||||||
if (tag == 'subtitle')
|
if (tag == 'subtitle')
|
||||||
inSubTitle = false;
|
inSubTitle = false;
|
||||||
}
|
}
|
||||||
|
|||||||
100
server/core/Reader/BookConverter/ConvertJpegPng.js
Normal file
100
server/core/Reader/BookConverter/ConvertJpegPng.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
//const utils = require('../../utils');
|
||||||
|
|
||||||
|
const ConvertBase = require('./ConvertBase');
|
||||||
|
|
||||||
|
class ConvertJpegPng extends ConvertBase {
|
||||||
|
check(data, opts) {
|
||||||
|
const {inputFiles} = opts;
|
||||||
|
|
||||||
|
return this.config.useExternalBookConverter &&
|
||||||
|
inputFiles.sourceFileType &&
|
||||||
|
(inputFiles.sourceFileType.ext == 'jpg' || inputFiles.sourceFileType.ext == 'png' );
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(data, opts) {
|
||||||
|
const {inputFiles, uploadFileName, imageFiles} = opts;
|
||||||
|
|
||||||
|
if (!imageFiles) {
|
||||||
|
if (!this.check(data, opts))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
if (imageFiles) {
|
||||||
|
files = imageFiles;
|
||||||
|
} else {
|
||||||
|
const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`;
|
||||||
|
await fs.copy(inputFiles.sourceFile, imageFile);
|
||||||
|
files.push({src: imageFile});
|
||||||
|
}
|
||||||
|
|
||||||
|
//читаем изображения
|
||||||
|
const limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
let imagesSize = 0;
|
||||||
|
|
||||||
|
const loadImage = async(image) => {
|
||||||
|
const src = path.parse(image.src);
|
||||||
|
let type = 'unknown';
|
||||||
|
switch (src.ext) {
|
||||||
|
case '.jpg': type = 'image/jpeg'; break;
|
||||||
|
case '.png': type = 'image/png'; break;
|
||||||
|
}
|
||||||
|
if (type != 'unknown') {
|
||||||
|
image.data = (await fs.readFile(image.src)).toString('base64');
|
||||||
|
image.type = type;
|
||||||
|
image.name = src.base;
|
||||||
|
|
||||||
|
imagesSize += image.data.length;
|
||||||
|
if (imagesSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let images = [];
|
||||||
|
let loading = [];
|
||||||
|
files.forEach(img => {
|
||||||
|
images.push(img);
|
||||||
|
loading.push(loadImage(img));
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (image.type) {
|
||||||
|
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: attrs});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pars.push({_n: 'p', _t: ''});
|
||||||
|
|
||||||
|
return this.formatFb2(fb2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConvertJpegPng;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//const _ = require('lodash');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@@ -14,17 +15,24 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(notUsed, opts) {
|
async run(notUsed, opts) {
|
||||||
if (!this.check(notUsed, opts))
|
if (!opts.pdfAsText || !this.check(notUsed, opts))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
await this.checkExternalConverterPresent();
|
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 pdftohtmlPath = '/usr/bin/pdftohtml';
|
||||||
|
if (!await fs.pathExists(pdftohtmlPath))
|
||||||
|
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||||
|
|
||||||
//конвертируем в xml
|
//конвертируем в xml
|
||||||
let perc = 0;
|
let perc = 0;
|
||||||
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
|
await this.execConverter(pdftohtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
|
||||||
perc = (perc < 80 ? perc + 10 : 40);
|
perc = (perc < 80 ? perc + 10 : 40);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
}, abort);
|
}, abort);
|
||||||
@@ -33,17 +41,24 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
const data = await fs.readFile(outFile);
|
const data = await fs.readFile(outFile);
|
||||||
callback(90);
|
callback(90);
|
||||||
|
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
//парсим xml
|
//парсим xml
|
||||||
let lines = [];
|
let lines = [];
|
||||||
|
let pagelines = [];
|
||||||
|
let line = {text: ''};
|
||||||
|
let page = {};
|
||||||
|
let fonts = {};
|
||||||
|
let sectionTitleFound = false;
|
||||||
|
|
||||||
let images = [];
|
let images = [];
|
||||||
let loading = [];
|
let loading = [];
|
||||||
|
|
||||||
let inText = false;
|
let inText = false;
|
||||||
let bold = false;
|
let bold = false;
|
||||||
let italic = false;
|
let italic = false;
|
||||||
let title = '';
|
|
||||||
let prevTop = 0;
|
|
||||||
let i = -1;
|
let i = -1;
|
||||||
let titleCount = 0;
|
|
||||||
|
|
||||||
const loadImage = async(image) => {
|
const loadImage = async(image) => {
|
||||||
const src = path.parse(image.src);
|
const src = path.parse(image.src);
|
||||||
@@ -57,7 +72,7 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
image.type = type;
|
image.type = type;
|
||||||
image.name = src.base;
|
image.name = src.base;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const putImage = (curTop) => {
|
const putImage = (curTop) => {
|
||||||
if (!isNaN(curTop) && images.length) {
|
if (!isNaN(curTop) && images.length) {
|
||||||
@@ -67,7 +82,72 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
images.shift();
|
images.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const isTextBold = (text) => {
|
||||||
|
const m = text.trim().match(/^<b>(.*)<\/b>$/);
|
||||||
|
return m && !m[1].match(/<b>|<\/b>|<i>|<\/i>/g);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTextEmpty = (text) => {
|
||||||
|
return text.replace(/<b>|<\/b>|<i>|<\/i>/g, '').trim() == '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const putPageLines = () => {
|
||||||
|
pagelines.sort((a, b) => (Math.abs(a.top - b.top) > 3 ? a.top - b.top : 0)*10000 + (a.left - b.left))
|
||||||
|
|
||||||
|
//объединяем в одну строку равные по высоте
|
||||||
|
const pl = [];
|
||||||
|
let pt = 0;
|
||||||
|
let j = -1;
|
||||||
|
pagelines.forEach(line => {
|
||||||
|
if (isTextEmpty(line.text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
//проверим, возможно это заголовок
|
||||||
|
if (line.fontId && line.pageWidth) {
|
||||||
|
const centerLeft = (line.pageWidth - line.width)/2;
|
||||||
|
if (isTextBold(line.text) && Math.abs(centerLeft - line.left) < 10) {
|
||||||
|
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.fontId ? fonts[prevLine.fontId] : (line.fontId ? fonts[line.fontId] : 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 onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
if (!cutCounter && inText) {
|
if (!cutCounter && inText) {
|
||||||
@@ -76,67 +156,80 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
let tClose = (italic ? '</i>' : '');
|
let tClose = (italic ? '</i>' : '');
|
||||||
tClose += (bold ? '</b>' : '');
|
tClose += (bold ? '</b>' : '');
|
||||||
|
|
||||||
lines[i].text += `${tOpen}${text}${tClose} `;
|
line.text += ` ${tOpen}${text}${tClose}`;
|
||||||
if (titleCount < 2 && text.trim() != '') {
|
|
||||||
title += text + (titleCount ? '' : ' - ');
|
|
||||||
titleCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
if (!cutCounter) {
|
if (inText) {
|
||||||
if (inText) {
|
switch (tag) {
|
||||||
switch (tag) {
|
case 'i':
|
||||||
case 'i':
|
italic = true;
|
||||||
italic = true;
|
break;
|
||||||
break;
|
case 'b':
|
||||||
case 'b':
|
bold = true;
|
||||||
bold = true;
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tag == 'text' && !inText) {
|
if (tag == 'page') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
const attrs = sax.getAttrsSync(tail);
|
||||||
const line = {
|
page = {
|
||||||
text: '',
|
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||||
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),
|
putPageLines();
|
||||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
}
|
||||||
|
|
||||||
|
if (tag == 'fontspec') {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
|
||||||
|
const fontSize = (attrs.size && attrs.size.value ? attrs.size.value : '');
|
||||||
|
|
||||||
|
if (fontId) {
|
||||||
|
fonts[fontId] = {fontSize};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'text' && !inText) {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
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),
|
||||||
|
isSectionTitle: false,
|
||||||
|
isSubtitle: false,
|
||||||
|
pageWidth: page.width,
|
||||||
|
fontId: (attrs.font && attrs.font.value ? attrs.font.value : ''),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (line.width != 0 || line.height != 0) {
|
||||||
|
inText = true;
|
||||||
|
pagelines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'image') {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
let src = (attrs.src && attrs.src.value ? attrs.src.value : '');
|
||||||
|
if (src) {
|
||||||
|
const image = {
|
||||||
|
isImage: true,
|
||||||
|
src,
|
||||||
|
data: '',
|
||||||
|
type: '',
|
||||||
|
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
|
||||||
|
left: parseInt((attrs.left && attrs.left.value ? attrs.left.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,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (line.width != 0 || line.height != 0) {
|
loading.push(loadImage(image));
|
||||||
inText = true;
|
images.push(image);
|
||||||
if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) {
|
images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
|
||||||
putImage(line.top);
|
|
||||||
i++;
|
|
||||||
lines[i] = line;
|
|
||||||
}
|
|
||||||
prevTop = line.top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag == 'image') {
|
|
||||||
const attrs = sax.getAttrsSync(tail);
|
|
||||||
const src = (attrs.src && attrs.src.value ? attrs.src.value : '');
|
|
||||||
if (src) {
|
|
||||||
const image = {
|
|
||||||
isImage: true,
|
|
||||||
src,
|
|
||||||
data: '',
|
|
||||||
type: '',
|
|
||||||
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
|
|
||||||
};
|
|
||||||
loading.push(loadImage(image));
|
|
||||||
images.push(image);
|
|
||||||
images.sort((a, b) => a.top - b.top)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag == 'page') {
|
|
||||||
putImage(100000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -162,9 +255,10 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
onStartNode, onEndNode, onTextNode
|
onStartNode, onEndNode, onTextNode
|
||||||
});
|
});
|
||||||
|
|
||||||
putImage(100000);
|
putPageLines();
|
||||||
|
|
||||||
await Promise.all(loading);
|
await Promise.all(loading);
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
//найдем параграфы и отступы
|
//найдем параграфы и отступы
|
||||||
const indents = [];
|
const indents = [];
|
||||||
@@ -185,16 +279,49 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
}
|
}
|
||||||
indents[0] = 0;
|
indents[0] = 0;
|
||||||
|
|
||||||
|
//author & title
|
||||||
|
let {author, title} = await this.getPdfTitleAndAuthor(inpFile);
|
||||||
|
|
||||||
|
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 concat = '';
|
||||||
let sp = '';
|
let sp = '';
|
||||||
|
let firstLine = true;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
if (text.length > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (line.isImage) {
|
if (line.isImage) {
|
||||||
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.isSectionTitle) {
|
||||||
|
if (firstLine)
|
||||||
|
text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
|
||||||
|
else
|
||||||
|
text += `<fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstLine = false;
|
||||||
|
|
||||||
|
if (line.isSubtitle) {
|
||||||
|
text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (concat == '') {
|
if (concat == '') {
|
||||||
const left = line.left || 0;
|
const left = line.left || 0;
|
||||||
sp = ' '.repeat(indents[left]);
|
sp = ' '.repeat(indents[left]);
|
||||||
@@ -212,8 +339,36 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
if (concat)
|
if (concat)
|
||||||
text += sp + concat + "\n";
|
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), {skipHtmlCheck: true, isText: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPdfTitleAndAuthor(pdfFile) {
|
||||||
|
const result = {author: '', title: ''};
|
||||||
|
|
||||||
|
const pdfinfoPath = '/usr/bin/pdfinfo';
|
||||||
|
|
||||||
|
if (!await fs.pathExists(pdfinfoPath))
|
||||||
|
throw new Error('Внешний конвертер pdfinfo не найден');
|
||||||
|
|
||||||
|
const execResult = await this.execConverter(pdfinfoPath, [pdfFile]);
|
||||||
|
|
||||||
|
const titlePrefix = 'Title:';
|
||||||
|
const authorPrefix = 'Author:';
|
||||||
|
|
||||||
|
const stdout = execResult.stdout.split("\n");
|
||||||
|
stdout.forEach(line => {
|
||||||
|
if (line.indexOf(titlePrefix) == 0)
|
||||||
|
result.title = line.substring(titlePrefix.length).trim();
|
||||||
|
|
||||||
|
if (line.indexOf(authorPrefix) == 0)
|
||||||
|
result.author = line.substring(authorPrefix.length).trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = ConvertPdf;
|
module.exports = ConvertPdf;
|
||||||
|
|||||||
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)
|
if (text === false)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle(text) {
|
getTitle(text) {
|
||||||
@@ -79,7 +79,7 @@ class ConvertSites extends ConvertHtml {
|
|||||||
let book = this.getTitle(text);
|
let book = this.getTitle(text);
|
||||||
book = book.replace(' (fb2) | Флибуста', '');
|
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">';
|
let begin = '<h3 class="book">';
|
||||||
if (text.indexOf(begin) <= 0)
|
if (text.indexOf(begin) <= 0)
|
||||||
@@ -95,12 +95,12 @@ class ConvertSites extends ConvertHtml {
|
|||||||
return text.substring(l, r)
|
return text.substring(l, r)
|
||||||
.replace(/blockquote class="?book"?/g, 'p')
|
.replace(/blockquote class="?book"?/g, 'p')
|
||||||
.replace(/<br\/?>\s*<\/h3>/g, '</h3>')
|
.replace(/<br\/?>\s*<\/h3>/g, '</h3>')
|
||||||
.replace(/<h3 class="?book"?>/g, '<br><br><subtitle>')
|
.replace(/<h3 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<h5 class="?book"?>/g, '<br><br><subtitle>')
|
.replace(/<h5 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<h3>/g, '<br><br><subtitle>')
|
.replace(/<h3>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<h5>/g, '<br><br><subtitle>')
|
.replace(/<h5>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<\/h3>/g, '</subtitle><br>')
|
.replace(/<\/h3>/g, '</fb2-subtitle><br>')
|
||||||
.replace(/<\/h5>/g, '</subtitle><br>')
|
.replace(/<\/h5>/g, '</fb2-subtitle><br>')
|
||||||
.replace(/<div class="?stanza"?>/g, '<br>')
|
.replace(/<div class="?stanza"?>/g, '<br>')
|
||||||
.replace(/<div>/g, '<br>')
|
.replace(/<div>/g, '<br>')
|
||||||
+ title;
|
+ title;
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ const FileDetector = require('../../FileDetector');
|
|||||||
|
|
||||||
//порядок важен
|
//порядок важен
|
||||||
const convertClassFactory = [
|
const convertClassFactory = [
|
||||||
|
require('./ConvertJpegPng'),
|
||||||
require('./ConvertEpub'),
|
require('./ConvertEpub'),
|
||||||
|
require('./ConvertDjvu'),
|
||||||
require('./ConvertPdf'),
|
require('./ConvertPdf'),
|
||||||
|
require('./ConvertPdfImages'),
|
||||||
require('./ConvertRtf'),
|
require('./ConvertRtf'),
|
||||||
require('./ConvertDocX'),
|
require('./ConvertDocX'),
|
||||||
require('./ConvertFb3'),
|
require('./ConvertFb3'),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const utils = require('../utils');
|
|||||||
const log = new (require('../AppLogger'))().log;//singleton
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
const cleanDirPeriod = 60*60*1000;//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;
|
let instance = null;
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class ReaderWorker {
|
|||||||
|
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
this.down = new FileDownloader(config.maxUploadFileSize);
|
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.bookConverter = new BookConverter(this.config);
|
||||||
|
|
||||||
this.remoteWebDavStorage = false;
|
this.remoteWebDavStorage = false;
|
||||||
@@ -81,7 +81,7 @@ class ReaderWorker {
|
|||||||
const decompDirname = utils.randomHexString(30);
|
const decompDirname = utils.randomHexString(30);
|
||||||
|
|
||||||
//download or use uploaded
|
//download or use uploaded
|
||||||
if (url.indexOf('file://') != 0) {//download
|
if (url.indexOf('disk://') != 0) {//download
|
||||||
const downdata = await this.down.load(url, (progress) => {
|
const downdata = await this.down.load(url, (progress) => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
}, q.abort);
|
}, q.abort);
|
||||||
@@ -130,6 +130,8 @@ class ReaderWorker {
|
|||||||
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
||||||
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
|
if (queue.freed > 0)
|
||||||
|
q.resetTimeout();
|
||||||
}, q.abort);
|
}, q.abort);
|
||||||
|
|
||||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||||
@@ -171,9 +173,15 @@ class ReaderWorker {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
if (e.message == 'abort')
|
let mes = e.message.split('|FORLOG|');
|
||||||
e.message = overLoadMes;
|
if (mes[1])
|
||||||
wState.set({state: 'error', error: e.message});
|
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 {
|
} finally {
|
||||||
//clean
|
//clean
|
||||||
if (q)
|
if (q)
|
||||||
@@ -208,7 +216,7 @@ class ReaderWorker {
|
|||||||
await fs.remove(file.path);
|
await fs.remove(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `file://${hash}`;
|
return `disk://${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreRemoteFile(filename) {
|
async restoreRemoteFile(filename) {
|
||||||
@@ -266,30 +274,38 @@ class ReaderWorker {
|
|||||||
files.push({name, stat});
|
files.push({name, stat});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files`);
|
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
|
||||||
|
|
||||||
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
let j = 0;
|
||||||
while (i < files.length && size > maxSize) {
|
while (i < files.length && size > maxSize) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const oldFile = `${dir}/${file.name}`;
|
const oldFile = `${dir}/${file.name}`;
|
||||||
|
|
||||||
|
let remoteSuccess = true;
|
||||||
//отправляем только this.config.tempPublicDir
|
//отправляем только this.config.tempPublicDir
|
||||||
//TODO: убрать в будущем, т.к. уже делается ленивое сохранение compFilename в удаленном хранилище
|
|
||||||
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
|
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
|
||||||
|
remoteSuccess = false;
|
||||||
try {
|
try {
|
||||||
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
|
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
|
||||||
await this.remoteWebDavStorage.putFile(oldFile);
|
await this.remoteWebDavStorage.putFile(oldFile);
|
||||||
|
remoteSuccess = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await fs.remove(oldFile);
|
//реально удаляем только если сохранили в хранилище
|
||||||
|
if (remoteSuccess || size > maxSize*1.2) {
|
||||||
|
await fs.remove(oldFile);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
size -= file.stat.size;
|
size -= file.stat.size;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
log(`removed ${i} files`);
|
log(`removed ${j} files`);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ class ZipStreamer {
|
|||||||
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
||||||
const entries = Object.values(unzip.entries());
|
const entries = Object.values(unzip.entries());
|
||||||
if (limitFileCount && entries.length > limitFileCount) {
|
if (limitFileCount && entries.length > limitFileCount) {
|
||||||
reject('Слишком много файлов');
|
reject(new Error('Слишком много файлов'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
||||||
reject('Файл слишком большой');
|
reject(new Error('Файл слишком большой'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ function parseSync(xstr, options) {
|
|||||||
onCdata: _onCdata = dummy,
|
onCdata: _onCdata = dummy,
|
||||||
onComment: _onComment = dummy,
|
onComment: _onComment = dummy,
|
||||||
onProgress: _onProgress = dummy,
|
onProgress: _onProgress = dummy,
|
||||||
innerCut = new Set()
|
innerCut = new Set(),
|
||||||
|
lowerCase = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -91,7 +92,8 @@ function parseSync(xstr, options) {
|
|||||||
} else {
|
} else {
|
||||||
tag = tagData;
|
tag = tagData;
|
||||||
}
|
}
|
||||||
tag = tag.toLowerCase();
|
if (lowerCase)
|
||||||
|
tag = tag.toLowerCase();
|
||||||
|
|
||||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||||
if (!cutCounter)
|
if (!cutCounter)
|
||||||
@@ -146,7 +148,8 @@ async function parse(xstr, options) {
|
|||||||
onCdata: _onCdata = dummy,
|
onCdata: _onCdata = dummy,
|
||||||
onComment: _onComment = dummy,
|
onComment: _onComment = dummy,
|
||||||
onProgress: _onProgress = dummy,
|
onProgress: _onProgress = dummy,
|
||||||
innerCut = new Set()
|
innerCut = new Set(),
|
||||||
|
lowerCase = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -231,7 +234,8 @@ async function parse(xstr, options) {
|
|||||||
} else {
|
} else {
|
||||||
tag = tagData;
|
tag = tagData;
|
||||||
}
|
}
|
||||||
tag = tag.toLowerCase();
|
if (lowerCase)
|
||||||
|
tag = tag.toLowerCase();
|
||||||
|
|
||||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||||
if (!cutCounter)
|
if (!cutCounter)
|
||||||
@@ -276,7 +280,7 @@ async function parse(xstr, options) {
|
|||||||
await _onProgress(100);
|
await _onProgress(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAttrsSync(tail) {
|
function getAttrsSync(tail, lowerCase = true) {
|
||||||
let result = {};
|
let result = {};
|
||||||
let name = '';
|
let name = '';
|
||||||
let value = '';
|
let value = '';
|
||||||
@@ -287,13 +291,16 @@ function getAttrsSync(tail) {
|
|||||||
let waitEq = false;
|
let waitEq = false;
|
||||||
|
|
||||||
const pushResult = () => {
|
const pushResult = () => {
|
||||||
|
if (lowerCase)
|
||||||
|
name = name.toLowerCase();
|
||||||
if (name != '') {
|
if (name != '') {
|
||||||
|
const fn = name;
|
||||||
let ns = '';
|
let ns = '';
|
||||||
if (name.indexOf(':') >= 0) {
|
if (fn.indexOf(':') >= 0) {
|
||||||
[ns, name] = name.split(':');
|
[ns, name] = fn.split(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
result[name] = {value, ns};
|
result[name] = {value, ns, fn};
|
||||||
}
|
}
|
||||||
name = '';
|
name = '';
|
||||||
value = '';
|
value = '';
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const baseX = require('base-x');
|
const baseX = require('base-x');
|
||||||
|
const pidusage = require('pidusage');
|
||||||
|
|
||||||
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||||
const bs36 = baseX(BASE36);
|
const bs36 = baseX(BASE36);
|
||||||
@@ -45,10 +47,11 @@ async function touchFile(filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function spawnProcess(cmd, opts) {
|
function spawnProcess(cmd, opts) {
|
||||||
let {args, killAfter, onData, abort} = opts;
|
let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts;
|
||||||
killAfter = (killAfter ? killAfter : 120);//seconds
|
killAfter = (killAfter ? killAfter : 120);//seconds
|
||||||
onData = (onData ? onData : () => {});
|
onData = (onData ? onData : () => {});
|
||||||
args = (args ? args : []);
|
args = (args ? args : []);
|
||||||
|
onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds
|
||||||
|
|
||||||
return new Promise((resolve, reject) => { (async() => {
|
return new Promise((resolve, reject) => { (async() => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
@@ -75,9 +78,19 @@ function spawnProcess(cmd, opts) {
|
|||||||
reject({status: 'error', error, stdout, stderr});
|
reject({status: 'error', error, stdout, stderr});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//ждем процесс, контролируем его работу раз в секунду
|
||||||
|
let onUsageCounter = onUsageInterval;
|
||||||
while (!resolved) {
|
while (!resolved) {
|
||||||
await sleep(1000);
|
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())) {
|
if (killAfter <= 0 || (abort && abort())) {
|
||||||
process.kill(proc.pid);
|
process.kill(proc.pid);
|
||||||
if (killAfter <= 0) {
|
if (killAfter <= 0) {
|
||||||
@@ -91,6 +104,22 @@ function spawnProcess(cmd, opts) {
|
|||||||
})().catch(reject); });
|
})().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 = {
|
module.exports = {
|
||||||
toBase36,
|
toBase36,
|
||||||
fromBase36,
|
fromBase36,
|
||||||
@@ -99,5 +128,6 @@ module.exports = {
|
|||||||
sleep,
|
sleep,
|
||||||
randomHexString,
|
randomHexString,
|
||||||
touchFile,
|
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