Compare commits

...

83 Commits

Author SHA1 Message Date
Book Pauk
403b9c0508 Merge branch 'release/0.9.10-1' 2020-12-04 18:37:58 +07:00
Book Pauk
ee8ba75371 Переделывание конвертера Djvu, теперь работает быстрее и без промежуточного конвертирования в pdf 2020-12-04 18:24:08 +07:00
Book Pauk
a2773fb180 Поправил readme 2020-12-04 18:23:57 +07:00
Book Pauk
ca36d588fc Merge tag '0.9.10' into develop
0.9.10
2020-12-03 22:05:47 +07:00
Book Pauk
1e65707b7f Merge branch 'release/0.9.10' 2020-12-03 22:05:42 +07:00
Book Pauk
eddf34ce55 Мелкая поправка 2020-12-03 22:04:47 +07:00
Book Pauk
0fb43aa33c Версия 0.9.10 2020-12-03 21:41:23 +07:00
Book Pauk
b273b02da4 Убрал "k" в статусбаре 2020-12-03 21:37:58 +07:00
Book Pauk
0b997f9673 Поправка приветственного сообщения 2020-12-03 21:37:27 +07:00
Book Pauk
bdb2ae57a8 Добавлена частичная поддержка формата Djvu 2020-12-03 21:30:50 +07:00
Book Pauk
b5e563679a Мелкая поправка 2020-12-03 20:00:22 +07:00
Book Pauk
992c104262 Удален ненужный файл 2020-12-03 19:57:27 +07:00
Book Pauk
555154031e Добавлен запрос persistent storage, увеличен размер кэша книг 2020-12-03 19:53:08 +07:00
Book Pauk
acb083e429 Поправлен readme 2020-12-03 19:30:27 +07:00
Book Pauk
4a527d192d Улучшено управление внешними конвертерами 2020-12-03 19:04:34 +07:00
Book Pauk
39c3bf17dd Добавлен пакет pidusage 2020-12-03 18:43:57 +07:00
Book Pauk
afc8c84f41 Мелкие поправки таймаутов 2020-12-03 12:39:16 +07:00
Book Pauk
a085e04c4d Поправки параметров запуска Rar 2020-12-03 10:24:47 +07:00
Book Pauk
2f82b0db34 Добавлена поддержка rar-архивов с помощью внешнего архиватора 2020-12-02 23:56:17 +07:00
Book Pauk
0124c2b17d Поправка цели dev 2020-12-02 23:55:23 +07:00
Book Pauk
d2cfbbc9f3 Поправлен триггер сокрытия диалога 2020-12-02 22:24:11 +07:00
Book Pauk
c59f48822c Префикс "file://" заменен на "disk://", т.к. порождается конфликт с браузерным протоколом file:// 2020-12-02 22:01:36 +07:00
Book Pauk
b2d6584c4a Merge tag '0.9.9-2' into develop
0.9.9-2
2020-11-24 04:53:10 +07:00
Book Pauk
8f7cafb240 Merge branch 'release/0.9.9-2' 2020-11-24 04:52:55 +07:00
Book Pauk
08fd0f15ff Улучшено распознавание параграфов чистого текста 2020-11-24 04:49:18 +07:00
Book Pauk
dbb1bfe587 Поправки распознавания кодировки fb2-файла 2020-11-24 02:09:17 +07:00
Book Pauk
fe4b7a5a85 Улучшено распознавание xml-формата 2020-11-23 23:49:20 +07:00
Book Pauk
d8df5d76e5 Поправка ридми 2020-11-23 21:57:52 +07:00
Book Pauk
b65dcc5ade Добавлен параметр -nodrm 2020-11-23 21:49:19 +07:00
Book Pauk
a5c387a19e Поправка строки запуска сервера 2020-11-23 21:35:45 +07:00
Book Pauk
07c38d9a9f Поправка для журналирования ошибок конвертера 2020-11-23 21:19:04 +07:00
Book Pauk
20ac8a444b Merge tag '0.9.9-1' into develop
0.9.9-1
2020-11-21 13:36:15 +07:00
Book Pauk
7b601c9c7f Merge branch 'release/0.9.9-1' 2020-11-21 13:36:11 +07:00
Book Pauk
8d2f74daa4 Небольшие доделки в связи с оптимизацией загрузки списка недавних 2020-11-21 13:34:58 +07:00
Book Pauk
01e82dca5f Merge tag '0.9.9' into develop
0.9.9
2020-11-21 04:06:54 +07:00
Book Pauk
094bb407ed Merge branch 'release/0.9.9' 2020-11-21 04:06:49 +07:00
Book Pauk
338baa55ec Версия 0.9.9 2020-11-21 04:06:16 +07:00
Book Pauk
d06d20a33e Оптимизация загрузки и хранения списка недавних книг 2020-11-21 04:03:54 +07:00
Book Pauk
d46ba6b92b Мелкий рефакторинг 2020-11-21 01:13:30 +07:00
Book Pauk
ec2639039d Поправка бага 2020-11-20 21:57:54 +07:00
Book Pauk
3a211ded2e Мелкий рефакторинг, добавлено удобочитаемое имя при сохраненнии fb2 2020-11-20 21:43:37 +07:00
Book Pauk
c2131e3654 Доработки парсинга fb2 - распознаем тег sequence 2020-11-20 21:26:16 +07:00
Book Pauk
594fb59395 Убрал лишнее 2020-11-20 20:45:47 +07:00
Book Pauk
f44378ec84 Рефакторинг 2020-11-20 18:25:24 +07:00
Book Pauk
0f6b366f62 Расширил лог 2020-11-20 16:17:54 +07:00
Book Pauk
8d0a5997ee Небольшая поправка 2020-11-20 16:06:23 +07:00
Book Pauk
347cb3417e Поправки названий и справки 2020-11-19 15:20:40 +07:00
Book Pauk
337fcebd10 Merge tag '0.9.8-5' into develop
0.9.8-5
2020-11-15 14:57:27 +07:00
Book Pauk
e057b130e9 Merge branch 'release/0.9.8-5' 2020-11-15 14:57:23 +07:00
Book Pauk
19a0765a1a Рефакторинг, плюс небольшие доделки 2020-11-15 14:55:02 +07:00
Book Pauk
b81cd3240b Merge tag '0.9.8-4' into develop
0.9.8-4
2020-11-15 00:09:10 +07:00
Book Pauk
1e6105b076 Merge branch 'release/0.9.8-4' 2020-11-15 00:09:03 +07:00
Book Pauk
d8d89b3463 Добавлен показ аннотации в начале книги 2020-11-15 00:07:45 +07:00
Book Pauk
459564cb2d Рефакторинг 2020-11-14 23:43:22 +07:00
Book Pauk
084df35184 Мелкий рефакторинг 2020-11-14 23:32:39 +07:00
Book Pauk
81912babeb Merge tag '0.9.8-3' into develop
0.9.8-3
2020-11-14 23:27:14 +07:00
Book Pauk
3943fc7d95 Merge branch 'release/0.9.8-3' 2020-11-14 23:27:09 +07:00
Book Pauk
3999dc930c Рефакторинг, плюс небольшие изменения внешнего вида 2020-11-14 23:24:20 +07:00
Book Pauk
d4dea16456 Убрал неиспользуемые компоненты 2020-11-14 23:23:51 +07:00
Book Pauk
ed38cb33a5 Merge tag '0.9.8-2' into develop
0.9.8-2
2020-11-14 12:01:44 +07:00
Book Pauk
4cf5a0f4c8 Merge branch 'release/0.9.8-2' 2020-11-14 12:01:39 +07:00
Book Pauk
8f0d526af2 Скрыл хоткей для "Библиотека" в режиме omnireader 2020-11-14 12:00:55 +07:00
Book Pauk
6ca3881841 Merge tag '0.9.8-1' into develop
0.9.8-1
2020-11-14 11:49:06 +07:00
Book Pauk
d8e765a04f Merge branch 'release/0.9.8-1' 2020-11-14 11:49:02 +07:00
Book Pauk
40ff572f94 Добавил проксирование для fantasy-worlds.org 2020-11-14 11:48:03 +07:00
Book Pauk
cc4275dc03 Merge tag '0.9.8' into develop
0.9.8
2020-11-14 00:05:14 +07:00
Book Pauk
b387f4a0db Merge branch 'release/0.9.8' 2020-11-14 00:05:07 +07:00
Book Pauk
1a096031c4 Версия 0.9.8 2020-11-14 00:04:30 +07:00
Book Pauk
83a60b4091 Небольшие поправки парсера 2020-11-13 23:59:55 +07:00
Book Pauk
b292407ec2 Работа над ContentsPage 2020-11-13 23:33:36 +07:00
Book Pauk
952c337b76 Работа над ContentsPage 2020-11-13 22:45:34 +07:00
Book Pauk
e947b887fe Работа над ContentsPage 2020-11-13 19:10:15 +07:00
Book Pauk
bd1e5485d7 Работа над ContentsPage 2020-11-13 18:47:11 +07:00
Book Pauk
e095c3318b Начало работы над ContentsPage 2020-11-13 14:23:55 +07:00
Book Pauk
d75a08b519 Merge tag '0.9.7' into develop
0.9.7
2020-11-12 19:44:15 +07:00
Book Pauk
d55a616fe0 Merge branch 'release/0.9.7' 2020-11-12 19:44:08 +07:00
Book Pauk
2146cb3576 Версия 0.9.7 2020-11-12 19:43:33 +07:00
Book Pauk
ae260e74f6 Поправка механизма чистки TempPublicDir 2020-11-12 19:39:57 +07:00
Book Pauk
355410c03c Исправление бага - не изменялись хоткеи в настройках 2020-11-12 19:13:24 +07:00
Book Pauk
718ad51fac Merge tag '0.9.6-1' into develop
0.9.6-1
2020-11-07 20:57:02 +07:00
Book Pauk
4242a8679f Merge branch 'release/0.9.6-1' 2020-11-07 20:56:57 +07:00
Book Pauk
4ff9ff699b Добавил ссылку на flibs.in в предустановленные закладки 2020-11-07 20:56:07 +07:00
Book Pauk
7a76673274 Merge tag '0.9.6' into develop
0.9.6
2020-11-06 18:00:31 +07:00
41 changed files with 1091 additions and 280 deletions

