Compare commits

...

73 Commits

Author SHA1 Message Date
Book Pauk
c6e534b9db Merge branch 'hotfix/0.10.1' 2021-10-10 18:35:11 +07:00
Book Pauk
032ab6a85d Хотфикс для исправления проблемы с пустой БД storage при инициализации 2021-10-10 18:34:18 +07:00
Book Pauk
21716163cb Merge branch 'release/0.10.0-2' 2021-02-10 20:19:47 +07:00
Book Pauk
ca924148a5 Поправки багов 2021-02-10 20:18:41 +07:00
Book Pauk
37aa9b84ae Merge tag '0.10.0-1' into develop
0.10.0-1
2021-02-10 15:41:24 +07:00
Book Pauk
c7bd7d4d7d Merge branch 'release/0.10.0-1' 2021-02-10 15:41:19 +07:00
Book Pauk
d81a50e696 Поправки багов 2021-02-10 15:40:44 +07:00
Book Pauk
dda9943dbe Merge tag '0.10.0' into develop
0.10.0
2021-02-10 03:23:57 +07:00
Book Pauk
2b4b9f24a1 Merge branch 'release/0.10.0' 2021-02-10 03:23:50 +07:00
Book Pauk
2af77f22d6 Мелкая поправка 2021-02-10 03:22:20 +07:00
Book Pauk
f142e5812d Добавлена опция "Не включать строку статуса в обои" 2021-02-10 03:18:47 +07:00
Book Pauk
ed901fc181 Добавлена возможность загружать пользовательские обои, пока без синхронизации 2021-02-10 02:55:47 +07:00
Book Pauk
87a068899a Поправки wallpaper 2021-02-09 22:29:20 +07:00
Book Pauk
115f683128 Улучшение отображения селектора обоев 2021-02-09 21:55:19 +07:00
Book Pauk
111568fc2e Поправлен баг 2021-02-09 21:16:17 +07:00
Book Pauk
825136b5ff 0.10.0 2021-02-09 21:05:26 +07:00
Book Pauk
eae34b1121 История 2021-02-09 21:04:56 +07:00
Book Pauk
b9d7a6a3bb Убрал дебаг 2021-02-09 21:00:51 +07:00
Book Pauk
1e5375f8f9 Рефакторинг 2021-02-09 21:00:18 +07:00
Book Pauk
f597c603bf Добавил цвета для статусбара и разделителя 2021-02-09 18:43:43 +07:00
Book Pauk
b93dd0a59e Поправка 2021-02-09 18:08:13 +07:00
Book Pauk
a5740e4349 Доработки 2021-02-09 18:07:02 +07:00
Book Pauk
dacbd05911 Работа над двухстраничным режимом 2021-02-09 17:47:10 +07:00
Book Pauk
65c66e0feb Работа над двухстраничным режимом 2021-02-09 15:46:57 +07:00
Book Pauk
52f9131f99 Доработки двухстраничного режима 2021-02-04 20:34:25 +07:00
Book Pauk
cfc946ad12 Работа над двухстраничным режимом 2021-02-04 20:08:06 +07:00
Book Pauk
a207a0554c Работа на двухстраничным режимом 2021-02-04 15:55:12 +07:00
Book Pauk
675e898163 Работа над двухстраничным режимом 2021-02-04 15:18:32 +07:00
Book Pauk
d2167d8605 Работа над двухстраничным режимом 2021-02-02 18:09:21 +07:00
Book Pauk
de849d3447 Рефакторинг 2021-02-02 18:08:55 +07:00
Book Pauk
6c20b0b83e Улучшения SqliteConnectionPool 2021-02-01 18:05:32 +07:00
Book Pauk
a09b70a991 Рефакторинг WebSocketConnection, небольшие улучшения 2021-02-01 17:57:24 +07:00
Book Pauk
2427a3e08b Поправка версии node 2020-12-30 03:41:59 +07:00
Book Pauk
1104f9b850 Небольшая поправка 2020-12-24 21:39:39 +07:00
Book Pauk
dc48700e9e Небольшая поправка 2020-12-24 21:35:31 +07:00
Book Pauk
f0b0c39328 Поправки по результату тестирования, незначительные улучшения 2020-12-24 20:51:02 +07:00
Book Pauk
aad74cf682 Поправки по результату тестирования, оптимизации 2020-12-24 18:32:57 +07:00
Book Pauk
d449478204 Небольшое форматирование 2020-12-24 18:21:18 +07:00
Book Pauk
d4f6536caa Поправки по результату тестирования 2020-12-24 16:33:44 +07:00
Book Pauk
1eac00f71c Поправка багов 2020-12-24 00:44:38 +07:00
Book Pauk
ca1170a9f0 Поправки по результату тестирования 2020-12-24 00:25:54 +07:00
Book Pauk
79dda03bac Рефакторинг, плюс небольшое улучшение механизма загрузки шрифта 2020-12-23 22:38:52 +07:00
Book Pauk
6c8e0b8573 Поправил баг 2020-12-23 22:23:37 +07:00
Book Pauk
17c14722fd Рефакторинг 2020-12-23 21:17:39 +07:00
Book Pauk
48612ee118 Поправлен баг 2020-12-22 02:24:46 +07:00
Book Pauk
205c676999 Переименование YandexMoney -> ЮMoney 2020-12-21 19:50:27 +07:00
Book Pauk
54e0dd0478 В список недавних добавлена полоска прочитанного 2020-12-21 18:08:35 +07:00
Book Pauk
2de8d7515e Добалвлен крестик в строку поиска 2020-12-21 17:48:49 +07:00
Book Pauk
a251d16432 Merge tag '0.9.12-1' into develop
0.9.12-1
2020-12-19 21:23:43 +07:00
Book Pauk
599caba912 Merge branch 'release/0.9.12-1' 2020-12-19 21:23:38 +07:00
Book Pauk
3477c43465 Поправка по результату чтения логов 2020-12-19 21:19:58 +07:00
Book Pauk
200dac7946 Небольшая поправка 2020-12-19 21:17:31 +07:00
Book Pauk
e60829946d Merge tag '0.9.12' into develop
0.9.12
2020-12-19 03:23:32 +07:00
Book Pauk
ef12a84285 Merge branch 'release/0.9.12' 2020-12-19 03:23:27 +07:00
Book Pauk
6a18ae3f27 Версия 0.9.12 2020-12-19 03:22:47 +07:00
Book Pauk
a250e95950 Поправил баг 2020-12-19 03:07:59 +07:00
Book Pauk
b174ae452b Оптимизации проверок типа файла 2020-12-19 03:05:58 +07:00
Book Pauk
0b63bce357 Исправления багов 2020-12-19 02:47:06 +07:00
Book Pauk
de0d10e792 Мелкая поправка 2020-12-19 02:46:46 +07:00
Book Pauk
b358b340b4 Улучшение формирования оглавления 2020-12-19 00:20:11 +07:00
Book Pauk
455aba7f4f Мелкая поправка текста 2020-12-19 00:17:36 +07:00
Book Pauk
fde0437157 Добавлено извлечение схемы документа в ConvertPdfImages, мелкий рефакторинг 2020-12-18 23:56:55 +07:00
Book Pauk
480c95bd63 Добавлена возможность конвертирования pdf как набор изображений.
Добавлены соответствующие настройки в читалку.
2020-12-18 23:30:13 +07:00
Book Pauk
972f957685 Работа над вкладкой "Конвертирование" 2020-12-18 22:44:20 +07:00
Book Pauk
40ff04e5dc Работа над вкладкой "Конвертирование" 2020-12-18 21:48:08 +07:00
Book Pauk
b3c028bd7a Убрал устаревшее 2020-12-18 21:23:02 +07:00
Book Pauk
51ec6a54fa Переименования, небольшое улучшение html-title 2020-12-17 23:39:45 +07:00
Book Pauk
7a29b16ee8 Коментарии к 0.9.12 2020-12-17 23:37:00 +07:00
Book Pauk
7af6fd8248 Новая вкладка 2020-12-17 23:36:30 +07:00
Book Pauk
e1c93169b5 Добавлена вкладка "Конвертирование" 2020-12-17 23:35:56 +07:00
Book Pauk
f4716d5a1e Поправлен баг 2020-12-17 23:12:36 +07:00
Book Pauk
f5c06ce420 Добавлен парсинг оглавления из djvu, добавлено отображение атрибута alt изображений в ContentsPage 2020-12-17 20:57:29 +07:00
Book Pauk
9492f85d80 Merge tag '0.9.11-4' into develop
0.9.11-4
2020-12-16 21:17:05 +07:00
58 changed files with 1803 additions and 930 deletions

View File

@@ -13,8 +13,7 @@ class Misc {
]};
try {
await wsc.open();
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error)
throw new Error(config.error);
return config;

View File

@@ -19,8 +19,7 @@ class Reader {
let response = {};
try {
await wsc.open();
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
let prevResponse = false;
while (1) {// eslint-disable-line no-constant-condition
@@ -124,8 +123,7 @@ class Reader {
let response = null
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
@@ -210,8 +208,7 @@ class Reader {
async storage(request) {
let response = null;
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http

View File

@@ -1,185 +1,3 @@
import * as utils from '../share/utils';
const cleanPeriod = 60*1000;//1 минута
class WebSocketConnection {
//messageLifeTime в минутах (cleanPeriod)
constructor(messageLifeTime = 5) {
this.ws = null;
this.timer = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTime;
this.requestId = 0;
this.connecting = false;
}
addListener(listener) {
if (this.listeners.indexOf(listener) < 0)
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
}
//рассылаем сообщение и удаляем те обработчики, которые его получили
emit(mes, isError) {
const len = this.listeners.length;
if (len > 0) {
let newListeners = [];
for (const listener of this.listeners) {
let emitted = false;
if (isError) {
if (listener.onError)
listener.onError(mes);
emitted = true;
} else {
if (listener.onMessage) {
if (listener.requestId) {
if (listener.requestId === mes.requestId) {
listener.onMessage(mes);
emitted = true;
}
} else {
listener.onMessage(mes);
emitted = true;
}
} else {
emitted = true;
}
}
if (!emitted)
newListeners.push(listener);
}
this.listeners = newListeners;
}
return this.listeners.length != len;
}
open(url) {
return new Promise((resolve, reject) => { (async() => {
//Ожидаем окончания процесса подключения, если open уже был вызван
let i = 0;
while (this.connecting && i < 200) {//10 сек
await utils.sleep(50);
i++;
}
if (i >= 200)
this.connecting = false;
//проверим подключение, и если нет, то подключимся заново
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
this.connecting = true;
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
url = url || `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(url);
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
this.ws.onopen = (e) => {
this.connecting = false;
resolve(e);
};
this.ws.onmessage = (e) => {
try {
const mes = JSON.parse(e.data);
this.messageQueue.push({regTime: Date.now(), mes});
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (!this.emit(message.mes)) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} catch (e) {
this.emit(e.message, true);
}
};
this.ws.onerror = (e) => {
this.emit(e.message, true);
if (this.connecting) {
this.connecting = false;
reject(e);
}
};
}
})() });
}
//timeout в минутах (cleanPeriod)
message(requestId, timeout = 2) {
return new Promise((resolve, reject) => {
this.addListener({
requestId,
timeout,
onMessage: (mes) => {
resolve(mes);
},
onError: (e) => {
reject(e);
}
});
});
}
send(req) {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
const requestId = ++this.requestId;
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
return requestId;
} else {
throw new Error('WebSocket connection is not ready');
}
}
close() {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
this.ws.close();
}
}
periodicClean() {
try {
this.timer = null;
const now = Date.now();
//чистка listeners
let newListeners = [];
for (const listener of this.listeners) {
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
newListeners.push(listener);
} else {
if (listener.onError)
listener.onError('Время ожидания ответа истекло');
}
}
this.listeners = newListeners;
//чистка messageQueue
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} finally {
if (this.ws.readyState == WebSocket.OPEN) {
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
}
}
import WebSocketConnection from '../../server/core/WebSocketConnection';
export default new WebSocketConnection();

View File

@@ -270,6 +270,14 @@ body, html, #app {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
from {
transform: rotate(0deg);
} to {
transform: rotate(360deg);
}
}
.notify-button-icon {
font-size: 16px !important;
}

View File

@@ -67,6 +67,7 @@
<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>
@@ -145,25 +146,36 @@ class ContentsPage extends ContentsPageProps {
await this.$nextTick();
const pc = parsed.contents;
const newpc = [];
//преобразуем все, кроме первого, разделы body в title-subtitle
let curSubtitles = [];
let prevBodyIndex = -1;
for (let i = 0; i < pc.length; i++) {
const cont = pc[i];
if (prevBodyIndex != cont.bodyIndex)
curSubtitles = [];
const ims = parsed.images;
const newpc = [];
if (pc.length) {//если есть оглавление
//преобразуем все, кроме первого, разделы body в title-subtitle
let curSubtitles = [];
let prevBodyIndex = -1;
for (let i = 0; i < pc.length; i++) {
const cont = pc[i];
if (prevBodyIndex != cont.bodyIndex)
curSubtitles = [];
prevBodyIndex = cont.bodyIndex;
prevBodyIndex = cont.bodyIndex;
if (cont.bodyIndex > 1) {
if (cont.inset < 1) {
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
if (cont.bodyIndex > 1) {
if (cont.inset < 1) {
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
} else {
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
}
} else {
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
newpc.push(cont);
}
}
} else {//попробуем вытащить из images
for (let i = 0; i < ims.length; i++) {
const image = ims[i];
if (image.alt) {
newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
}
} else {
newpc.push(cont);
}
}
@@ -212,19 +224,18 @@ class ContentsPage extends ContentsPageProps {
//формируем newImages
const newImages = [];
const ims = parsed.images;
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.num}`;
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
const indentStyle = getIndentStyle(1);
const labelStyle = getLabelStyle(0);
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, id: image.id, local: image.local});
indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
}
this.images = newImages;
@@ -389,6 +400,10 @@ class ContentsPage extends ContentsPageProps {
transform: rotate(90deg);
}
.image-num {
font-size: 120%;
padding-bottom: 3px;
}
.image-type {
border: 1px solid black;
border-radius: 6px;

View File

@@ -3,10 +3,10 @@
<div class="box">
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
<div class="address">
<img class="logo" src="./assets/yandex.png">
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
<div class="para">{{ yandexAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
<img class="logo" src="./assets/yoomoney.png">
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">Пожертвовать</q-btn><br>
<div class="para">{{ yooAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
@@ -60,7 +60,7 @@ import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({
})
class DonateHelpPage extends Vue {
yandexAddress = '410018702323056';
yooAddress = '410018702323056';
paypalAddress = 'bookpauk@gmail.com';
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
@@ -69,8 +69,8 @@ class DonateHelpPage extends Vue {
created() {
}
donateYandexMoney() {
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank');
donateYooMoney() {
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
}
async copyAddress(address, prefix) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -68,7 +68,7 @@ class PasteTextPage extends Vue {
}
loadBuffer() {
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
this.close();
}

View File

@@ -39,9 +39,9 @@
<q-icon name="la la-copy" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
</button>
<button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
<q-icon name="la la-retweet" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip>
<button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
<q-icon name="la la-magic" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
</button>
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
@@ -131,6 +131,9 @@ import ContentsPage from './ContentsPage/ContentsPage.vue';
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager';
import wallpaperStorage from './share/wallpaperStorage';
import dynamicCss from '../../share/dynamicCss';
import rstore from '../../store/modules/reader';
import readerApi from '../../api/reader';
import miscApi from '../../api/misc';
@@ -194,6 +197,10 @@ export default @Component({
}
})();
},
dualPageMode(newValue) {
if (newValue)
this.stopScrolling();
},
},
})
class Reader extends Vue {
@@ -227,6 +234,7 @@ class Reader extends Vue {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
dualPageMode = false;
created() {
this.rstore = rstore;
@@ -269,6 +277,7 @@ class Reader extends Vue {
this.updateHeaderMinWidth();
(async() => {
await wallpaperStorage.init();
await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent);
@@ -317,6 +326,12 @@ class Reader extends Vue {
this.showToolButton = settings.showToolButton;
this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara;
this.djvuQuality = settings.djvuQuality;
this.pdfAsText = settings.pdfAsText;
this.pdfQuality = settings.pdfQuality;
this.dualPageMode = settings.dualPageMode;
this.userWallpapers = settings.userWallpapers;
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
this.$root.readerActionByKeyEvent = (event) => {
@@ -324,6 +339,30 @@ class Reader extends Vue {
}
this.updateHeaderMinWidth();
this.loadWallpapers();//no await
}
//wallpaper css
async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength();
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
this.wallpaperDataLength = wallpaperDataLength;
let newCss = '';
for (const wp of this.userWallpapers) {
const data = await wallpaperStorage.getData(wp.cssClass);
if (!data) {
//здесь будем восстанавливать данные с сервера
}
if (data) {
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
}
}
dynamicCss.replace('wallpapers', newCss);
}
}
async checkNewVersionAvailable() {
@@ -336,7 +375,7 @@ class Reader extends Vue {
let againMes = '';
if (this.isFirstNeedUpdateNotify) {
againMes = ' ЕЩЕ один раз';
againMes = ' еще один раз';
}
if (this.version != this.clientVersion)
@@ -345,9 +384,9 @@ class Reader extends Vue {
console.error(e);
} finally {
this.checkingNewVersion = false;
}
}
this.isFirstNeedUpdateNotify = false;
}
this.isFirstNeedUpdateNotify = false;
}
updateHeaderMinWidth() {
@@ -703,6 +742,12 @@ class Reader extends Vue {
}
}
convOptionsToggle() {
this.settingsToggle();
if (this.settingsActive)
this.$refs.settingsPage.selectedTab = 'convert';
}
helpToggle() {
this.helpActive = !this.helpActive;
if (this.helpActive) {
@@ -729,15 +774,9 @@ class Reader extends Vue {
}
}
refreshBook(mode) {
refreshBook() {
const mrb = this.mostRecentBook();
if (mrb) {
if (mode && mode == 'split') {
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, skipCheck: true, isText: true, force: true});
} else {
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
}
}
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
}
undoAction() {
@@ -774,10 +813,9 @@ class Reader extends Vue {
case 'loader':
case 'fullScreen':
case 'setPosition':
case 'scrolling':
case 'search':
case 'copyText':
case 'splitToPara':
case 'convOptions':
case 'refresh':
case 'contents':
case 'libs':
@@ -790,6 +828,13 @@ class Reader extends Vue {
classResult = classActive;
}
break;
case 'scrolling':
if (this.progressActive || this.dualPageMode) {
classResult = classDisabled;
} else if (this[`${action}Active`]) {
classResult = classActive;
}
break;
case 'undoAction':
if (this.actionCur <= 0)
classResult = classDisabled;
@@ -811,7 +856,6 @@ class Reader extends Vue {
case 'contents':
classResult = classDisabled;
break;
case 'splitToPara':
case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive)
@@ -973,10 +1017,13 @@ class Reader extends Vue {
if (!book) {
book = await readerApi.loadBook({
url,
skipCheck: (opts.skipCheck ? true : false),
isText: (opts.isText ? true : false),
uploadFileName,
enableSitesFilter: this.enableSitesFilter,
uploadFileName
skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false),
djvuQuality: this.djvuQuality,
pdfAsText: this.pdfAsText,
pdfQuality: this.pdfQuality,
},
(state) => {
progress.setState(state);
@@ -1102,8 +1149,8 @@ class Reader extends Vue {
case 'copyText':
this.copyTextToggle();
break;
case 'splitToPara':
this.refreshBook('split');
case 'convOptions':
this.convOptionsToggle();
break;
case 'refresh':
this.refreshBook();

View File

@@ -57,37 +57,6 @@
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog>
<Dialog ref="dialog3" v-model="liberamaTopVisible">
<template slot="header">
Здравствуйте, уважаемые читатели!
</template>
<div style="word-break: normal">
Создан новый ресурс:<br><br>
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
<br><br>
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
<ul>
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
</ul>
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
Спасибо, что вы с нами!
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div>
<span slot="footer">
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
</span>
</Dialog>
</div>
</template>
@@ -114,7 +83,6 @@ class ReaderDialogs extends Vue {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
liberamaTopVisible = false;
created() {
this.commit = this.$store.commit;
@@ -127,14 +95,12 @@ class ReaderDialogs extends Vue {
async init() {
await this.showWhatsNew();
await this.showDonation();
await this.showLiberamaTop();
}
loadSettings() {
const settings = this.settings;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
}
async showWhatsNew() {
@@ -171,7 +137,6 @@ class ReaderDialogs extends Vue {
openDonate() {
this.donationVisible = false;
this.liberamaTopVisible = false;
this.$emit('donate-toggle');
}
@@ -210,24 +175,8 @@ class ReaderDialogs extends Vue {
return this.$store.state.reader.donationRemindDate;
}
async showLiberamaTop() {
const today = utils.formatDate(new Date(), 'coDate');
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
await utils.sleep(3000);
this.liberamaTopVisible = true;
}
}
liberamaTopDialogDisable() {
this.liberamaTopVisible = false;
if (this.showLiberamaTopDialog2020) {
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
}
}
keyHook() {
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
return true;
return false;
}

View File

@@ -27,8 +27,11 @@
placeholder="Найти"
v-model="search"
@click.stop
/>
>
<template v-slot:append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch"/>
</template>
</q-input>
<span v-html="props.cols[2].label"></span>
</q-th>
</q-tr>
@@ -53,6 +56,7 @@
<div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">{{ props.row.desc.author }}</div>
<div>{{ props.row.desc.title }}</div>
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
</div>
</q-td>
@@ -106,7 +110,7 @@ export default @Component({
})
class RecentBooksPage extends Vue {
loading = false;
search = null;
search = '';
tableData = [];
columns = [];
pagination = {};
@@ -200,11 +204,13 @@ class RecentBooksPage extends Vue {
d.setTime(book.touchTime);
const t = utils.formatDate(d).split(' ');
let readPart = 0;
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
readPart = p/book.textLength;
perc = ` [${(readPart*100).toFixed(2)}%]`;
textLen = ` ${Math.round(book.textLength/1000)}k`;
}
@@ -223,6 +229,7 @@ class RecentBooksPage extends Vue {
author,
title: `${title}${perc}${textLen}`,
},
readPart,
descString: `${author}${title}${perc}${textLen}`,//для сортировки
url: book.url,
path: book.path,
@@ -244,6 +251,11 @@ class RecentBooksPage extends Vue {
this.updating = false;
}
resetSearch() {
this.search = '';
this.$refs.input.focus();
}
wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
const deci = num % 100;
@@ -346,6 +358,10 @@ class RecentBooksPage extends Vue {
white-space: normal;
}
.read-bar {
height: 3px;
background-color: #aaaaaa;
}
</style>
<style>

View File

@@ -26,6 +26,7 @@
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
<div v-show="tabsScrollable" class="q-pt-lg"/>
@@ -53,6 +54,10 @@
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
@@include('./include/PageMoveTab.inc');
</div>
<!-- Конвертирование ------------------------------------------------------------->
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
@@include('./include/ConvertTab.inc');
</div>
<!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel">
@@include('./include/OthersTab.inc');
@@ -74,9 +79,11 @@ import Component from 'vue-class-component';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
@@ -108,7 +115,7 @@ export default @Component({
},
vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName);
if (this.fontShifts[font] != newValue) {
if (this.fontShifts[font] != newValue || this.fontVertShift != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
}
@@ -125,6 +132,10 @@ export default @Component({
if (newValue != '' && this.pageChangeAnimation == 'flip')
this.pageChangeAnimation = '';
},
dualPageMode(newValue) {
if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
this.pageChangeAnimation = '';
},
textColor: function(newValue) {
this.textColorFiltered = newValue;
},
@@ -139,11 +150,25 @@ export default @Component({
if (hex.test(newValue))
this.backgroundColor = newValue;
},
dualDivColor(newValue) {
this.dualDivColorFiltered = newValue;
},
dualDivColorFiltered(newValue) {
if (hex.test(newValue))
this.dualDivColor = newValue;
},
statusBarColor(newValue) {
this.statusBarColorFiltered = newValue;
},
statusBarColorFiltered(newValue) {
if (hex.test(newValue))
this.statusBarColor = newValue;
},
},
})
class SettingsPage extends Vue {
selectedTab = 'profiles';
selectedViewTab = 'color';
selectedViewTab = 'mode';
selectedKeysTab = 'mouse';
form = {};
fontBold = false;
@@ -152,6 +177,7 @@ class SettingsPage extends Vue {
tabsScrollable = false;
textColorFiltered = '';
bgColorFiltered = '';
dualDivColorFiltered = '';
webFonts = [];
fonts = [];
@@ -212,12 +238,18 @@ class SettingsPage extends Vue {
this.vertShift = this.fontShifts[font] || 0;
this.textColorFiltered = this.textColor;
this.bgColorFiltered = this.backgroundColor;
this.dualDivColorFiltered = this.dualDivColor;
this.statusBarColorFiltered = this.statusBarColor;
}
get mode() {
return this.$store.state.config.mode;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
get settings() {
return this.$store.state.reader.settings;
}
@@ -247,9 +279,19 @@ class SettingsPage extends Vue {
get wallpaperOptions() {
let result = [{label: 'Нет', value: ''}];
for (let i = 1; i < 10; i++) {
const userWallpapers = _.cloneDeep(this.userWallpapers);
userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
for (const wp of userWallpapers) {
if (wallpaperStorage.keyExists(wp.cssClass))
result.push({label: wp.label, value: wp.cssClass});
}
for (let i = 1; i <= 17; i++) {
result.push({label: i, value: `paper${i}`});
}
return result;
}
@@ -273,13 +315,15 @@ class SettingsPage extends Vue {
let result = [
{label: 'Нет', value: ''},
{label: 'Вверх-вниз', value: 'downShift'},
{label: 'Вправо-влево', value: 'rightShift'},
(!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
{label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'},
];
if (this.wallpaper == '')
result.push({label: 'Листание', value: 'flip'});
(this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
];
result = result.filter(v => v);
return result;
}
@@ -342,6 +386,12 @@ class SettingsPage extends Vue {
case 'bg':
result += `background-color: ${this.backgroundColor};`
break;
case 'div':
result += `background-color: ${this.dualDivColor};`
break;
case 'statusbar':
result += `background-color: ${this.statusBarColor};`
break;
}
return result;
}
@@ -508,6 +558,71 @@ class SettingsPage extends Vue {
}
loadWallpaperFileClick() {
this.$refs.file.click();
}
loadWallpaperFile() {
const file = this.$refs.file.files[0];
if (file.size > 10*1024*1024) {
this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
return;
}
if (file.type != 'image/png' && file.type != 'image/jpeg') {
this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
return;
}
if (this.userWallpapers.length >= 100) {
this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
return;
}
this.$refs.file.value = '';
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
(async() => {
const data = e.target.result;
const key = utils.toHex(cryptoUtils.sha256(data));
const label = `#${key.substring(0, 4)}`;
const cssClass = `user-paper${key}`;
const newUserWallpapers = _.cloneDeep(this.userWallpapers);
const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
if (index < 0)
newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass))
await wallpaperStorage.setData(cssClass, data);
this.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass;
})();
}
reader.readAsDataURL(file);
}
}
async delWallpaper() {
if (this.wallpaper.indexOf('user-paper') == 0) {
const newUserWallpapers = [];
for (const wp of this.userWallpapers) {
if (wp.cssClass != this.wallpaper) {
newUserWallpapers.push(wp);
}
}
await wallpaperStorage.removeData(this.wallpaper);
this.userWallpapers = newUserWallpapers;
this.wallpaper = '';
}
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
@@ -544,7 +659,7 @@ class SettingsPage extends Vue {
margin-bottom: 5px;
}
.label-1 {
.label-1, .label-7 {
width: 75px;
}
@@ -556,7 +671,7 @@ class SettingsPage extends Vue {
width: 100px;
}
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
display: flex;
flex-direction: column;
justify-content: center;

View 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>

View File

@@ -65,22 +65,6 @@
<!---------------------------------------------->
<div class="part-header">Другое</div>
<div class="item row">
<div class="label-6">Обработка</div>
<div class="col row">
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Html-фильтр вырезает лишние элементы со<br>
страницы для определенных сайтов, таких как:<br>
samlib.ru<br>
www.fanfiction.net<br>
archiveofourown.org<br>
и других
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-6">Обработка</div>
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">

View File

@@ -7,6 +7,7 @@
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="mode" label="Режим" />
<q-tab name="color" label="Цвет" />
<q-tab name="font" label="Шрифт" />
<q-tab name="text" label="Текст" />
@@ -16,6 +17,10 @@
<div class="q-mb-sm"/>
<div class="col tab-panel">
<div v-if="selectedViewTab == 'mode'">
@@include('./ViewTab/Mode.inc');
</div>
<div v-if="selectedViewTab == 'color'">
@@include('./ViewTab/Color.inc');
</div>

View File

@@ -22,8 +22,6 @@
</q-icon>
</template>
</q-input>
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
</div>
</div>
@@ -36,7 +34,6 @@
v-model="bgColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
:disable="wallpaper != ''"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
@@ -48,11 +45,51 @@
</q-icon>
</template>
</q-input>
<div class="q-px-sm"/>
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
</div>
</div>
<div class="q-mt-md"/>
<div class="item row">
<div class="label-2">Обои</div>
<div class="col row items-center">
<q-select class="col-left no-mp" v-model="wallpaper" :options="wallpaperOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<template v-slot:selected-item="scope">
<div >{{ scope.opt.label }}</div>
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
</template>
<template v-slot:option="scope">
<q-item
v-bind="scope.itemProps"
v-on="scope.itemEvents"
>
<q-item-section style="min-width: 50px;">
<q-item-label v-html="scope.opt.label" />
</q-item-section>
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;"/>
</q-item>
</template>
</q-select>
<div class="q-px-xs"/>
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить файл обоев</q-tooltip>
</q-btn>
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить выбранные обои</q-tooltip>
</q-btn>
</div>
</div>
<div class="q-mt-sm"/>
<div class="item row">
<div class="label-2"></div>
<div class="col row items-center">
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
</div>
</div>
<input type="file" ref="file" @change="loadWallpaperFile" style='display: none;'/>

View File

@@ -0,0 +1,124 @@
<!---------------------------------------------->
<div class="hidden part-header">Режим</div>
<div class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="dualPageMode" size="xs" label="Двухстраничный режим" />
</div>
</div>
<div class="part-header">Страницы</div>
<div class="item row">
<div class="label-2">Отступ границ</div>
<div class="col row">
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Слева/справа от края экрана
</q-tooltip>
</NumInput>
<div class="q-px-sm"/>
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сверху/снизу от края экрана
</q-tooltip>
</NumInput>
</div>
</div>
<div v-show="dualPageMode" class="item row">
<div class="label-2">Отступ внутри</div>
<div class="col row">
<NumInput class="col-left" v-model="dualIndentLR" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Слева/справа внутри страницы
</q-tooltip>
</NumInput>
</div>
</div>
<div v-show="dualPageMode">
<div class="part-header">Разделитель</div>
<div class="item row no-wrap">
<div class="label-2">Цвет</div>
<div class="col-left row">
<q-input class="col-left no-mp"
outlined dense
v-model="dualDivColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
:disable="dualDivColorAsText"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('div')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="dualDivColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="q-px-xs"/>
<q-checkbox v-model="dualDivColorAsText" size="xs" label="Как у текста" />
</div>
<div class="item row">
<div class="label-2">Прозрачность</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
</div>
</div>
<div class="item row">
<div class="label-2">Ширина (px)</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivWidth" :min="0" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Ширина разделителя
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Высота (%)</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivHeight" :min="0" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Высота разделителя
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Пунктир</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivStrokeFill" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Заполнение пунктира
</q-tooltip>
</NumInput>
<div class="q-px-sm"/>
<NumInput class="col" v-model="dualDivStrokeGap" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Промежуток пунктира
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Ширина тени</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivShadowWidth" :min="0" :max="100"/>
</div>
</div>
</div>

View File

@@ -5,25 +5,53 @@
<div class="label-2">Статус</div>
<div class="col row">
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
<q-checkbox v-show="showStatusBar" class="q-ml-sm" v-model="statusBarTop" size="xs" label="Вверху/внизу" />
</div>
</div>
<div class="item row">
<div class="label-2">Высота</div>
<div class="col row">
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
<div v-show="showStatusBar" class="item row no-wrap">
<div class="label-2">Цвет</div>
<div class="col-left row">
<q-input class="col-left no-mp"
outlined dense
v-model="statusBarColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
:disable="statusBarColorAsText"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('statusbar')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="statusBarColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="q-px-xs"/>
<q-checkbox v-model="statusBarColorAsText" size="xs" label="Как у текста"/>
</div>
<div class="item row">
<div v-show="showStatusBar" class="item row">
<div class="label-2">Прозрачность</div>
<div class="col row">
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
</div>
</div>
<div class="item row">
<div v-show="showStatusBar" class="item row">
<div class="label-2">Высота</div>
<div class="col row">
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100"/>
</div>
</div>
<div v-show="showStatusBar" class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">

View File

@@ -15,23 +15,6 @@
</div>
</div>
<div class="item row">
<div class="label-2">Отступ</div>
<div class="col row">
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Слева/справа
</q-tooltip>
</NumInput>
<div class="q-px-sm"/>
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сверху/снизу
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Сдвиг</div>
<div class="col row">
@@ -123,7 +106,7 @@
<div class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
<q-checkbox v-model="imageFitWidth" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
</div>
</div>

View File

@@ -2,11 +2,11 @@ import {sleep} from '../../../share/utils';
export default class DrawHelper {
fontBySize(size) {
return `${size}px ${this.fontName}`;
return `${size}px '${this.fontName}'`;
}
fontByStyle(style) {
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px ${this.fontName}`;
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
}
measureText(text, style) {// eslint-disable-line no-unused-vars
@@ -19,6 +19,109 @@ export default class DrawHelper {
return this.context.measureText(text).width;
}
drawLine(line, lineIndex, baseLineIndex, sel, imageDrawn) {
/* line:
{
begin: Number,
end: Number,
first: Boolean,
last: Boolean,
parts: array of {
style: {bold: Boolean, italic: Boolean, center: Boolean},
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
text: String,
}
}*/
let out = '<div>';
let lineText = '';
let center = false;
let space = 0;
let j = 0;
//формируем строку
for (const part of line.parts) {
let tOpen = '';
tOpen += (part.style.bold ? '<b>' : '');
tOpen += (part.style.italic ? '<i>' : '');
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
let tClose = '';
tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : '');
tClose += (part.style.bold ? '</b>' : '');
let text = '';
if (lineIndex == 0 && this.searching) {
for (let k = 0; k < part.text.length; k++) {
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
j++;
}
} else
text = part.text;
if (text && text.trim() == '')
text = `<span style="white-space: pre">${text}</span>`;
lineText += `${tOpen}${text}${tClose}`;
center = center || part.style.center;
space = (part.style.space > space ? part.style.space : space);
//избражения
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
const img = part.image;
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > img.h) {
resize = `height: ${img.h}px`;
}
const left = (this.w - img.w)/2;
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (lineIndex - baseLineIndex - img.imageLine)*this.lineHeight;
if (img.local) {
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
} else {
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
}
}
imageDrawn.add(img.paraIndex);
}
if (img && img.id && img.inline) {
if (img.local) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > this.fontSize) {
resize = `height: ${this.fontSize - 3}px`;
}
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
}
} else {
//
}
}
}
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
if ((line.first || space) && !center) {
let p = (line.first ? this.p : 0);
p = (space ? p + this.p*space : p);
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
}
if (line.last || center)
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
out += lineText + '</div>';
return out;
}
drawPage(lines, isScrolling) {
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
return '';
@@ -26,134 +129,65 @@ export default class DrawHelper {
const font = this.fontByStyle({});
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` +
const boxH = this.h + (isScrolling ? this.lineHeight : 0);
let out = `<div class="row no-wrap" style="width: ${this.boxW}px; height: ${boxH}px;` +
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
let imageDrawn = new Set();
let imageDrawn1 = new Set();
let imageDrawn2 = new Set();
let len = lines.length;
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
len = (len > lineCount ? lineCount : len);
for (let i = 0; i < len; i++) {
const line = lines[i];
/* line:
{
begin: Number,
end: Number,
first: Boolean,
last: Boolean,
parts: array of {
style: {bold: Boolean, italic: Boolean, center: Boolean},
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
text: String,
}
}*/
let sel = new Set();
//поиск
if (i == 0 && this.searching) {
let pureText = '';
for (const part of line.parts) {
pureText += part.text;
}
pureText = pureText.toLowerCase();
let j = 0;
while (1) {// eslint-disable-line no-constant-condition
j = pureText.indexOf(this.needle, j);
if (j >= 0) {
for (let k = 0; k < this.needle.length; k++) {
sel.add(j + k);
}
} else
break;
j++;
}
//поиск
let sel = new Set();
if (len > 0 && this.searching) {
const line = lines[0];
let pureText = '';
for (const part of line.parts) {
pureText += part.text;
}
let lineText = '';
let center = false;
let space = 0;
pureText = pureText.toLowerCase();
let j = 0;
//формируем строку
for (const part of line.parts) {
let tOpen = '';
tOpen += (part.style.bold ? '<b>' : '');
tOpen += (part.style.italic ? '<i>' : '');
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
let tClose = '';
tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : '');
tClose += (part.style.bold ? '</b>' : '');
let text = '';
if (i == 0 && this.searching) {
for (let k = 0; k < part.text.length; k++) {
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
j++;
while (1) {// eslint-disable-line no-constant-condition
j = pureText.indexOf(this.needle, j);
if (j >= 0) {
for (let k = 0; k < this.needle.length; k++) {
sel.add(j + k);
}
} else
text = part.text;
if (text && text.trim() == '')
text = `<span style="white-space: pre">${text}</span>`;
lineText += `${tOpen}${text}${tClose}`;
center = center || part.style.center;
space = (part.style.space > space ? part.style.space : space);
//избражения
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
const img = part.image;
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > img.h) {
resize = `height: ${img.h}px`;
}
const left = (this.w - img.w)/2;
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (i - img.imageLine)*this.lineHeight;
if (img.local) {
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
} else {
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
}
}
imageDrawn.add(img.paraIndex);
}
if (img && img.id && img.inline) {
if (img.local) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > this.fontSize) {
resize = `height: ${this.fontSize - 3}px`;
}
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
}
} else {
//
}
}
break;
j++;
}
}
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
if ((line.first || space) && !center) {
let p = (line.first ? this.p : 0);
p = (space ? p + this.p*space : p);
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
//отрисовка строк
if (!this.dualPageMode) {
out += `<div class="fit">`;
for (let i = 0; i < len; i++) {
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
}
out += `</div>`;
} else {
//левая страница
out += `<div style="width: ${this.w}px; margin-left: ${this.dualIndentLR}px; position: relative;">`;
const l2 = (this.pageRowsCount > len ? len : this.pageRowsCount);
for (let i = 0; i < l2; i++) {
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
}
out += '</div>';
if (line.last || center)
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
//разделитель
out += `<div style="width: ${this.dualIndentLR*2}px;"></div>`;
out += (i > 0 ? '<br>' : '') + lineText;
//правая страница
out += `<div style="width: ${this.w}px; margin-right: ${this.dualIndentLR}px; position: relative;">`;
for (let i = l2; i < len; i++) {
out += this.drawLine(lines[i], i, l2, sel, imageDrawn2);
}
out += '</div>';
}
out += '</div>';
@@ -179,8 +213,8 @@ export default class DrawHelper {
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
const barWidth = w - w1 - w2 - fh2;
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarColor);
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarColor);
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarRgbaColor);
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarRgbaColor);
}
if (w1 <= w)
@@ -193,12 +227,12 @@ export default class DrawHelper {
let out = `<div class="layout" style="` +
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
`color: ${this.statusBarColor}">`;
`color: ${this.statusBarRgbaColor}">`;
const fontSize = statusBarHeight*0.75;
const font = 'bold ' + this.fontBySize(fontSize);
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarColor);
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarRgbaColor);
const date = new Date();
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
@@ -207,7 +241,7 @@ export default class DrawHelper {
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
out += this.drawPercentBar(this.realWidth/2 + fontSize, 2, this.realWidth/2 - timeW - 3*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
out += '</div>';
return out;
@@ -274,7 +308,7 @@ export default class DrawHelper {
}
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
const s = this.w + this.fontSize;
const s = this.boxW + this.fontSize;
if (isDown) {
page1.style.transform = `translateX(${s}px)`;

View File

@@ -0,0 +1,93 @@
@keyframes page1-animation-thaw {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes page2-animation-thaw {
0% { opacity: 1; }
100% { opacity: 0; }
}
.paper1 {
background: url("images/paper1.jpg") center;
background-size: 100% 100%;
}
.paper2 {
background: url("images/paper2.jpg") center;
background-size: 100% 100%;
}
.paper3 {
background: url("images/paper3.jpg") center;
background-size: 100% 100%;
}
.paper4 {
background: url("images/paper4.jpg") center;
background-size: 100% 100%;
}
.paper5 {
background: url("images/paper5.jpg") center;
background-size: 100% 100%;
}
.paper6 {
background: url("images/paper6.jpg") center;
background-size: 100% 100%;
}
.paper7 {
background: url("images/paper7.jpg") center;
background-size: 100% 100%;
}
.paper8 {
background: url("images/paper8.jpg") center;
background-size: 100% 100%;
}
.paper9 {
background: url("images/paper9.jpg");
}
.paper10 {
background: url("images/paper10.png") center;
background-size: 100% 100%;
}
.paper11 {
background: url("images/paper11.png") center;
background-size: 100% 100%;
}
.paper12 {
background: url("images/paper12.png") center;
background-size: 100% 100%;
}
.paper13 {
background: url("images/paper13.png") center;
background-size: 100% 100%;
}
.paper14 {
background: url("images/paper14.png") center;
background-size: 100% 100%;
}
.paper15 {
background: url("images/paper15.png") center;
background-size: 100% 100%;
}
.paper16 {
background: url("images/paper16.png") center;
background-size: 100% 100%;
}
.paper17 {
background: url("images/paper17.png") center;
background-size: 100% 100%;
}

View File

@@ -1,8 +1,8 @@
<template>
<div ref="main" class="main">
<div class="layout back" @wheel.prevent.stop="onMouseWheel">
<div v-html="background"></div>
<!-- img -->
<div class="absolute" v-html="background"></div>
<div class="absolute" v-html="pageDivider"></div>
</div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
@@ -27,7 +27,7 @@
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick">
</div>
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
</div>
@@ -40,7 +40,10 @@ import Component from 'vue-class-component';
import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import './TextPage.css';
import * as utils from '../../../share/utils';
import bookManager from '../share/bookManager';
import DrawHelper from './DrawHelper';
import rstore from '../../../store/modules/reader';
@@ -74,6 +77,7 @@ class TextPage extends Vue {
clickControl = true;
background = null;
pageDivider = null;
page1 = null;
page2 = null;
statusBar = null;
@@ -110,7 +114,11 @@ class TextPage extends Vue {
this.debouncedDrawStatusBar = _.throttle(() => {
this.drawStatusBar();
}, 60);
}, 60);
this.debouncedDrawPageDividerAndOrnament = _.throttle(() => {
this.drawPageDividerAndOrnament();
}, 65);
this.debouncedLoadSettings = _.debounce(() => {
this.loadSettings();
@@ -152,7 +160,7 @@ class TextPage extends Vue {
const wideLetter = 'Щ';
//preloaded fonts
this.fontList = [`12px ${this.fontName}`];
this.fontList = [`12px '${this.fontName}'`];
//widths
this.realWidth = this.$refs.main.clientWidth;
@@ -161,14 +169,16 @@ class TextPage extends Vue {
this.$refs.layoutEvents.style.width = this.realWidth + 'px';
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
this.w = this.realWidth - 2*this.indentLR;
const dual = (this.dualPageMode ? 2 : 1);
this.boxW = this.realWidth - 2*this.indentLR;
this.w = this.boxW/dual - (this.dualPageMode ? 2*this.dualIndentLR : 0);
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
this.h = this.scrollHeight - 2*this.indentTB;
this.lineHeight = this.fontSize + this.lineInterval;
this.pageLineCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
this.$refs.scrollingPage1.style.width = this.w + 'px';
this.$refs.scrollingPage2.style.width = this.w + 'px';
this.lineHeight = this.fontSize + this.lineInterval;
this.pageRowsCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
this.pageLineCount = (this.dualPageMode ? this.pageRowsCount*2 : this.pageRowsCount)
//stuff
this.currentAnimation = '';
@@ -180,7 +190,10 @@ class TextPage extends Vue {
this.$refs.statusBar.style.left = '0px';
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
const sbColor = (this.statusBarColorAsText ? this.textColor : this.statusBarColor);
this.statusBarRgbaColor = this.hex2rgba(sbColor || '#000000', this.statusBarColorAlpha);
const ddColor = (this.dualDivColorAsText ? this.textColor : this.dualDivColor);
this.dualDivRgbaColor = this.hex2rgba(ddColor || '#000000', this.dualDivColorAlpha);
//drawHelper
this.drawHelper.realWidth = this.realWidth;
@@ -188,10 +201,20 @@ class TextPage extends Vue {
this.drawHelper.lastBook = this.lastBook;
this.drawHelper.book = this.book;
this.drawHelper.parsed = this.parsed;
this.drawHelper.pageRowsCount = this.pageRowsCount;
this.drawHelper.pageLineCount = this.pageLineCount;
this.drawHelper.dualPageMode = this.dualPageMode;
this.drawHelper.dualIndentLR = this.dualIndentLR;
/*this.drawHelper.dualDivWidth = this.dualDivWidth;
this.drawHelper.dualDivHeight = this.dualDivHeight;
this.drawHelper.dualDivRgbaColor = this.dualDivRgbaColor;
this.drawHelper.dualDivStrokeFill = this.dualDivStrokeFill;
this.drawHelper.dualDivStrokeGap = this.dualDivStrokeGap;
this.drawHelper.dualDivShadowWidth = this.dualDivShadowWidth;*/
this.drawHelper.backgroundColor = this.backgroundColor;
this.drawHelper.statusBarColor = this.statusBarColor;
this.drawHelper.statusBarRgbaColor = this.statusBarRgbaColor;
this.drawHelper.fontStyle = this.fontStyle;
this.drawHelper.fontWeight = this.fontWeight;
this.drawHelper.fontSize = this.fontSize;
@@ -200,6 +223,7 @@ class TextPage extends Vue {
this.drawHelper.textColor = this.textColor;
this.drawHelper.textShift = this.textShift;
this.drawHelper.p = this.p;
this.drawHelper.boxW = this.boxW;
this.drawHelper.w = this.w;
this.drawHelper.h = this.h;
this.drawHelper.indentLR = this.indentLR;
@@ -228,32 +252,33 @@ class TextPage extends Vue {
//parsed
if (this.parsed) {
this.parsed.p = this.p;
this.parsed.w = this.w;// px, ширина текста
this.parsed.font = this.font;
this.parsed.fontSize = this.fontSize;
this.parsed.wordWrap = this.wordWrap;
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
let t = wideLetter;
if (!this.drawHelper.measureText(t, {}))
let wideLine = wideLetter;
if (!this.drawHelper.measureText(wideLine, {}))
throw new Error('Ошибка measureText');
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
this.parsed.maxWordLength = t.length - 1;
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
this.parsed.lineHeight = this.lineHeight;
this.parsed.showImages = this.showImages;
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
this.parsed.imageHeightLines = this.imageHeightLines;
this.parsed.imageFitWidth = this.imageFitWidth;
this.parsed.compactTextPerc = this.compactTextPerc;
while (this.drawHelper.measureText(wideLine, {}) < this.w) wideLine += wideLetter;
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
this.parsed.setSettings({
p: this.p,
w: this.w,
font: this.font,
fontSize: this.fontSize,
wordWrap: this.wordWrap,
cutEmptyParagraphs: this.cutEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs,
maxWordLength: wideLine.length - 1,
lineHeight: this.lineHeight,
showImages: this.showImages,
showInlineImagesInCenter: this.showInlineImagesInCenter,
imageHeightLines: this.imageHeightLines,
imageFitWidth: this.imageFitWidth,
compactTextPerc: this.compactTextPerc,
testWidth: 0,
measureText: this.drawHelper.measureText.bind(this.drawHelper),
});
}
//scrolling page
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
const pageSpace = this.scrollHeight - this.pageRowsCount*this.lineHeight;
let top = pageSpace/2;
if (this.showStatusBar)
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
@@ -262,14 +287,14 @@ class TextPage extends Vue {
page1.perspective = page2.perspective = '3072px';
page1.width = page2.width = this.w + this.indentLR + 'px';
page1.width = page2.width = this.boxW + this.indentLR + 'px';
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
page1.top = page2.top = top + 'px';
page1.left = page2.left = this.indentLR + 'px';
page1 = this.$refs.scrollingPage1.style;
page2 = this.$refs.scrollingPage2.style;
page1.width = page2.width = this.w + this.indentLR + 'px';
page1.width = page2.width = this.boxW + this.indentLR + 'px';
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
}
@@ -333,20 +358,36 @@ class TextPage extends Vue {
if (!omitLoadFonts)
await this.loadFonts();
this.draw();
// ширина шрифта некоторое время выдается неверно, поэтому
if (!omitLoadFonts) {
const parsed = this.parsed;
let i = 0;
const t = this.parsed.testText;
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
if (omitLoadFonts) {
this.draw();
} else {
// ширина шрифта некоторое время выдается неверно,
// не удалось событийно отловить этот момент, поэтому костыль
while (this.checkingFont) {
this.stopCheckingFont = true;
await utils.sleep(100);
}
if (this.parsed === parsed) {
this.parsed.testWidth = this.drawHelper.measureText(t, {});
this.draw();
this.checkingFont = true;
this.stopCheckingFont = false;
try {
const parsed = this.parsed;
let i = 0;
const t = 'Это тестовый текст. Его ширина выдается системой неправильно некоторое время.';
let twprev = 0;
//5 секунд проверяем изменения шрифта
while (!this.stopCheckingFont && i++ < 50 && this.parsed === parsed) {
const tw = this.drawHelper.measureText(t, {});
if (tw !== twprev) {
this.parsed.setSettings({testWidth: tw});
this.draw();
twprev = tw;
}
await utils.sleep(100);
}
} finally {
this.checkingFont = false;
}
}
}
@@ -430,8 +471,18 @@ class TextPage extends Vue {
}
setBackground() {
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` background-color: ${this.backgroundColor}"></div>`;
if (this.wallpaperIgnoreStatusBar) {
this.background = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` background-color: ${this.backgroundColor}">` +
`<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
`</div>` +
`</div>`;
} else {
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` background-color: ${this.backgroundColor}"></div>`;
}
}
async onResize() {
@@ -449,7 +500,7 @@ class TextPage extends Vue {
}
get font() {
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontName}`;
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
}
onPage1TransitionEnd() {
@@ -490,7 +541,7 @@ class TextPage extends Vue {
async startTextScrolling() {
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
this.linesDown.length <= this.pageLineCount) {
this.linesDown.length <= this.pageLineCount || this.dualPageMode) {
this.doStopScrolling();
return;
}
@@ -608,6 +659,7 @@ class TextPage extends Vue {
if (!this.pageChangeAnimation)
this.debouncedPrepareNextPage();
this.debouncedDrawStatusBar();
this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true);
@@ -747,6 +799,25 @@ class TextPage extends Vue {
}
}
drawPageDividerAndOrnament() {
if (this.dualPageMode) {
this.pageDivider = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
`<div class="fit row justify-center items-center no-wrap">` +
`<div style="height: ${Math.round(this.scrollHeight*this.dualDivHeight/100)}px; width: ${this.dualDivWidth}px; ` +
`box-shadow: 0 0 ${this.dualDivShadowWidth}px ${this.dualDivRgbaColor}; ` +
`background-image: url(&quot;data:image/svg+xml;utf8,<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'>` +
`<line x1='${this.dualDivWidth/2}' y1='0' x2='${this.dualDivWidth/2}' y2='100%' stroke='${this.dualDivRgbaColor}' ` +
`stroke-width='${this.dualDivWidth}' stroke-dasharray='${this.dualDivStrokeFill} ${this.dualDivStrokeGap}'/>` +
`</svg>&quot;);">` +
`</div>` +
`</div>` +
`</div>`;
} else {
this.pageDivider = null;
}
}
blinkCachedLoadMessage(state) {
if (state === 'finish') {
this.statusBarMessage = '';
@@ -1161,60 +1232,3 @@ class TextPage extends Vue {
}
</style>
<style>
.paper1 {
background: url("images/paper1.jpg") center;
background-size: cover;
}
.paper2 {
background: url("images/paper2.jpg") center;
background-size: cover;
}
.paper3 {
background: url("images/paper3.jpg") center;
background-size: cover;
}
.paper4 {
background: url("images/paper4.jpg") center;
background-size: cover;
}
.paper5 {
background: url("images/paper5.jpg") center;
background-size: cover;
}
.paper6 {
background: url("images/paper6.jpg") center;
background-size: cover;
}
.paper7 {
background: url("images/paper7.jpg") center;
background-size: cover;
}
.paper8 {
background: url("images/paper8.jpg") center;
background-size: cover;
}
.paper9 {
background: url("images/paper9.jpg");
}
@keyframes page1-animation-thaw {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes page2-animation-thaw {
0% { opacity: 1; }
100% { opacity: 0; }
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -4,23 +4,55 @@ import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
// defaults
const defaultSettings = {
p: 30, //px, отступ параграфа
w: 500, //px, ширина страницы
font: '', //css описание шрифта
fontSize: 20, //px, размер шрифта
wordWrap: false, //перенос по слогам
cutEmptyParagraphs: false, //убирать пустые параграфы
addEmptyParagraphs: 0, //добавлять n пустых параграфов перед непустым
maxWordLength: 500, //px, максимальная длина слова без пробелов
lineHeight: 26, //px, высота строки
showImages: true, //показыввать изображения
showInlineImagesInCenter: true, //выносить изображения в центр, работает на этапе первичного парсинга (parse)
imageHeightLines: 100, //кол-во строк, максимальная высота изображения
imageFitWidth: true, //ширина изображения не более ширины страницы
dualPageMode: false, //двухстраничный режим
compactTextPerc: 0, //проценты, степень компактности текста
testWidth: 0, //ширина тестовой строки, пересчитывается извне при изменении шрифта браузером
isTesting: false, //тестовый режим
//заглушка, измеритель ширины текста
measureText: (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
},
};
//for splitToSlogi()
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
const soglas = new Set([
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
]);
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
const alpha = new Set([...glas, ...soglas, ...znak]);
export default class BookParser {
constructor(settings) {
if (settings) {
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
}
constructor(settings = {}) {
this.sets = {};
// defaults
this.p = 30;// px, отступ параграфа
this.w = 300;// px, ширина страницы
this.wordWrap = false;// перенос по слогам
//заглушка
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
};
this.setSettings(defaultSettings);
this.setSettings(settings);
}
setSettings(settings = {}) {
this.sets = Object.assign({}, this.sets, settings);
this.measureText = this.sets.measureText;
}
async parse(data, callback) {
if (!callback)
callback = () => {};
@@ -76,6 +108,7 @@ export default class BookParser {
*/
const getImageDimensions = (binaryId, binaryType, data) => {
return new Promise ((resolve, reject) => { (async() => {
data = data.replace(/[\n\r\s]/g, '');
const i = new Image();
let resolved = false;
i.onload = () => {
@@ -120,14 +153,59 @@ export default class BookParser {
})().catch(reject); });
};
const newParagraph = (text, len, addIndex) => {
const correctCurrentPara = () => {
//коррекция текущего параграфа
if (paraIndex >= 0) {
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим непустым
if (p.text.trim() != '') {
for (let i = 0; i < 2; i++) {
para[paraIndex] = {
index: paraIndex,
offset: paraOffset,
length: 1,
text: ' ',
addIndex: i + 1,
};
paraIndex++;
paraOffset++;
}
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
}
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
let newParaText = p.text.trim();
newParaText = (newParaText.length ? newParaText : ' ');
const ldiff = p.text.length - newParaText.length;
if (ldiff != 0) {
p.text = newParaText;
p.length -= ldiff;
}
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
paraOffset += p.length;
}
};
const newParagraph = (text = '', len = 0) => {
correctCurrentPara();
//новый параграф
paraIndex++;
let p = {
index: paraIndex,
offset: paraOffset,
length: len,
text: text,
addIndex: (addIndex ? addIndex : 0),
addIndex: 0,
};
if (inSubtitle) {
@@ -137,53 +215,26 @@ export default class BookParser {
}
para[paraIndex] = p;
paraOffset += p.length;
paraOffset += len;
};
const growParagraph = (text, len) => {
if (paraIndex < 0) {
newParagraph(' ', 1);
newParagraph();
growParagraph(text, len);
return;
}
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
paraIndex--;
for (let i = 0; i < 2; i++) {
newParagraph(' ', 1, i + 1);
}
paraIndex++;
p.index = paraIndex;
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);
}
p.length += len;
p.text += text;
if (inSubtitle) {
curSubtitle.title += text;
} else if (inTitle) {
curTitle.title += text;
}
para[paraIndex] = p;
paraOffset += p.length;
const p = para[paraIndex];
p.length += len;
p.text += text;
paraOffset += len;
};
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
@@ -196,8 +247,8 @@ export default class BookParser {
if (tag == 'binary') {
let attrs = sax.getAttrsSync(tail);
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
binaryType = (binaryType == 'image/jpg' ? 'image/jpeg' : binaryType);
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
binaryId = (attrs.id.value ? attrs.id.value : '');
}
@@ -205,26 +256,31 @@ export default class BookParser {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
if (href[0] == '#') {//local
imageNum++;
if (inPara && !this.showInlineImagesInCenter && !center)
if (inPara && !this.sets.showInlineImagesInCenter && !center)
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
else
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
this.images.push({paraIndex, num: imageNum, id, local});
this.images.push({paraIndex, num: imageNum, id, local, alt});
if (inPara && this.showInlineImagesInCenter)
newParagraph(' ', 1);
if (inPara && this.sets.showInlineImagesInCenter)
newParagraph();
} else {//external
imageNum++;
dimPromises.push(getExternalImageDimensions(href));
if (!this.sets.isTesting) {
dimPromises.push(getExternalImageDimensions(href));
} else {
dimPromises.push(this.sets.getExternalImageDimensions(this, href));
}
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
this.images.push({paraIndex, num: imageNum, id, local});
this.images.push({paraIndex, num: imageNum, id, local, alt});
}
}
}
@@ -263,25 +319,25 @@ export default class BookParser {
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
});
if (ann.length)
newParagraph(' ', 1);
newParagraph();
}
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
const bt = utils.getBookTitle(fb2);
if (bt.sequence) {
newParagraph(bt.sequence, bt.sequence.length);
newParagraph(' ', 1);
newParagraph();
}
}
if (!isFirstBody)
newParagraph(' ', 1);
newParagraph();
isFirstBody = false;
bodyIndex++;
}
if (tag == 'title') {
newParagraph(' ', 1);
newParagraph();
isFirstTitlePara = true;
bold = true;
center = true;
@@ -293,7 +349,7 @@ export default class BookParser {
if (tag == 'section') {
if (!isFirstSection)
newParagraph(' ', 1);
newParagraph();
isFirstSection = false;
sectionLevel++;
}
@@ -304,7 +360,7 @@ export default class BookParser {
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
if (!(tag == 'p' && isFirstTitlePara))
newParagraph(' ', 1);
newParagraph();
if (tag == 'p') {
inPara = true;
isFirstTitlePara = false;
@@ -312,7 +368,7 @@ export default class BookParser {
}
if (tag == 'subtitle') {
newParagraph(' ', 1);
newParagraph();
isFirstTitlePara = true;
bold = true;
center = true;
@@ -333,11 +389,12 @@ export default class BookParser {
}
if (tag == 'poem') {
newParagraph(' ', 1);
newParagraph();
}
if (tag == 'text-author') {
newParagraph(' ', 1);
newParagraph();
bold = true;
space += 1;
}
}
@@ -379,15 +436,15 @@ export default class BookParser {
if (tag == 'epigraph' || tag == 'annotation') {
italic = false;
space -= 1;
if (tag == 'annotation')
newParagraph(' ', 1);
newParagraph();
}
if (tag == 'stanza') {
newParagraph(' ', 1);
newParagraph();
}
if (tag == 'text-author') {
bold = false;
space -= 1;
}
}
@@ -404,17 +461,14 @@ export default class BookParser {
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
text = he.decode(text);
text = text.replace(/>/g, '&gt;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/[\t\n\r\xa0]/g, ' ');
if (text && text.trim() == '')
text = (text.indexOf(' ') >= 0 ? ' ' : '');
text = ' ';
if (!text)
return;
text = text.replace(/[\t\n\r\xa0]/g, ' ');
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
switch (path) {
case '/fictionbook/description/title-info/author/first-name':
@@ -452,24 +506,31 @@ export default class BookParser {
fb2.annotation += text;
}
let tOpen = (center ? '<center>' : '');
tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
tOpen += (space ? `<space w="${space}">` : '');
let tClose = (space ? '</space>' : '');
tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
tClose += (center ? '</center>' : '');
if (binaryId) {
if (!this.sets.isTesting) {
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
} else {
dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
}
}
if (path.indexOf('/fictionbook/body/title') == 0 ||
path.indexOf('/fictionbook/body/section') == 0 ||
path.indexOf('/fictionbook/body/epigraph') == 0
) {
growParagraph(`${tOpen}${text}${tClose}`, text.length);
}
let tOpen = (center ? '<center>' : '');
tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
tOpen += (space ? `<space w="${space}">` : '');
let tClose = (space ? '</space>' : '');
tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
tClose += (center ? '</center>' : '');
if (binaryId) {
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
if (text != ' ')
growParagraph(`${tOpen}${text}${tClose}`, text.length);
else
growParagraph(' ', 1);
}
};
@@ -481,6 +542,7 @@ export default class BookParser {
await sax.parse(data, {
onStartNode, onEndNode, onTextNode, onProgress
});
correctCurrentPara();
if (dimPromises.length) {
try {
@@ -541,9 +603,19 @@ export default class BookParser {
let style = {};
let image = {};
//оптимизация по памяти
const copyStyle = (s) => {
const r = {};
for (const prop in s) {
if (s[prop])
r[prop] = s[prop];
}
return r;
};
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
result.push({
style: Object.assign({}, style),
style: copyStyle(style),
image,
text
});
@@ -588,7 +660,7 @@ export default class BookParser {
img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({
style: Object.assign({}, style),
style: copyStyle(style),
image: img,
text: ''
});
@@ -631,7 +703,7 @@ export default class BookParser {
});
//длинные слова (или белиберду без пробелов) тоже разобьем
const maxWordLength = this.maxWordLength;
const maxWordLength = this.sets.maxWordLength;
const parts = result;
result = [];
for (const part of parts) {
@@ -644,7 +716,7 @@ export default class BookParser {
spaceIndex = i;
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) {
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.sets.w - this.sets.p) {
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
spaceIndex = -1;
@@ -662,86 +734,87 @@ export default class BookParser {
splitToSlogi(word) {
let result = [];
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
const soglas = new Set([
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
]);
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
const alpha = new Set([...glas, ...soglas, ...znak]);
let slog = '';
let slogLen = 0;
const len = word.length;
word += ' ';
for (let i = 0; i < len; i++) {
slog += word[i];
if (alpha.has(word[i]))
slogLen++;
if (len > 3) {
let slog = '';
let slogLen = 0;
word += ' ';
for (let i = 0; i < len; i++) {
slog += word[i];
if (alpha.has(word[i]))
slogLen++;
if (slogLen > 1 && i < len - 2 && (
//гласная, а следом не 2 согласные буквы
(glas.has(word[i]) && !(soglas.has(word[i + 1]) &&
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) ||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) &&
soglas.has(word[i]) && soglas.has(word[i + 1]) &&
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) &&
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) ||
//мягкий или твердый знак или Й
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
(word[i] == '-')
) &&
//нельзя оставлять окончания на ь, ъ, й
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
if (slogLen > 1 && i < len - 2 && (
//гласная, а следом не 2 согласные буквы
(glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) ||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) ||
//мягкий или твердый знак или Й
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
(word[i] == '-')
) &&
//нельзя оставлять окончания на ь, ъ, й
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
) {
result.push(slog);
slog = '';
slogLen = 0;
) {
result.push(slog);
slog = '';
slogLen = 0;
}
}
if (slog)
result.push(slog);
} else {
result.push(word);
}
if (slog)
result.push(slog);
return result;
}
parsePara(paraIndex) {
const para = this.para[paraIndex];
const s = this.sets;
//перераспарсиваем только при изменении одного из параметров
if (!this.force &&
para.parsed &&
para.parsed.testWidth === this.testWidth &&
para.parsed.w === this.w &&
para.parsed.p === this.p &&
para.parsed.wordWrap === this.wordWrap &&
para.parsed.maxWordLength === this.maxWordLength &&
para.parsed.font === this.font &&
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs &&
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
para.parsed.showImages === this.showImages &&
para.parsed.imageHeightLines === this.imageHeightLines &&
para.parsed.imageFitWidth === this.imageFitWidth &&
para.parsed.compactTextPerc === this.compactTextPerc
para.parsed.p === s.p &&
para.parsed.w === s.w &&
para.parsed.font === s.font &&
para.parsed.fontSize === s.fontSize &&
para.parsed.wordWrap === s.wordWrap &&
para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
para.parsed.maxWordLength === s.maxWordLength &&
para.parsed.lineHeight === s.lineHeight &&
para.parsed.showImages === s.showImages &&
para.parsed.imageHeightLines === s.imageHeightLines &&
para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
para.parsed.compactTextPerc === s.compactTextPerc &&
para.parsed.testWidth === s.testWidth
)
return para.parsed;
const parsed = {
testWidth: this.testWidth,
w: this.w,
p: this.p,
wordWrap: this.wordWrap,
maxWordLength: this.maxWordLength,
font: this.font,
cutEmptyParagraphs: this.cutEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs,
showImages: this.showImages,
imageHeightLines: this.imageHeightLines,
imageFitWidth: this.imageFitWidth,
compactTextPerc: this.compactTextPerc,
p: s.p,
w: s.w,
font: s.font,
fontSize: s.fontSize,
wordWrap: s.wordWrap,
cutEmptyParagraphs: s.cutEmptyParagraphs,
addEmptyParagraphs: s.addEmptyParagraphs,
maxWordLength: s.maxWordLength,
lineHeight: s.lineHeight,
showImages: s.showImages,
imageHeightLines: s.imageHeightLines,
imageFitWidth: (s.imageFitWidth || s.dualPageMode),
compactTextPerc: s.compactTextPerc,
testWidth: s.testWidth,
visible: true, //вычисляется позже
};
@@ -773,7 +846,7 @@ export default class BookParser {
let ofs = 0;//смещение от начала параграфа para.offset
let imgW = 0;
let imageInPara = false;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) {
style = part.style;
@@ -786,14 +859,14 @@ export default class BookParser {
if (!bin)
bin = {h: 1, w: 1};
let lineCount = this.imageHeightLines;
let c = Math.ceil(bin.h/this.lineHeight);
let lineCount = parsed.imageHeightLines;
let c = Math.ceil(bin.h/parsed.lineHeight);
const maxH = lineCount*this.lineHeight;
const maxH = lineCount*parsed.lineHeight;
let maxH2 = maxH;
if (this.imageFitWidth && bin.w > this.w) {
maxH2 = bin.h*this.w/bin.w;
c = Math.ceil(maxH2/this.lineHeight);
if (parsed.imageFitWidth && bin.w > parsed.w) {
maxH2 = bin.h*parsed.w/bin.w;
c = Math.ceil(maxH2/parsed.lineHeight);
}
lineCount = (c < lineCount ? c : lineCount);
@@ -833,10 +906,10 @@ export default class BookParser {
continue;
}
if (part.image.id && part.image.inline && this.showImages) {
if (part.image.id && part.image.inline && parsed.showImages) {
const bin = this.binary[part.image.id];
if (bin) {
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
let imgH = (bin.h > parsed.fontSize ? parsed.fontSize : bin.h);
imgW += bin.w*imgH/bin.h;
line.parts.push({style, text: '',
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
@@ -951,11 +1024,11 @@ export default class BookParser {
//parsed.visible
if (imageInPara) {
parsed.visible = this.showImages;
parsed.visible = parsed.showImages;
} else {
parsed.visible = !(
(para.addIndex > this.addEmptyParagraphs) ||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
(para.addIndex > parsed.addEmptyParagraphs) ||
(para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
);
}

View File

@@ -0,0 +1,40 @@
import localForage from 'localforage';
//import _ from 'lodash';
const wpStore = localForage.createInstance({
name: 'wallpaperStorage'
});
class WallpaperStorage {
constructor() {
this.cachedKeys = [];
}
async init() {
this.cachedKeys = await wpStore.keys();
}
async getLength() {
return await wpStore.length();
}
async setData(key, data) {
await wpStore.setItem(key, data);
this.cachedKeys = await wpStore.keys();
}
async getData(key) {
return await wpStore.getItem(key);
}
async removeData(key) {
await wpStore.removeItem(key);
this.cachedKeys = await wpStore.keys();
}
keyExists(key) {//не асинхронная
return this.cachedKeys.includes(key);
}
}
export default new WallpaperStorage();

View File

@@ -1,4 +1,32 @@
export const versionHistory = [
{
showUntil: '2021-02-16',
header: '0.10.0 (2021-02-09)',
content:
`
<ul>
<li>добавлен двухстраничный режим</li>
<li>в настройки добавлены все кириллические веб-шрифты от google</li>
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
<li>немного улучшен парсинг fb2</li>
</ul>
`
},
{
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)',

View File

@@ -21,7 +21,8 @@ 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} from 'quasar/src/components/item';
import {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';
@@ -49,7 +50,8 @@ const components = {
QTabs, QTab,
//QTabPanels, QTabPanel,
QSeparator,
//QList, QItem, QItemSection, QItemLabel,
//QList,
QItem, QItemSection, QItemLabel,
QTooltip,
QSpinner,
QTable, QTh, QTr, QTd,

View File

@@ -0,0 +1,22 @@
class DynamicCss {
constructor() {
this.cssNodes = {};
}
replace(name, cssText) {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = cssText;
const parent = document.getElementsByTagName('head')[0];
if (this.cssNodes[name]) {
parent.removeChild(this.cssNodes[name]);
delete this.cssNodes[name];
}
this.cssNodes[name] = parent.appendChild(style);
}
}
export default new DynamicCss();

View File

@@ -13,6 +13,10 @@ export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function toHex(buf) {
return Buffer.from(buf).toString('hex');
}
export function stringToHex(str) {
return Buffer.from(str).toString('hex');
}

View File

@@ -0,0 +1 @@
["Alegreya","Alegreya SC","Alegreya Sans","Alegreya Sans SC","Alice","Amatic SC","Andika","Anonymous Pro","Arimo","Arsenal","Bad Script","Balsamiq Sans","Bellota","Bellota Text","Bitter","Caveat","Comfortaa","Commissioner","Cormorant","Cormorant Garamond","Cormorant Infant","Cormorant SC","Cormorant Unicase","Cousine","Cuprum","Didact Gothic","EB Garamond","El Messiri","Exo 2","Fira Code","Fira Mono","Fira Sans","Fira Sans Condensed","Fira Sans Extra Condensed","Forum","Gabriela","Hachi Maru Pop","IBM Plex Mono","IBM Plex Sans","IBM Plex Serif","Inter","Istok Web","JetBrains Mono","Jost","Jura","Kelly Slab","Kosugi","Kosugi Maru","Kurale","Ledger","Literata","Lobster","Lora","M PLUS 1p","M PLUS Rounded 1c","Manrope","Marck Script","Marmelad","Merriweather","Montserrat","Montserrat Alternates","Neucha","Noto Sans","Noto Serif","Nunito","Old Standard TT","Open Sans","Open Sans Condensed","Oranienbaum","Oswald","PT Mono","PT Sans","PT Sans Caption","PT Sans Narrow","PT Serif","PT Serif Caption","Pacifico","Pangolin","Pattaya","Philosopher","Piazzolla","Play","Playfair Display","Playfair Display SC","Podkova","Poiret One","Prata","Press Start 2P","Prosto One","Raleway","Roboto","Roboto Condensed","Roboto Mono","Roboto Slab","Rubik","Rubik Mono One","Ruda","Ruslan Display","Russo One","Sawarabi Gothic","Scada","Seymour One","Source Code Pro","Source Sans Pro","Source Serif Pro","Spectral","Spectral SC","Stalinist One","Tenor Sans","Tinos","Ubuntu","Ubuntu Condensed","Ubuntu Mono","Underdog","Viaoda Libre","Vollkorn","Vollkorn SC","Yanone Kaffeesatz","Yeseva One"]

View File

@@ -0,0 +1,13 @@
const fs = require('fs-extra');
async function main() {
const webfonts = await fs.readFile('webfonts.json');
let fonts = JSON.parse(webfonts);
fonts = fonts.items.filter(item => item.subsets.includes('cyrillic'));
fonts = fonts.map(item => item.family);
fonts.sort();
await fs.writeFile('fonts.json', JSON.stringify(fonts));
}
main();

View File

@@ -1,4 +1,5 @@
import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json';
const readerActions = {
'help': 'Вызвать cправку',
@@ -12,7 +13,7 @@ const readerActions = {
'setPosition': 'Установить позицию',
'search': 'Найти в тексте',
'copyText': 'Скопировать текст со страницы',
'splitToPara': 'Обновить с разбиением на параграфы',
'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу',
'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки',
@@ -41,7 +42,7 @@ const toolButtons = [
{name: 'setPosition', show: true},
{name: 'search', show: true},
{name: 'copyText', show: false},
{name: 'splitToPara', show: false},
{name: 'convOptions', show: true},
{name: 'refresh', show: true},
{name: 'contents', show: true},
{name: 'libs', show: true},
@@ -60,8 +61,8 @@ const hotKeys = [
{name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'splitToPara', codes: ['Shift+R']},
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'convOptions', codes: ['Ctrl+M']},
{name: 'refresh', codes: ['R']},
{name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']},
@@ -91,125 +92,22 @@ const fonts = [
{name: 'Rubik', fontVertShift: 0},
];
const webFonts = [
{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Alegreya+Sans', name: 'Alegreya Sans', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Alegreya+SC', name: 'Alegreya SC', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Alice', name: 'Alice', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Amatic+SC', name: 'Amatic SC', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Andika', name: 'Andika', fontVertShift: -35},
{css: 'https://fonts.googleapis.com/css?family=Anonymous+Pro', name: 'Anonymous Pro', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Arsenal', name: 'Arsenal', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Bad+Script', name: 'Bad Script', fontVertShift: -30},
{css: 'https://fonts.googleapis.com/css?family=Caveat', name: 'Caveat', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Comfortaa', name: 'Comfortaa', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Cormorant', name: 'Cormorant', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Garamond', name: 'Cormorant Garamond', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Infant', name: 'Cormorant Infant', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Unicase', name: 'Cormorant Unicase', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cousine', name: 'Cousine', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cuprum', name: 'Cuprum', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Didact+Gothic', name: 'Didact Gothic', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=EB+Garamond', name: 'EB Garamond', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=El+Messiri', name: 'El Messiri', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Mono', name: 'Fira Mono', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans', name: 'Fira Sans', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Condensed', name: 'Fira Sans Condensed', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Extra+Condensed', name: 'Fira Sans Extra Condensed', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Forum', name: 'Forum', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Gabriela', name: 'Gabriela', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Mono', name: 'IBM Plex Mono', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Sans', name: 'IBM Plex Sans', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Serif', name: 'IBM Plex Serif', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Istok+Web', name: 'Istok Web', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Jura', name: 'Jura', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Kelly+Slab', name: 'Kelly Slab', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Kosugi', name: 'Kosugi', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Kosugi+Maru', name: 'Kosugi Maru', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Kurale', name: 'Kurale', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Ledger', name: 'Ledger', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Lobster', name: 'Lobster', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Lora', name: 'Lora', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Marck+Script', name: 'Marck Script', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Marmelad', name: 'Marmelad', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Merriweather', name: 'Merriweather', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Montserrat', name: 'Montserrat', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Montserrat+Alternates', name: 'Montserrat Alternates', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Neucha', name: 'Neucha', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans', name: 'Noto Sans', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans+SC', name: 'Noto Sans SC', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif', name: 'Noto Serif', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif+TC', name: 'Noto Serif TC', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Old+Standard+TT', name: 'Old Standard TT', fontVertShift: 15},
{css: 'https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300', name: 'Open Sans Condensed', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Oranienbaum', name: 'Oranienbaum', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Oswald', name: 'Oswald', fontVertShift: -20},
{css: 'https://fonts.googleapis.com/css?family=Pacifico', name: 'Pacifico', fontVertShift: -35},
{css: 'https://fonts.googleapis.com/css?family=Pangolin', name: 'Pangolin', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Pattaya', name: 'Pattaya', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Philosopher', name: 'Philosopher', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Play', name: 'Play', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display', name: 'Playfair Display', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display+SC', name: 'Playfair Display SC', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Podkova', name: 'Podkova', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Poiret+One', name: 'Poiret One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Prata', name: 'Prata', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Prosto+One', name: 'Prosto One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=PT+Mono', name: 'PT Mono', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=PT+Sans', name: 'PT Sans', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Caption', name: 'PT Sans Caption', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Narrow', name: 'PT Sans Narrow', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Serif', name: 'PT Serif', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Serif+Caption', name: 'PT Serif Caption', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=Roboto+Condensed', name: 'Roboto Condensed', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Roboto+Mono', name: 'Roboto Mono', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Roboto+Slab', name: 'Roboto Slab', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Ruslan+Display', name: 'Ruslan Display', fontVertShift: 20},
{css: 'https://fonts.googleapis.com/css?family=Russo+One', name: 'Russo One', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Sawarabi+Gothic', name: 'Sawarabi Gothic', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Scada', name: 'Scada', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Seymour+One', name: 'Seymour One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro', name: 'Source Sans Pro', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Spectral', name: 'Spectral', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Stalinist+One', name: 'Stalinist One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Tinos', name: 'Tinos', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Tenor+Sans', name: 'Tenor Sans', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Underdog', name: 'Underdog', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Mono', name: 'Ubuntu Mono', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Condensed', name: 'Ubuntu Condensed', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Vollkorn', name: 'Vollkorn', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Vollkorn+SC', name: 'Vollkorn SC', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz', name: 'Yanone Kaffeesatz', fontVertShift: 20},
{css: 'https://fonts.googleapis.com/css?family=Yeseva+One', name: 'Yeseva One', fontVertShift: 10},
];
//webFonts: [{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: 0}, ...],
const webFonts = [];
for (const family of googleFonts) {
webFonts.push({
css: `https://fonts.googleapis.com/css?family=${family.replace(/\s/g, '+')}`,
name: family,
fontVertShift: 0,
});
}
//----------------------------------------------------------------------------------------------------------
const settingDefaults = {
textColor: '#000000',
backgroundColor: '#EBE2C9',
backgroundColor: '#ebe2c9',
wallpaper: '',
wallpaperIgnoreStatusBar: false,
fontStyle: '',// 'italic'
fontWeight: '',// 'bold'
fontSize: 20,// px
@@ -226,9 +124,22 @@ const settingDefaults = {
wordWrap: true,//перенос по слогам
keepLastToFirst: false,// перенос последней строки в первую при листании
dualPageMode: false,
dualIndentLR: 10,// px, отступ слева и справа внутри страницы в двухстраничном режиме
dualDivWidth: 2,// px, ширина разделителя
dualDivHeight: 100,// процент, высота разделителя
dualDivColorAsText: true,//цвет как у текста
dualDivColor: '#000000',
dualDivColorAlpha: 0.7,// прозрачность разделителя
dualDivStrokeFill: 1,// px, заполнение пунктира
dualDivStrokeGap: 1,// px, промежуток пунктира
dualDivShadowWidth: 0,// px, ширина тени
showStatusBar: true,
statusBarTop: false,// top, bottom
statusBarHeight: 19,// px
statusBarColorAsText: true,//цвет как у текста
statusBarColor: '#000000',
statusBarColorAlpha: 0.4,
statusBarClickOpen: true,
@@ -252,16 +163,20 @@ const settingDefaults = {
imageHeightLines: 100,
imageFitWidth: true,
enableSitesFilter: true,
splitToPara: false,
djvuQuality: 20,
pdfAsText: true,
pdfQuality: 20,
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
showLiberamaTopDialog2020: true,
showNeedUpdateNotify: true,
fontShifts: {},
showToolButton: {},
userHotKeys: {},
userWallpapers: [],
};
for (const font of fonts)
@@ -273,12 +188,13 @@ for (const button of toolButtons)
for (const hotKey of hotKeys)
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
const excludeDiffHotKeys = [];
const diffExclude = [];
for (const hotKey of hotKeys)
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
diffExclude.push(`userHotKeys/${hotKey.name}`);
diffExclude.push('userWallpapers');
function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.9.11",
"version": "0.10.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.9.11",
"version": "0.10.1",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -10,8 +10,8 @@
"scripts": {
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
"build:client": "webpack --config build/webpack.prod.config.js",
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
"build:linux": "npm run build:client && node build/linux && pkg -t node12-linux-x64 -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t node12-win-x64 -o dist/win/liberama .",
"lint": "eslint --ext=.js,.vue client server",
"build:client-dev": "webpack --config build/webpack.dev.config.js",
"postinstall": "npm run build:client-dev && node build/linux"

View File

@@ -20,9 +20,12 @@ class ReaderController extends BaseController {
const workerId = this.readerWorker.loadBookUrl({
url: request.url,
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
skipHtmlCheck: (request.hasOwnProperty('skipHtmlCheck') ? request.skipHtmlCheck : false),
isText: (request.hasOwnProperty('isText') ? request.isText : false),
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
djvuQuality: (request.hasOwnProperty('djvuQuality') ? request.djvuQuality : false),
pdfAsText: (request.hasOwnProperty('pdfAsText') ? request.pdfAsText : false),
pdfQuality: (request.hasOwnProperty('pdfQuality') ? request.pdfQuality : false),
});
const state = this.workerState.getState(workerId);
return (state ? state : {});

View File

@@ -50,8 +50,14 @@ class WebSocketController {
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
}
ws.lastActivity = Date.now();
req = JSON.parse(message);
ws.lastActivity = Date.now();
//pong for WebSocketConnection
if (req._rpo === 1)
this.send({_rok: 1}, req, ws);
switch (req.action) {
case 'test':
await this.test(req, ws); break;

View File

@@ -104,7 +104,7 @@ class ConvertBase {
}
isDataXml(data) {
const str = data.toString().trim();
const str = data.slice(0, 100).toString().trim();
return (str.indexOf('<?xml version="1.0"') == 0 || str.indexOf('<?xml version=\'1.0\'') == 0 );
}

View File

@@ -16,12 +16,21 @@ class ConvertDjvu extends ConvertJpegPng {
if (!this.check(data, opts))
return false;
const {inputFiles, callback, abort} = opts;
let {inputFiles, callback, abort, djvuQuality} = opts;
djvuQuality = (djvuQuality && djvuQuality <= 100 && djvuQuality >= 10 ? djvuQuality : 20);
let jpegQuality = djvuQuality;
let tiffQuality = djvuQuality + 30;
tiffQuality = (tiffQuality < 85 ? tiffQuality : 85);
const ddjvuPath = '/usr/bin/ddjvu';
if (!await fs.pathExists(ddjvuPath))
throw new Error('Внешний конвертер ddjvu не найден');
const djvusedPath = '/usr/bin/djvused';
if (!await fs.pathExists(djvusedPath))
throw new Error('Внешний конвертер djvused не найден');
const tiffsplitPath = '/usr/bin/tiffsplit';
if (!await fs.pathExists(tiffsplitPath))
throw new Error('Внешний конвертер tiffsplitPath не найден');
@@ -36,7 +45,7 @@ class ConvertDjvu extends ConvertJpegPng {
//конвертируем в tiff
let perc = 0;
await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
await this.execConverter(ddjvuPath, ['-format=tiff', `-quality=${tiffQuality}`, '-verbose', inputFiles.sourceFile, tifFile], () => {
perc = (perc < 100 ? perc + 1 : 40);
callback(perc);
}, abort);
@@ -53,22 +62,57 @@ class ConvertDjvu extends ConvertJpegPng {
await fs.remove(tifFile);
//конвертируем в jpg
await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
await this.execConverter(mogrifyPath, ['-quality', jpegQuality, '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
perc = (perc < 100 ? perc + 1 : 40);
callback(perc);
}, abort);
limitSize = 2*this.config.maxUploadFileSize;
let jpgFilesSize = 0;
//ищем изображения
let files = [];
await utils.findFiles(async(file) => {
if (path.extname(file) == '.jpg')
if (path.extname(file) == '.jpg') {
jpgFilesSize += (await fs.stat(file)).size;
if (jpgFilesSize > limitSize) {
throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
}
files.push({name: file, base: path.basename(file)});
}
}, dir);
files.sort((a, b) => a.base.localeCompare(b.base));
//схема документа (outline)
const djvusedResult = await this.execConverter(djvusedPath, ['-u', '-e', 'print-outline', inputFiles.sourceFile], null, abort);
const outline = [];
const lines = djvusedResult.stdout.match(/\(\s*".*"\s*?"#\d+"/g);
if (lines) {
lines.forEach(l => {
const m = l.match(/"(.*)"\s*?"#(\d+)"/);
if (m) {
const pageNum = m[2];
let s = outline[pageNum];
if (!s)
s = m[1].trim();
else
s += `${(s[s.length - 1] != '.' ? '.' : '')} ${m[1].trim()}`;
outline[pageNum] = s;
}
});
}
await utils.sleep(100);
return await super.run(data, Object.assign({}, opts, {imageFiles: files.map(f => f.name)}));
let i = 0;
const imageFiles = files.map(f => {
i++;
let alt = (outline[i] ? outline[i] : '');
return {src: f.name, alt};
});
return await super.run(data, Object.assign({}, opts, {imageFiles}));
}
}

View File

@@ -13,22 +13,30 @@ class ConvertFb2 extends ConvertBase {
}
async run(data, opts) {
let newData = data;
let newData = data.slice(0, 1024);
//Корректируем кодировку, 16-битные кодировки должны стать utf-8
//Корректируем кодировку для проверки, 16-битные кодировки должны стать utf-8
const encoding = textUtils.getEncoding(newData);
if (encoding.indexOf('UTF-16') == 0) {
newData = Buffer.from(iconv.decode(newData, encoding));
}
//Проверяем
if (!this.check(newData, opts))
return false;
//Корректируем кодировку всего объема
newData = data;
if (encoding.indexOf('UTF-16') == 0) {
newData = Buffer.from(iconv.decode(newData, encoding));
}
//Корректируем пробелы, всякие файлы попадаются :(
if (newData[0] == 32) {
newData = Buffer.from(newData.toString().trim());
}
//Окончательно корректируем кодировку
return this.checkEncoding(newData);
}

View File

@@ -45,7 +45,7 @@ class ConvertFb3 extends ConvertHtml {
.replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
.replace(/<\/subtitle>/g, '</fb2-subtitle>')
;
return await super.run(Buffer.from(text), {skipCheck: true});
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
}
}

View File

@@ -16,7 +16,7 @@ class ConvertHtml extends ConvertBase {
}
//из буфера обмена?
if (data.toString().indexOf('<buffer>') == 0) {
if (data.slice(0, 50).toString().indexOf('<buffer>') == 0) {
return {isText: false};
}
@@ -24,15 +24,13 @@ class ConvertHtml extends ConvertBase {
}
async run(data, opts) {
let isText = false;
if (!opts.skipCheck) {
let {isText = false, uploadFileName = ''} = opts;
if (!opts.skipHtmlCheck) {
const checkResult = this.check(data, opts);
if (!checkResult)
return false;
isText = checkResult.isText;
} else {
isText = opts.isText;
}
let titleInfo = {};
@@ -242,6 +240,9 @@ class ConvertHtml extends ConvertBase {
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
});
if (!title)
title = uploadFileName;
titleInfo['book-title'] = title;
if (author)
titleInfo.author = {'last-name': author};

View File

@@ -27,7 +27,7 @@ class ConvertJpegPng extends ConvertBase {
} else {
const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`;
await fs.copy(inputFiles.sourceFile, imageFile);
files.push(imageFile);
files.push({src: imageFile});
}
//читаем изображения
@@ -55,10 +55,9 @@ class ConvertJpegPng extends ConvertBase {
let images = [];
let loading = [];
files.forEach(f => {
const image = {src: f};
images.push(image);
loading.push(loadImage(image));
files.forEach(img => {
images.push(img);
loading.push(loadImage(img));
});
await Promise.all(loading);
@@ -82,8 +81,14 @@ class ConvertJpegPng extends ConvertBase {
const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': image.type}, _t: image.data};
binary.push(img);
const attrs = {'l:href': `#${image.name}`};
if (image.alt) {
image.alt = (image.alt.length > 256 ? image.alt.substring(0, 256) : image.alt);
attrs.alt = image.alt;
}
pars.push({_n: 'p', _t: ''});
pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
pars.push({_n: 'image', _attrs: attrs});
}
}
pars.push({_n: 'p', _t: ''});

View File

@@ -15,7 +15,7 @@ class ConvertPdf extends ConvertHtml {
}
async run(notUsed, opts) {
if (!this.check(notUsed, opts))
if (!opts.pdfAsText || !this.check(notUsed, opts))
return false;
await this.checkExternalConverterPresent();
@@ -27,7 +27,6 @@ class ConvertPdf extends ConvertHtml {
const outFile = `${outBasename}.xml`;
const pdftohtmlPath = '/usr/bin/pdftohtml';
if (!await fs.pathExists(pdftohtmlPath))
throw new Error('Внешний конвертер pdftohtml не найден');
@@ -342,7 +341,7 @@ class ConvertPdf extends ConvertHtml {
//console.log(text);
await utils.sleep(100);
return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
return await super.run(Buffer.from(text), {skipHtmlCheck: true, isText: true});
}
async getPdfTitleAndAuthor(pdfFile) {

View 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;

View File

@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
if (text === false)
return false;
return await super.run(Buffer.from(text), {skipCheck: true});
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
}
getTitle(text) {

View File

@@ -7,6 +7,7 @@ const convertClassFactory = [
require('./ConvertEpub'),
require('./ConvertDjvu'),
require('./ConvertPdf'),
require('./ConvertPdfImages'),
require('./ConvertRtf'),
require('./ConvertDocX'),
require('./ConvertFb3'),

View File

@@ -36,7 +36,7 @@ class ReaderWorker {
this.remoteWebDavStorage = false;
if (config.remoteWebDavStorage) {
this.remoteWebDavStorage = new RemoteWebDavStorage(
Object.assign({maxContentLength: config.maxUploadFileSize}, config.remoteWebDavStorage)
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteWebDavStorage)
);
}

View File

@@ -0,0 +1,237 @@
const isBrowser = (typeof window !== 'undefined');
const utils = {
sleep: (ms) => { return new Promise(resolve => setTimeout(resolve, ms)); }
};
const cleanPeriod = 5*1000;//5 секунд
class WebSocketConnection {
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
this.url = url;
this.ws = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTimeSecs*1000;
this.openTimeout = openTimeoutSecs*1000;
this.requestId = 0;
this.wsErrored = false;
this.closed = false;
this.connecting = false;
this.periodicClean();//no await
}
//рассылаем сообщение и удаляем те обработчики, которые его получили
emit(mes, isError) {
const len = this.listeners.length;
if (len > 0) {
let newListeners = [];
for (const listener of this.listeners) {
let emitted = false;
if (isError) {
listener.onError(mes);
emitted = true;
} else {
if ( (listener.requestId && mes.requestId && listener.requestId === mes.requestId) ||
(!listener.requestId && !mes.requestId) ) {
listener.onMessage(mes);
emitted = true;
}
}
if (!emitted)
newListeners.push(listener);
}
this.listeners = newListeners;
}
return this.listeners.length != len;
}
get isOpen() {
return (this.ws && this.ws.readyState == this.WebSocket.OPEN);
}
processMessageQueue() {
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (!this.emit(message.mes)) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
}
_open() {
return new Promise((resolve, reject) => { (async() => {
if (this.closed)
reject(new Error('Этот экземпляр класса уничтожен. Пожалуйста, создайте новый.'));
if (this.connecting) {
let i = this.openTimeout/100;
while (i-- > 0 && this.connecting) {
await utils.sleep(100);
}
}
//проверим подключение, и если нет, то подключимся заново
if (this.isOpen) {
resolve(this.ws);
} else {
this.connecting = true;
this.terminate();
if (isBrowser) {
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
const url = this.url || `${protocol}//${window.location.host}/ws`;
this.ws = new this.WebSocket(url);
} else {
this.ws = new this.WebSocket(this.url);
}
const onopen = (e) => {
this.connecting = false;
resolve(this.ws);
};
const onmessage = (data) => {
try {
if (isBrowser)
data = data.data;
const mes = JSON.parse(data);
this.messageQueue.push({regTime: Date.now(), mes});
this.processMessageQueue();
} catch (e) {
this.emit(e.message, true);
}
};
const onerror = (e) => {
this.emit(e.message, true);
reject(new Error(e.message));
};
const onclose = (e) => {
this.emit(e.message, true);
reject(new Error(e.message));
};
if (isBrowser) {
this.ws.onopen = onopen;
this.ws.onmessage = onmessage;
this.ws.onerror = onerror;
this.ws.onclose = onclose;
} else {
this.ws.on('open', onopen);
this.ws.on('message', onmessage);
this.ws.on('error', onerror);
this.ws.on('close', onclose);
}
await utils.sleep(this.openTimeout);
reject(new Error('Соединение не удалось'));
}
})() });
}
//timeout в секундах (проверка каждый cleanPeriod интервал)
message(requestId, timeoutSecs = 4) {
return new Promise((resolve, reject) => {
this.listeners.push({
regTime: Date.now(),
requestId,
timeout: timeoutSecs*1000,
onMessage: (mes) => {
resolve(mes);
},
onError: (mes) => {
reject(new Error(mes));
}
});
this.processMessageQueue();
});
}
async send(req, timeoutSecs = 4) {
await this._open();
if (this.isOpen) {
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
const requestId = this.requestId;//реентерабельность!!!
this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1
let resp = {};
try {
resp = await this.message(requestId, timeoutSecs);
} catch(e) {
this.terminate();
throw new Error('WebSocket не отвечает');
}
if (resp._rok) {
return requestId;
} else {
throw new Error('Запрос не принят сервером');
}
} else {
throw new Error('WebSocket коннект закрыт');
}
}
terminate() {
if (this.ws) {
if (isBrowser) {
this.ws.close();
} else {
this.ws.terminate();
}
}
this.ws = null;
}
close() {
this.terminate();
this.closed = true;
}
async periodicClean() {
while (!this.closed) {
try {
const now = Date.now();
//чистка listeners
let newListeners = [];
for (const listener of this.listeners) {
if (now - listener.regTime < listener.timeout) {
newListeners.push(listener);
} else {
if (listener.onError)
listener.onError('Время ожидания ответа истекло');
}
}
this.listeners = newListeners;
//чистка messageQueue
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (now - message.regTime < this.messageLifeTime) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} catch(e) {
//
}
await utils.sleep(cleanPeriod);
}
}
}
module.exports = WebSocketConnection;

View File

@@ -32,11 +32,11 @@ class ConnManager {
const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
//бэкап
if (await fs.pathExists(dbFileName))
if (!poolConfig.noBak && await fs.pathExists(dbFileName))
await fs.copy(dbFileName, `${dbFileName}.bak`);
const connPool = new SqliteConnectionPool();
await connPool.open(poolConfig.connCount, dbFileName);
await connPool.open(poolConfig, dbFileName);
log(`Opened database "${poolConfig.poolName}"`);
//миграции

View File

@@ -1,27 +1,32 @@
const sqlite = require('sqlite');
const SQL = require('sql-template-strings');
const utils = require('../core/utils');
const waitingDelay = 100; //ms
class SqliteConnectionPool {
constructor() {
this.closed = true;
}
async open(connCount, dbFileName) {
if (!Number.isInteger(connCount) || connCount <= 0)
return;
async open(poolConfig, dbFileName) {
const connCount = poolConfig.connCount || 1;
const busyTimeout = poolConfig.busyTimeout || 60*1000;
const cacheSize = poolConfig.cacheSize || 2000;
this.dbFileName = dbFileName;
this.connections = [];
this.freed = new Set();
this.waitingQueue = [];
for (let i = 0; i < connCount; i++) {
let client = await sqlite.open(dbFileName);
client.configure('busyTimeout', 10000); //ms
client.configure('busyTimeout', busyTimeout); //ms
await client.exec(`PRAGMA cache_size = ${cacheSize}`);
client.ret = () => {
this.freed.add(i);
if (this.waitingQueue.length) {
this.waitingQueue.shift().onFreed(i);
}
};
this.freed.add(i);
@@ -30,30 +35,27 @@ class SqliteConnectionPool {
this.closed = false;
}
_setImmediate() {
get() {
return new Promise((resolve) => {
setImmediate(() => {
return resolve();
if (this.closed)
throw new Error('Connection pool closed');
const freeConnIndex = this.freed.values().next().value;
if (freeConnIndex !== undefined) {
this.freed.delete(freeConnIndex);
resolve(this.connections[freeConnIndex]);
return;
}
this.waitingQueue.push({
onFreed: (connIndex) => {
this.freed.delete(connIndex);
resolve(this.connections[connIndex]);
},
});
});
}
async get() {
if (this.closed)
return;
let freeConnIndex = this.freed.values().next().value;
if (freeConnIndex == null) {
if (waitingDelay)
await utils.sleep(waitingDelay);
return await this._setImmediate().then(() => this.get());
}
this.freed.delete(freeConnIndex);
return this.connections[freeConnIndex];
}
async run(query) {
const dbh = await this.get();
try {