View File

@@ -181,9 +181,8 @@ class Reader {
maxUploadFileSize = 10*1024*1024;
if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData();
formData.append('file', file);
formData.append('file', file, file.name);
const options = {
headers: {

View File

@@ -128,6 +128,10 @@ class App extends Vue {
this.setAppTitle();
(async() => {
//запросим persistent storage
if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist();
}
await this.routerReady();
this.redirectIfNeeded();
})();

View File

@@ -159,6 +159,7 @@ import * as lu from './linkUtils';
const proxySubst = {
'http://flibusta.is': 'http://b.liberama.top:23480',
'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
};
export default @Component({
@@ -407,7 +408,7 @@ class ExternalLibs extends Vue {
}
get header() {
let result = (this.ready ? 'Библиотека' : 'Загрузка...');
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
if (this.ready && this.selectedLink) {
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
}
@@ -786,12 +787,17 @@ class ExternalLibs extends Vue {
showHelp() {
this.$root.stdDialog.alert(`
<p>Окно 'Библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
что особенно актуально для мобильных устройств.</p>
<p>'Библиотека' разрешает свободный доступ к сайту flibusta.is. Имеется возможность управлять закладками
<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
к сожалению, в нем открываются не все страницы.
к сожалению, в нем открываются не все страницы.</p>
<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
к третьим лицам.
</p>
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.

View File

@@ -0,0 +1,247 @@
<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="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 item q-px-sm no-wrap">
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
</div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">{{ item.perc }}%</div>
</div>
</div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
<div v-for="subitem in item.list" :key="subitem.key" class="row subitem q-px-sm no-wrap">
<div class="col row clickable" @click="setBookPos(subitem.offset)">
<div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<div class="column justify-center">{{ subitem.perc }}%</div>
</div>
</div>
</div>
</div>
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
Оглавление отсутствует
</div>
</div>
</div>
<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';
export default @Component({
components: {
Window,
},
watch: {
},
})
class ContentsPage extends Vue {
selectedTab = 'contents';
contents = [];
created() {
}
async init(currentBook, parsed) {
this.$refs.window.init();
//закладки
//далее формаирование оглавления
if (this.parsed == parsed)
return;
this.parsed = parsed;
this.contents = [];
await this.$nextTick();
const pc = parsed.contents;
const newpc = [];
//преобразуем все, кроме первого, разделы body в title-subtitle
let curSubtitles = [];
let prevBodyIndex = -1;
for (let i = 0; i < pc.length; i++) {
const cont = pc[i];
if (prevBodyIndex != cont.bodyIndex)
curSubtitles = [];
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);
}
}
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;
}
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 {
border-bottom: 1px solid #e0e0e0;
}
.item:hover, .subitem:hover {
background-color: #f0f0f0;
}
.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);
}
</style>

View File

@@ -7,8 +7,8 @@
<span class="greeting"><b>{{ title }}</b></span>
<div class="q-my-sm"></div>
<span class="greeting">Добро пожаловать!</span>
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz<span v-if="isExternalConverter">, rar</span></b></span>
<span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
</div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">

View File

@@ -48,6 +48,10 @@
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
</button>
<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>
<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>
@@ -89,12 +93,13 @@
@stop-text-search="stopTextSearch">
</SearchPage>
<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>
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<ContentsPage v-show="contentsActive" ref="contentsPage" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
</div>
@@ -121,6 +126,8 @@ import SettingsPage from './SettingsPage/SettingsPage.vue';
import HelpPage from './HelpPage/HelpPage.vue';
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
import ServerStorage from './ServerStorage/ServerStorage.vue';
import ContentsPage from './ContentsPage/ContentsPage.vue';
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager';
@@ -143,6 +150,8 @@ export default @Component({
HelpPage,
ClickMapPage,
ServerStorage,
ContentsPage,
ReaderDialogs,
},
watch: {
@@ -200,6 +209,7 @@ class Reader extends Vue {
settingsActive = false;
helpActive = false;
clickMapActive = false;
contentsActive = false;
bookPos = null;
allowUrlParamBookPos = false;
@@ -240,11 +250,11 @@ class Reader extends Vue {
await this.$nextTick();
this.paramPosIgnore = false;
}
}, 500, {'maxWait':5000});
}, 500, {maxWait: 5000});
this.scrollingSetRecentBook = _.debounce((newValue) => {
this.debouncedSetRecentBook(newValue);
}, 15000, {'maxWait':20000});
}, 15000, {maxWait: 20000});
document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null);
@@ -490,6 +500,7 @@ class Reader extends Vue {
this.stopScrolling();
this.stopSearch();
this.helpActive = false;
this.contentsActive = false;
}
loaderToggle() {
@@ -603,6 +614,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() {
if (this.libsActive)
this.libsToogle();
@@ -707,6 +733,7 @@ class Reader extends Vue {
case 'copyText':
case 'splitToPara':
case 'refresh':
case 'contents':
case 'libs':
case 'recentBooks':
case 'offlineMode':
@@ -735,6 +762,7 @@ class Reader extends Vue {
case 'scrolling':
case 'search':
case 'copyText':
case 'contents':
classResult = classDisabled;
break;
case 'splitToPara':
@@ -818,8 +846,12 @@ class Reader extends Vue {
let url = encodeURI(decodeURI(opts.url));
//TODO: убрать конвертирование 'file://' после 06.2021
if (url.length == 71 && url.indexOf('file://') == 0)
url = url.replace(/^file/, 'disk');
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('file://') != 0))
(url.indexOf('disk://') != 0))
url = 'http://' + url;
// уже просматривается сейчас
@@ -896,7 +928,8 @@ class Reader extends Vue {
url,
skipCheck: (opts.skipCheck ? true : false),
isText: (opts.isText ? true : false),
enableSitesFilter: this.enableSitesFilter
enableSitesFilter: this.enableSitesFilter,
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : ''),
},
(state) => {
progress.setState(state);
@@ -949,7 +982,7 @@ class Reader extends Vue {
progress.hide(); this.progressActive = false;
await this.loadBook({url});
await this.loadBook({url, uploadFileName: opts.file.name});
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
@@ -1026,6 +1059,9 @@ class Reader extends Vue {
case 'refresh':
this.refreshBook();
break;
case 'contents':
this.contentsPageToggle();
break;
case 'libs':
this.libsToogle();
break;
@@ -1125,6 +1161,9 @@ class Reader extends Vue {
if (!result && this.copyTextActive)
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)
result = this.$refs.page.keyHook(event);

View File

@@ -59,7 +59,7 @@
<q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%">
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
</div>
</q-td>
@@ -87,7 +87,7 @@
import Vue from 'vue';
import Component from 'vue-class-component';
import path from 'path';
import _ from 'lodash';
//import _ from 'lodash';
import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue';
@@ -112,6 +112,7 @@ class RecentBooksPage extends Vue {
pagination = {};
created() {
this.firstInit = true;
this.pagination = {rowsPerPage: 0};
this.columns = [
@@ -167,26 +168,11 @@ class RecentBooksPage extends Vue {
this.initing = true;
if (!bookManager.loaded) {
await this.updateTableData(10);
//для отзывчивости
await utils.sleep(100);
let i = 0;
let j = 5;
while (i < 500 && !bookManager.loaded) {
if (i % j == 0) {
bookManager.sortedRecentCached = null;
await this.updateTableData(20);
j *= 2;
}
await utils.sleep(100);
i++;
}
} else {
//для отзывчивости
await utils.sleep(100);
if (this.firstInit) {//для отзывчивости
await this.updateTableData(20);
this.firstInit = false;
}
await utils.sleep(50);
await this.updateTableData();
this.initing = false;
})();
@@ -222,30 +208,11 @@ class RecentBooksPage extends Vue {
textLen = ` ${Math.round(book.textLength/1000)}k`;
}
const fb2 = (book.fb2 ? book.fb2 : {});
const bt = utils.getBookTitle(book.fb2);
let title = fb2.bookTitle;
if (title)
title = `"${title}"`;
else
title = '';
let author = '';
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
author = authorNames.join(', ');
} else {//TODO: убрать в будущем
author = _.compact([
fb2.lastName,
fb2.firstName,
fb2.middleName
]).join(' ');
}
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
result.push({
num,
@@ -256,9 +223,10 @@ class RecentBooksPage extends Vue {
author,
title: `${title}${perc}${textLen}`,
},
descString: `${author}${title}${perc}${textLen}`,
descString: `${author}${title}${perc}${textLen}`,//для сортировки
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
});
}
@@ -291,13 +259,18 @@ class RecentBooksPage extends Vue {
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
}
async downloadBook(fb2path) {
async downloadBook(fb2path, fullTitle) {
try {
await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download;
d.href = fb2path;
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
try {
const fn = utils.makeValidFilename(fullTitle);
d.download = fn.substring(0, 100) + '.fb2';
} catch(e) {
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
}
d.click();
} catch (e) {
@@ -308,14 +281,6 @@ class RecentBooksPage extends Vue {
}
}
openOriginal(url) {
window.open(url, '_blank');
}
openFb2(path) {
window.open(path, '_blank');
}
async handleDel(key) {
await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
@@ -331,7 +296,7 @@ class RecentBooksPage extends Vue {
isUrl(url) {
if (url)
return (url.indexOf('file://') != 0);
return (url.indexOf('disk://') != 0);
else
return false;
}

View File

@@ -507,10 +507,10 @@ class ServerStorage extends Vue {
if (md.key && result[md.key])
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
if (!bookManager.loaded) {
/*if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией');
while (!bookManager.loaded) await utils.sleep(100);
}
}*/
if (newRecent.rev != this.cachedRecent.rev)
await this.setCachedRecent(newRecent);
@@ -580,8 +580,8 @@ class ServerStorage extends Vue {
let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent
while (!bookManager.loaded)
await utils.sleep(100);
/*while (!bookManager.loaded)
await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};

View File

@@ -104,8 +104,12 @@ class UserHotKeys extends UserHotKeysProps {
this.updateTableData();
}
get mode() {
return this.$store.state.config.mode;
}
updateTableData() {
let result = rstore.hotKeys.map(hk => hk.name);
let result = rstore.hotKeys.map(hk => hk.name).filter(name => (this.mode == 'liberama.top' || name != 'libs'));
const search = this.search.toLowerCase();
const codesIncludeSearch = (action) => {

View File

@@ -159,7 +159,7 @@ export default class DrawHelper {
const fh = h - 2*pad;
const fh2 = fh/2;
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
const w1 = this.measureTextFont(t1, font) + fh2;
const read = (bookPos + 1)/textLength;
const t2 = `${(read*100).toFixed(2)}%`;

View File

@@ -40,7 +40,7 @@ import Component from 'vue-class-component';
import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import {sleep} from '../../../share/utils';
import * as utils from '../../../share/utils';
import bookManager from '../share/bookManager';
import DrawHelper from './DrawHelper';
import rstore from '../../../store/modules/reader';
@@ -134,7 +134,7 @@ class TextPage extends Vue {
this.$root.$on('resize', async() => {
this.$nextTick(this.onResize);
await sleep(500);
await utils.sleep(500);
this.$nextTick(this.onResize);
});
}
@@ -285,7 +285,7 @@ class TextPage extends Vue {
let close = null;
(async() => {
await sleep(500);
await utils.sleep(500);
if (this.fontsLoading)
close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
})();
@@ -342,7 +342,7 @@ class TextPage extends Vue {
let i = 0;
const t = this.parsed.testText;
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
await sleep(100);
await utils.sleep(100);
if (this.parsed === parsed) {
this.parsed.testWidth = this.drawHelper.measureText(t, {});
@@ -366,7 +366,6 @@ class TextPage extends Vue {
this.updateLayout();
this.book = null;
this.meta = null;
this.fb2 = null;
this.parsed = null;
this.linesUp = null;
@@ -383,7 +382,7 @@ class TextPage extends Vue {
try {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10);
while (this.doingLazyParse) await utils.sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
@@ -392,21 +391,9 @@ class TextPage extends Vue {
this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
const bt = utils.getBookTitle(this.meta.fb2);
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.title = bt.fullTitle;
this.$root.$emit('set-app-title', this.title);
@@ -493,7 +480,7 @@ class TextPage extends Vue {
let wait = (timeout + 201)/100;
while (wait > 0 && !this[stopPropertyName]) {
wait--;
await sleep(100);
await utils.sleep(100);
}
resolve();
})().catch(reject); });
@@ -509,7 +496,7 @@ class TextPage extends Vue {
}
//ждем анимацию
while (this.inAnimation) await sleep(10);
while (this.inAnimation) await utils.sleep(10);
this.stopScrolling = false;
this.doingScrolling = true;
@@ -520,7 +507,7 @@ class TextPage extends Vue {
this.page1 = this.page2;
this.toggleLayout = true;
await this.$nextTick();
await sleep(50);
await utils.sleep(50);
this.cachedPos = -1;
this.draw();
@@ -557,7 +544,7 @@ class TextPage extends Vue {
page.style.transform = 'none';
page.offsetHeight;
while (this.doingScrolling) await sleep(10);
while (this.doingScrolling) await utils.sleep(10);
}
draw() {
@@ -766,7 +753,7 @@ class TextPage extends Vue {
for (let i = 0; i < this.parsed.para.length; i++) {
j++;
if (j > 1) {
await sleep(1);
await utils.sleep(1);
j = 0;
}
if (this.stopLazyParse)
@@ -788,7 +775,7 @@ class TextPage extends Vue {
async refreshTime() {
if (!this.timeRefreshing) {
this.timeRefreshing = true;
await sleep(60*1000);
await utils.sleep(60*1000);
if (this.book && this.parsed.textLength) {
this.debouncedDrawStatusBar();
@@ -905,7 +892,7 @@ class TextPage extends Vue {
this.settingsChanging = true;
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
this.commit('reader/setSettings', {fontSize: newSize});
await sleep(50);
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -915,7 +902,7 @@ class TextPage extends Vue {
this.settingsChanging = true;
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
this.commit('reader/setSettings', {fontSize: newSize});
await sleep(50);
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -925,7 +912,7 @@ class TextPage extends Vue {
this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
this.commit('reader/setSettings', {scrollingDelay: newDelay});
await sleep(50);
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -935,7 +922,7 @@ class TextPage extends Vue {
this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
this.commit('reader/setSettings', {scrollingDelay: newDelay});
await sleep(50);
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -949,7 +936,7 @@ class TextPage extends Vue {
let delay = 400;
while (this.repDoing) {
this.handleClick(pointX, pointY);
await sleep(delay);
await utils.sleep(delay);
if (delay > 15)
delay *= 0.8;
}
@@ -1066,7 +1053,7 @@ class TextPage extends Vue {
onStatusBarClick() {
const url = this.meta.url;
if (url && url.indexOf('file://') != 0) {
if (url && url.indexOf('disk://') != 0) {
window.open(url, '_blank');
} else {
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});

View File

@@ -1,6 +1,6 @@
import he from 'he';
import sax from '../../../../server/core/sax';
import {sleep} from '../../../share/utils';
import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
@@ -46,11 +46,21 @@ export default class BookParser {
let isFirstSection = true;
let isFirstTitlePara = false;
//изображения
this.binary = {};
let binaryId = '';
let binaryType = '';
let dimPromises = [];
//оглавление
this.contents = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
let paraIndex = -1;
let paraOffset = 0;
let para = []; /*array of
@@ -80,7 +90,7 @@ export default class BookParser {
i.onerror = reject;
i.src = `data:${binaryType};base64,${data}`;
await sleep(30*1000);
await utils.sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
})().catch(reject); });
@@ -102,7 +112,7 @@ export default class BookParser {
i.onerror = reject;
i.src = src;
await sleep(30*1000);
await utils.sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
})().catch(reject); });
@@ -118,6 +128,12 @@ export default class BookParser {
addIndex: (addIndex ? addIndex : 0),
};
if (inSubtitle) {
curSubtitle.title += '<p>';
} else if (inTitle) {
curTitle.title += '<p>';
}
para[paraIndex] = p;
paraOffset += p.length;
};
@@ -129,6 +145,7 @@ export default class BookParser {
return;
}
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
@@ -143,6 +160,11 @@ export default class BookParser {
p.offset = paraOffset;
para[paraIndex] = p;
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
//уберем начальный пробел
p.length = 0;
p.text = p.text.substr(1);
@@ -151,6 +173,13 @@ export default class BookParser {
p.length += len;
p.text += text;
if (inSubtitle) {
curSubtitle.title += text;
} else if (inTitle) {
curTitle.title += text;
}
para[paraIndex] = p;
paraOffset += p.length;
};
@@ -160,7 +189,7 @@ export default class BookParser {
return;
tag = elemName;
path += '/' + elemName;
path += '/' + tag;
if (tag == 'binary') {
let attrs = sax.getAttrsSync(tail);
@@ -187,17 +216,55 @@ export default class BookParser {
}
}
if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author)
fb2.author = [];
fb2.author.push({});
}
const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
if (!fb2.sequence)
fb2.sequence = [];
if (!isPublishSequence || !fb2.sequence.length) {
const attrs = sax.getAttrsSync(tail);
const seq = {};
if (attrs.name && attrs.name.value) {
seq.name = attrs.name.value;
}
if (attrs.number && attrs.number.value) {
seq.number = attrs.number.value;
}
fb2.sequence.push(seq);
}
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => {
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
});
if (ann.length)
newParagraph(' ', 1);
}
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
const bt = utils.getBookTitle(fb2);
if (bt.sequence) {
newParagraph(bt.sequence, bt.sequence.length);
newParagraph(' ', 1);
}
}
if (!isFirstBody)
newParagraph(' ', 1);
isFirstBody = false;
bodyIndex++;
}
if (tag == 'title') {
@@ -205,12 +272,17 @@ export default class BookParser {
isFirstTitlePara = true;
bold = true;
center = true;
inTitle = true;
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
this.contents.push(curTitle);
}
if (tag == 'section') {
if (!isFirstSection)
newParagraph(' ', 1);
isFirstSection = false;
sectionLevel++;
}
if (tag == 'emphasis' || tag == 'strong') {
@@ -231,9 +303,13 @@ export default class BookParser {
isFirstTitlePara = true;
bold = true;
center = true;
inSubtitle = true;
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
curTitle.subtitles.push(curSubtitle);
}
if (tag == 'epigraph') {
if (tag == 'epigraph' || tag == 'annotation') {
italic = true;
space += 1;
}
@@ -260,6 +336,11 @@ export default class BookParser {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong') {
@@ -274,11 +355,14 @@ export default class BookParser {
isFirstTitlePara = false;
bold = false;
center = false;
inSubtitle = false;
}
if (tag == 'epigraph') {
if (tag == 'epigraph' || tag == 'annotation') {
italic = false;
space -= 1;
if (tag == 'annotation')
newParagraph(' ', 1);
}
if (tag == 'stanza') {
@@ -372,7 +456,7 @@ export default class BookParser {
};
const onProgress = async(prog) => {
await sleep(1);
await utils.sleep(1);
callback(prog);
};
@@ -394,7 +478,7 @@ export default class BookParser {
this.textLength = paraOffset;
callback(100);
await sleep(10);
await utils.sleep(10);
return {fb2};
}

View File

@@ -4,19 +4,25 @@ import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
const maxDataSize = 300*1024*1024;//compressed bytes
const maxDataSize = 500*1024*1024;//compressed bytes
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
name: 'bmMetaStore'
});
//локальный кэш самих книг, ограничение maxDataSize
const bmDataStore = localForage.createInstance({
name: 'bmDataStore'
});
const bmRecentStore = localForage.createInstance({
//список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
class BookManager {
async init(settings) {
@@ -25,15 +31,80 @@ class BookManager {
this.eventListeners = [];
this.books = {};
this.recent = {};
this.recentLast = await bmRecentStore.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
this.recent = {};
this.saveRecent = _.debounce(() => {
bmRecentStoreNew.setItem('recent', this.recent);
}, 300, {maxWait: 800});
this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300});
//загрузка bmRecentStore
this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
if (this.recentRev) {
this.recent = await bmRecentStoreNew.getItem('recent');
if (!this.recent)
this.recent = {};
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem;
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
await this.cleanRecentBooks();
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
{
await this.convertFileToDiskPrefix();
if (this.recentRev > 10)
await bmRecentStoreOld.clear();
}
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
let key = null;
const len = await bmRecentStoreOld.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStoreOld.key(i);
if (key) {
let r = await bmRecentStoreOld.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStoreOld.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await bmRecentStoreNew.setItem('recent', this.recent);
this.recentRev = 1;
await bmRecentStoreNew.setItem('rev', this.recentRev);
}
this.recentChanged = true;
@@ -41,9 +112,7 @@ class BookManager {
this.loadStored();//no await
}
//Долгая асинхронная загрузка из хранилища.
//Хранение в отдельных записях дает относительно
//нормальное поведение при нескольких вкладках с читалкой в браузере.
//Ленивая асинхронная загрузка bmMetaStore
async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
await utils.sleep(2000);
@@ -70,32 +139,7 @@ class BookManager {
}
}
let key = null;
len = await bmRecentStore.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStore.key(i);
if (key) {
let r = await bmRecentStore.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStore.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await this.cleanBooks();
await this.cleanRecentBooks();
this.recentChanged = true;
this.loaded = true;
this.emit('load-stored-finish');
}
@@ -238,7 +282,7 @@ class BookManager {
let book = this.books[meta.key];
if (!book && !this.loaded) {
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (book)
this.books[meta.key] = book;
}
@@ -254,7 +298,7 @@ class BookManager {
result = this.books[meta.key];
if (!result) {
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (result)
this.books[meta.key] = result;
}
@@ -328,6 +372,43 @@ class BookManager {
}
//-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev');
if (rev != this.recentRev && !skipCheck) {
const newRecent = await bmRecentStoreNew.getItem('recent');
Object.assign(this.recent, newRecent);
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
this.recentRev = rev;
}
const prevKey = (this.recentItem ? this.recentItem.key : '');
if (item) {
this.recent[item.key] = item;
this.recentItem = item;
} else {
this.recentItem = null;
}
this.saveRecentItem();
if (!item || prevKey != item.key) {
this.saveRecent();
}
this.recentChanged = true;
if (item) {
this.emit('recent-changed', item.key);
} else {
this.emit('recent-changed');
}
}
async recentSetLastKey(key) {
this.recentLastKey = key;
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
}
async setRecentBook(value) {
const result = this.metaOnly(value);
result.touchTime = Date.now();
@@ -341,38 +422,25 @@ class BookManager {
result.bookPosSeen = this.recent[result.key].bookPosSeen;
}
this.recent[result.key] = result;
await bmRecentStore.setItem(result.key, result);
this.recentLast = result;
await bmRecentStore.setItem('recent-last', this.recentLast);
this.recentChanged = true;
this.emit('recent-changed', result.key);
await this.recentSetLastKey(result.key);
await this.recentSetItem(result);
return result;
}
async getRecentBook(value) {
let result = this.recent[value.key];
if (!result) {
result = await bmRecentStore.getItem(value.key);
if (result)
this.recent[value.key] = result;
}
return result;
return this.recent[value.key];
}
async delRecentBook(value) {
this.recent[value.key].deleted = 1;
await bmRecentStore.setItem(value.key, this.recent[value.key]);
const item = this.recent[value.key];
item.deleted = 1;
if (this.recentLast.key == value.key) {
this.recentLast = null;
await bmRecentStore.setItem('recent-last', this.recentLast);
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
}
await this.recentSetItem(item);
this.emit('recent-deleted', value.key);
this.emit('recent-changed', value.key);
}
async cleanRecentBooks() {
@@ -380,24 +448,49 @@ class BookManager {
let isDel = false;
for (let i = 1000; i < sorted.length; i++) {
await bmRecentStore.removeItem(sorted[i].key);
delete this.recent[sorted[i].key];
await bmRecentStore.removeItem(sorted[i].key);
isDel = true;
}
this.sortedRecentCached = null;
if (isDel)
this.emit('recent-changed');
await this.recentSetItem();
return isDel;
}
mostRecentBook() {
if (this.recentLast) {
return this.recentLast;
async convertFileToDiskPrefix() {
let isConverted = false;
const newRecent = {};
for (let key of Object.keys(this.recent)) {
let newKey = key;
let newUrl = this.recent[key].url;
if (newKey.indexOf('66696c65') == 0) {
newKey = newKey.replace(/^66696c65/, '6469736b');
if (newUrl)
newUrl = newUrl.replace(/^file/, 'disk');
isConverted = true;
}
newRecent[newKey] = this.recent[key];
newRecent[newKey].key = newKey;
if (newUrl)
newRecent[newKey].url = newUrl;
}
const oldRecentLast = this.recentLast;
if (isConverted) {
this.recent = newRecent;
await this.recentSetItem(null, true);
}
return isConverted;
}
mostRecentBook() {
if (this.recentLastKey) {
return this.recent[this.recentLastKey];
}
const oldKey = this.recentLastKey;
let max = 0;
let result = null;
@@ -408,10 +501,11 @@ class BookManager {
result = book;
}
}
this.recentLast = result;
bmRecentStore.setItem('recent-last', this.recentLast);//no await
const newRecentLastKey = (result ? result.key : null);
this.recentSetLastKey(newRecentLastKey);//no await
if (this.recentLast !== oldRecentLast)
if (newRecentLastKey !== oldKey)
this.emit('recent-changed');
return result;
@@ -442,24 +536,12 @@ class BookManager {
delete mergedRecent[i];
}
//"ленивое" обновление хранилища
(async() => {
for (const rec of Object.values(mergedRecent)) {
if (rec.key) {
await bmRecentStore.setItem(rec.key, rec);
await utils.sleep(1);
}
}
})();
this.recent = mergedRecent;
this.recentLast = null;
await bmRecentStore.setItem('recent-last', this.recentLast);
await this.recentSetLastKey(null);
await this.recentSetItem(null, true);
this.recentChanged = true;
this.emit('set-recent');
this.emit('recent-changed');
}
addEventListener(listener) {

View File

@@ -1,4 +1,50 @@
export const versionHistory = [
{
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)',

View File

@@ -1,5 +1,5 @@
<template>
<q-dialog v-model="active">
<q-dialog v-model="active" no-route-dismiss>
<div class="column bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">

View File

@@ -1,5 +1,5 @@
<template>
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
<slot></slot>
<!--------------------------------------------------->

View File

@@ -21,7 +21,7 @@ import {QSlider} from 'quasar/src/components/slider';
import {QTabs, QTab} from 'quasar/src/components/tabs';
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
import {QSeparator} from 'quasar/src/components/separator';
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
//import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
import {QTooltip} from 'quasar/src/components/tooltip';
import {QSpinner} from 'quasar/src/components/spinner';
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
@@ -32,6 +32,7 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = {
//QLayout,
@@ -48,7 +49,7 @@ const components = {
QTabs, QTab,
//QTabPanels, QTabPanel,
QSeparator,
QList, QItem, QItemSection, QItemLabel,
//QList, QItem, QItemSection, QItemLabel,
QTooltip,
QSpinner,
QTable, QTh, QTr, QTd,
@@ -58,7 +59,8 @@ const components = {
QPopupProxy,
QDialog,
QChip,
QTree
QTree,
//QExpansionItem,
};
//directives

View File

@@ -103,30 +103,57 @@ export function fromBase64(data) {
));
}
export function getObjDiff(oldObj, newObj) {
const result = {__isDiff: true, change: {}, add: {}, del: []};
export function getObjDiff(oldObj, newObj, opts = {}) {
const {
exclude = [],
excludeAdd = [],
excludeDel = [],
} = opts;
for (const key of Object.keys(oldObj)) {
if (newObj.hasOwnProperty(key)) {
if (!_.isEqual(oldObj[key], newObj[key])) {
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
} else {
result.change[key] = _.cloneDeep(newObj[key]);
const ex = new Set(exclude);
const exAdd = new Set(excludeAdd);
const exDel = new Set(excludeDel);
const makeObjDiff = (oldObj, newObj, keyPath) => {
const result = {__isDiff: true, change: {}, add: {}, del: []};
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)) {
if (!oldObj.hasOwnProperty(key)) {
result.add[key] = _.cloneDeep(newObj[key]);
}
}
return result;
return makeObjDiff(oldObj, newObj, '');
}
export function isObjDiff(diff) {
@@ -277,3 +304,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
}
return result;
}
export function removeHtmlTags(s) {
return s.replace(/(<([^>]+)>)/ig, '');
}
export function makeValidFilename(filename, repl = '_') {
let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
f = f.trim();
while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
f = f.substring(0, f.length - 1);
}
if (f)
return f;
else
throw new Error('Invalid filename');
}
export function getBookTitle(fb2) {
fb2 = (fb2 ? fb2 : {});
const result = {};
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
result.author = authorNames.join(', ');
}
if (fb2.sequence) {
const seqs = fb2.sequence.map(s => _.compact([
s.name,
(s.number ? `#${s.number}` : null),
]).join(' '));
result.sequence = seqs.join(', ');
if (result.sequence)
result.sequenceTitle = `(${result.sequence})`;
}
result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
result.fullTitle = _.compact([
result.author,
result.bookTitle
]).join(' - ');
return result;
}

View File

@@ -15,7 +15,8 @@ const readerActions = {
'splitToPara': 'Обновить с разбиением на параграфы',
'refresh': 'Принудительно обновить книгу',
'offlineMode': 'Автономный режим (без интернета)',
'libs': 'Библиотека',
'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
@@ -42,6 +43,7 @@ const toolButtons = [
{name: 'copyText', show: false},
{name: 'splitToPara', show: false},
{name: 'refresh', show: true},
{name: 'contents', show: true},
{name: 'libs', show: true},
{name: 'recentBooks', show: true},
{name: 'offlineMode', show: false},
@@ -61,9 +63,10 @@ const hotKeys = [
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'splitToPara', codes: ['Shift+R']},
{name: 'refresh', codes: ['R']},
{name: 'offlineMode', codes: ['O']},
{name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']},
{name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']},
{name: 'bookBegin', codes: ['Home']},
@@ -268,9 +271,12 @@ for (const button of toolButtons)
for (const hotKey of hotKeys)
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);
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
}
@@ -288,15 +294,18 @@ const libsDefaults = {
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
{l: 'https://flibs.in', c: 'Flibs'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},
{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: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},

View File

@@ -1,3 +1,3 @@
#!/bin/bash
sudo -H -u www-data /home/beta.liberama/liberama
sudo -H -u www-data bash -c "cd /var/www; /home/beta.liberama/liberama"

View File

@@ -126,3 +126,18 @@ server {
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;
}
}

View File

@@ -27,15 +27,19 @@ sudo chown www-data.www-data /home/liberama
### external converter `calibre`, download from https://download.calibre-ebook.com/
```
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
wget "https://download.calibre-ebook.com/5.5.0/calibre-5.5.0-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
```
### external converters
```
sudo apt install rar
sudo apt install libreoffice
sudo apt install poppler-utils
sudo apt install djvulibre-bin
sudo apt install libtiff-tools
sudo apt install graphicsmagick-imagemagick-compat
```
### nginx, server config

View File

@@ -1,7 +1,7 @@
#!/bin/bash
if ! pgrep -x "liberama" > /dev/null ; then
sudo -H -u www-data /home/liberama/liberama
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
else
echo "Process 'liberama' already running"
fi

View File

@@ -1,4 +1,4 @@
#!/bin/bash
sudo -H -u www-data /home/liberama/liberama &
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" &
sudo service cron start

17
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.9.6",
"version": "0.9.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -10124,6 +10124,21 @@
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
"dev": true
},
"pidusage": {
"version": "2.0.21",
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz",
"integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==",
"requires": {
"safe-buffer": "^5.2.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.9.6",
"version": "0.9.10",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -8,7 +8,7 @@
"node": ">=10.0.0"
},
"scripts": {
"dev": "nodemon --inspect --exec 'node server'",
"dev": "nodemon --inspect --ignore server/public --ignore server/data --exec 'node server'",
"build:client": "webpack --config build/webpack.prod.config.js",
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
@@ -72,6 +72,7 @@
"multer": "^1.4.2",
"pako": "^1.0.11",
"path-browserify": "^1.0.0",
"pidusage": "^2.0.21",
"quasar": "^1.14.3",
"safe-buffer": "^5.2.0",
"sjcl": "^1.0.8",

View File

@@ -18,7 +18,7 @@ module.exports = {
loggingEnabled: true,
maxUploadFileSize: 50*1024*1024,//50Мб
maxTempPublicDirSize: 512*1024*1024,//512Мб
maxTempPublicDirSize: 512*1024*1024,//512Мб + 20% квота если проблема с remoteWebDavStorage
maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false,

View File

@@ -22,6 +22,7 @@ class ReaderController extends BaseController {
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
isText: (request.hasOwnProperty('isText') ? request.isText : false),
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
});
const state = this.workerState.getState(workerId);
return (state ? state : {});

View File

@@ -138,6 +138,7 @@ class WebSocketController {
i++;
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
break;
}
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
}

View File

@@ -15,6 +15,13 @@ class FileDecompressor {
constructor(limitFileSize = 0) {
this.detector = new FileDetector();
this.limitFileSize = limitFileSize;
this.rarPath = '/usr/bin/rar';
this.rarExists = false;
(async() => {
if (await fs.pathExists(this.rarPath))
this.rarExists = true;
})();
}
async decompressNested(filename, outputDir) {
@@ -30,7 +37,11 @@ class FileDecompressor {
files: []
};
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) {
if (!fileType || !(
fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz'
|| fileType.ext == 'tar' || (this.rarExists && fileType.ext == 'rar')
)
) {
return result;
}
@@ -94,6 +105,11 @@ class FileDecompressor {
async decompress(fileExt, filename, outputDir) {
let files = [];
if (fileExt == 'rar' && this.rarExists) {
files = await this.unRar(filename, outputDir);
return files;
}
switch (fileExt) {
case 'zip':
files = await this.unZip(filename, outputDir);
@@ -123,8 +139,7 @@ class FileDecompressor {
decodeEntryNameCallback: (nameRaw) => {
return utils.bufferRemoveZeroes(nameRaw);
}
}
);
});
} catch (e) {
fs.emptyDir(outputDir);
return await zip.unpack(filename, outputDir, {
@@ -222,7 +237,39 @@ class FileDecompressor {
inputStream.pipe(stream).pipe(outputStream);
})().catch(reject); });
}
}
async unRar(filename, outputDir) {
try {
const args = ['x', '-p-', '-y', filename, `${outputDir}`];
const result = await utils.spawnProcess(this.rarPath, {
killAfter: 60,
args
});
if (result.code == 0) {
const files = [];
await utils.findFiles(async(file) => {
const stat = await fs.stat(file);
files.push({path: path.relative(outputDir, file), size: stat.size});
}, outputDir);
return files;
} else {
const error = `${result.code}|FORLOG|, exec: ${this.rarPath}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
throw new Error(`Архиватор Rar завершился с ошибкой: ${error}`);
}
} catch(e) {
if (e.status == 'killed') {
throw new Error('Слишком долгое ожидание архиватора Rar');
} else if (e.status == 'error') {
throw new Error(e.error);
} else {
throw new Error(e);
}
}
}
async gzipBuffer(buf) {
return new Promise((resolve, reject) => {

View File

@@ -54,7 +54,7 @@ class FileDetector {
fromBuffer(buffer, callback) {
let result = null;
//console.log(buffer);
const invalidSignaturesList = this.validateSigantures();
if (invalidSignaturesList.length) {
return callback(invalidSignaturesList);

View File

@@ -676,7 +676,9 @@
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" }
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" },
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00220031002e0030002200" },
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00270031002e0030002700" }
]
}
]

View File

@@ -1 +0,0 @@
test

View File

@@ -6,7 +6,7 @@ const LimitedQueue = require('../../LimitedQueue');
const textUtils = require('./textUtils');
const utils = require('../../utils');
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
const queue = new LimitedQueue(3, 20, 3*60*1000);//3 минуты ожидание подвижек
class ConvertBase {
constructor(config) {
@@ -44,18 +44,22 @@ class ConvertBase {
try {
const result = await utils.spawnProcess(path, {
killAfter: 600,
killAfter: 3600,//1 час
args,
onData: (data) => {
q.resetTimeout();
onData(data);
},
//будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
onUsage: (stats) => {
if (queue.freed > 1 && stats.cpu >= 10)
onData('.');
},
onUsageInterval: 10,
abort
});
if (result.code != 0) {
let error = result.code;
if (this.config.branch == 'development')
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
}
} catch(e) {

View File

@@ -0,0 +1,97 @@
const fs = require('fs-extra');
const path = require('path');
const utils = require('../../utils');
const ConvertHtml = require('./ConvertHtml');
class ConvertDjvu extends ConvertHtml {
check(data, opts) {
const {inputFiles} = opts;
return this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'djvu';
}
async run(data, opts) {
if (!this.check(data, opts))
return false;
const {inputFiles, callback, abort, uploadFileName} = opts;
const ddjvuPath = '/usr/bin/ddjvu';
if (!await fs.pathExists(ddjvuPath))
throw new Error('Внешний конвертер ddjvu не найден');
const tiffsplitPath = '/usr/bin/tiffsplit';
if (!await fs.pathExists(tiffsplitPath))
throw new Error('Внешний конвертер tiffsplitPath не найден');
const mogrifyPath = '/usr/bin/mogrify';
if (!await fs.pathExists(mogrifyPath))
throw new Error('Внешний конвертер mogrifyPath не найден');
const dir = `${inputFiles.filesDir}/`;
const inpFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
const tifFile = `${inpFile}.tif`;
//конвертируем в tiff
let perc = 0;
await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
perc = (perc < 100 ? perc + 1 : 40);
callback(perc);
}, abort);
const tifFileSize = (await fs.stat(tifFile)).size;
const limitSize = 3*this.config.maxUploadFileSize;
if (tifFileSize > limitSize) {
throw new Error(`Файл для конвертирования слишком большой|FORLOG| ${tifFileSize} > ${limitSize}`);
}
//разбиваем на файлы
await this.execConverter(tiffsplitPath, [tifFile, dir], null, abort);
await fs.remove(tifFile);
//конвертируем в jpg
await this.execConverter(mogrifyPath, ['-quality', '20', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
perc = (perc < 100 ? perc + 1 : 40);
callback(perc);
}, abort);
//читаем изображения
const loadImage = async(image) => {
image.data = (await fs.readFile(image.file)).toString('base64');
image.name = path.basename(image.file);
}
let files = [];
await utils.findFiles(async(file) => {
if (path.extname(file) == '.jpg')
files.push({name: file, base: path.basename(file)});
}, dir);
files.sort((a, b) => a.base.localeCompare(b.base));
let images = [];
let loading = [];
files.forEach(f => {
const image = {file: f.name};
images.push(image);
loading.push(loadImage(image));
});
await Promise.all(loading);
//формируем текст
let title = '';
if (uploadFileName)
title = uploadFileName;
let text = `<title>${title}</title>`;
for (const image of images) {
text += `<fb2-image type="image/jpeg" name="${image.name}">${image.data}</fb2-image>`;
}
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
}
}
module.exports = ConvertDjvu;

View File

@@ -1,5 +1,6 @@
const ConvertBase = require('./ConvertBase');
const iconv = require('iconv-lite');
const textUtils = require('./textUtils');
class ConvertFb2 extends ConvertBase {
check(data, opts) {
@@ -9,26 +10,46 @@ class ConvertFb2 extends ConvertBase {
}
async run(data, opts) {
if (!this.check(data, opts))
let newData = data;
//Корректируем кодировку, 16-битные кодировки должны стать utf-8
const encoding = textUtils.getEncoding(newData);
if (encoding.indexOf('UTF-16') == 0) {
newData = Buffer.from(iconv.decode(newData, encoding));
}
if (!this.check(newData, opts))
return false;
return this.checkEncoding(data);
return this.checkEncoding(newData);
}
checkEncoding(data) {
let result = data;
const left = data.indexOf('<?xml version="1.0"');
let q = '"';
let left = data.indexOf('<?xml version="1.0"');
if (left < 0) {
left = data.indexOf('<?xml version=\'1.0\'');
q = '\'';
}
if (left >= 0) {
const right = data.indexOf('?>', left);
if (right >= 0) {
const head = data.slice(left, right + 2).toString();
const m = head.match(/encoding="(.*?)"/);
const m = head.match(/encoding=['"](.*?)['"]/);
if (m) {
let encoding = m[1].toLowerCase();
if (encoding != 'utf-8') {
result = iconv.decode(data, encoding);
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
//encoding может не соответсвовать реальной кодировке файла, поэтому:
let calcEncoding = textUtils.getEncoding(data);
if (calcEncoding.indexOf('ISO-8859') >= 0) {
calcEncoding = encoding;
}
result = iconv.decode(data, calcEncoding);
result = Buffer.from(result.toString().replace(m[0], `encoding=${q}utf-8${q}`));
}
}
}

View File

@@ -218,7 +218,7 @@ class ConvertHtml extends ConvertBase {
let i = 0;
//если разброс не слишком большой, выделяем параграфы
if (d < 10 && spaceCounter.length) {
if (d < 20 && spaceCounter.length) {
total /= 20;
i = spaceCounter.length - 1;
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;

View File

@@ -14,17 +14,20 @@ class ConvertPdf extends ConvertHtml {
}
async run(notUsed, opts) {
if (!this.check(notUsed, opts))
return false;
if (!opts.skipCheck) {
if (!this.check(notUsed, opts))
return false;
}
await this.checkExternalConverterPresent();
const {inputFiles, callback, abort} = opts;
const {inputFiles, callback, abort, uploadFileName} = opts;
const inpFile = (opts.pdfFile ? opts.pdfFile : inputFiles.sourceFile);
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
//конвертируем в xml
let perc = 0;
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
await this.execConverter(this.pdfToHtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
perc = (perc < 80 ? perc + 10 : 40);
callback(perc);
}, abort);
@@ -186,6 +189,8 @@ class ConvertPdf extends ConvertHtml {
indents[0] = 0;
//формируем текст
if (!title && uploadFileName)
title = uploadFileName;
let text = `<title>${title}</title>`;
let concat = '';
let sp = '';

View File

@@ -4,6 +4,7 @@ const FileDetector = require('../../FileDetector');
//порядок важен
const convertClassFactory = [
require('./ConvertEpub'),
require('./ConvertDjvu'),
require('./ConvertPdf'),
require('./ConvertRtf'),
require('./ConvertDocX'),

View File

@@ -12,7 +12,7 @@ const utils = require('../utils');
const log = new (require('../AppLogger'))().log;//singleton
const cleanDirPeriod = 60*60*1000;//1 раз в час
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
const queue = new LimitedQueue(5, 100, 4*60*1000);//4 минуты ожидание подвижек
let instance = null;
@@ -30,7 +30,7 @@ class ReaderWorker {
this.workerState = new WorkerState();
this.down = new FileDownloader(config.maxUploadFileSize);
this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
this.bookConverter = new BookConverter(this.config);
this.remoteWebDavStorage = false;
@@ -81,7 +81,7 @@ class ReaderWorker {
const decompDirname = utils.randomHexString(30);
//download or use uploaded
if (url.indexOf('file://') != 0) {//download
if (url.indexOf('disk://') != 0) {//download
const downdata = await this.down.load(url, (progress) => {
wState.set({progress});
}, q.abort);
@@ -130,6 +130,7 @@ class ReaderWorker {
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
wState.set({progress});
q.resetTimeout();
}, q.abort);
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
@@ -171,9 +172,15 @@ class ReaderWorker {
} catch (e) {
log(LM_ERR, e.stack);
if (e.message == 'abort')
e.message = overLoadMes;
wState.set({state: 'error', error: e.message});
let mes = e.message.split('|FORLOG|');
if (mes[1])
log(LM_ERR, mes[0] + mes[1]);
log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
mes = mes[0];
if (mes == 'abort')
mes = overLoadMes;
wState.set({state: 'error', error: mes});
} finally {
//clean
if (q)
@@ -208,7 +215,7 @@ class ReaderWorker {
await fs.remove(file.path);
}
return `file://${hash}`;
return `disk://${hash}`;
}
async restoreRemoteFile(filename) {
@@ -266,30 +273,38 @@ class ReaderWorker {
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);
let i = 0;
let j = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
const oldFile = `${dir}/${file.name}`;
let remoteSuccess = true;
//отправляем только this.config.tempPublicDir
//TODO: убрать в будущем, т.к. уже делается ленивое сохранение compFilename в удаленном хранилище
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
remoteSuccess = false;
try {
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
await this.remoteWebDavStorage.putFile(oldFile);
remoteSuccess = true;
} catch (e) {
log(LM_ERR, e.stack);
}
}
await fs.remove(oldFile);
//реально удаляем только если сохранили в хранилище
if (remoteSuccess || size > maxSize*1.2) {
await fs.remove(oldFile);
j++;
}
size -= file.stat.size;
i++;
}
log(`removed ${i} files`);
log(`removed ${j} files`);
} catch(e) {
log(LM_ERR, e.stack);
} finally {

View File

@@ -1,7 +1,9 @@
const { spawn } = require('child_process');
const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto');
const baseX = require('base-x');
const pidusage = require('pidusage');
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
const bs36 = baseX(BASE36);
@@ -45,10 +47,11 @@ async function touchFile(filename) {
}
function spawnProcess(cmd, opts) {
let {args, killAfter, onData, abort} = opts;
let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts;
killAfter = (killAfter ? killAfter : 120);//seconds
onData = (onData ? onData : () => {});
args = (args ? args : []);
onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds
return new Promise((resolve, reject) => { (async() => {
let resolved = false;
@@ -75,9 +78,19 @@ function spawnProcess(cmd, opts) {
reject({status: 'error', error, stdout, stderr});
});
//ждем процесс, контролируем его работу раз в секунду
let onUsageCounter = onUsageInterval;
while (!resolved) {
await sleep(1000);
killAfter -= 1;
onUsageCounter--;
if (onUsage && onUsageCounter <= 0) {
const stats = await pidusage(proc.pid);
onUsage(stats);
onUsageCounter = onUsageInterval;
}
killAfter--;
if (killAfter <= 0 || (abort && abort())) {
process.kill(proc.pid);
if (killAfter <= 0) {
@@ -91,6 +104,22 @@ function spawnProcess(cmd, opts) {
})().catch(reject); });
}
async function findFiles(callback, dir) {
if (!(callback && dir))
return;
let result = true;
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const found = path.resolve(dir, file.name);
if (file.isDirectory())
result = await findFiles(callback, found);
else
await callback(found);
}
return result;
}
module.exports = {
toBase36,
fromBase36,
@@ -99,5 +128,6 @@ module.exports = {
sleep,
randomHexString,
touchFile,
spawnProcess
spawnProcess,
findFiles
};