Compare commits

...

28 Commits
1.2.0 ... 1.2.3

Author SHA1 Message Date
Book Pauk
fc8e986acb Merge branch 'release/1.2.3' 2024-08-02 15:22:24 +07:00
Book Pauk
64539785c2 1.2.3 2024-08-02 15:22:07 +07:00
Book Pauk
f530455146 Версия 1.2.3 2024-08-02 15:21:43 +07:00
Book Pauk
70dc66e1ae Исправление мелких багов при прокрутке 2024-08-02 15:15:54 +07:00
Book Pauk
3e5894d9e0 Исправление багов 2024-07-31 11:44:07 +07:00
Book Pauk
d7ac9d1bfc Улучшение отображения примечаний 2024-07-31 11:30:31 +07:00
Book Pauk
5160c5fb75 Мелкая поправка текста 2024-07-30 21:29:02 +07:00
Book Pauk
d9c7964410 Поправки багов 2024-07-30 21:28:27 +07:00
Book Pauk
110952b4c4 К предыдущему 2024-07-30 18:41:21 +07:00
Book Pauk
ece17dc0dd Улучшение отображения сносок 2024-07-30 18:23:52 +07:00
Book Pauk
35e1087531 Merge tag '1.2.2' into develop
1.2.2
2024-07-28 20:22:59 +07:00
Book Pauk
59c4b62770 Merge branch 'release/1.2.2' 2024-07-28 20:22:54 +07:00
Book Pauk
4be9ce5ff3 Версия 1.2.2 2024-07-28 20:22:33 +07:00
Book Pauk
92a811cabd Поправки парсинга примечаний 2024-07-28 20:20:45 +07:00
Book Pauk
897cdc8ac7 Исправление парсинга примечаний 2024-07-28 20:13:35 +07:00
Book Pauk
418ff482ae Merge tag '1.2.1' into develop
1.2.1
2024-07-28 17:55:11 +07:00
Book Pauk
8858d6d1f2 Merge branch 'release/1.2.1' 2024-07-28 17:55:05 +07:00
Book Pauk
41f8a28631 Версия 1.2.1 2024-07-28 17:52:16 +07:00
Book Pauk
da0771d5e5 Мелкая поправка разметки 2024-07-28 17:47:15 +07:00
Book Pauk
c03995367a Поправки багов 2024-07-28 17:45:18 +07:00
Book Pauk
0430105061 Добавлено отображение примечаний на месте, по клику на примечании (#50) 2024-07-28 17:23:16 +07:00
Book Pauk
afd4d02dad Улучшение BUCServer 2024-07-26 17:19:45 +07:00
Book Pauk
d634ebf14c Улучшение BUCServer 2024-07-26 15:54:41 +07:00
Book Pauk
613230256a Небольшой тюнинг BUCServer 2024-07-26 00:49:39 +07:00
Book Pauk
2da1736c99 Поправка для игнорирования невалидных сертификатов 2024-07-25 18:10:13 +07:00
Book Pauk
1914092520 npx update-browserslist-db@latest 2024-07-25 16:51:44 +07:00
Book Pauk
4a6f93a14f edit 2024-03-25 13:02:13 +07:00
Book Pauk
9da8142078 Merge tag '1.2.0' into develop
1.2.0
2024-03-25 12:54:14 +07:00
11 changed files with 307 additions and 46 deletions

View File

@@ -594,7 +594,7 @@ class RecentBooksPage {
} }
async handleDel(item) { async handleDel(item) {
if (item.group) { if (item.group?.length) {
const keys = [{key: item.key}]; const keys = [{key: item.key}];
for (const book of item.group) for (const book of item.group)
keys.push({key: book.key}); keys.push({key: book.key});
@@ -615,14 +615,14 @@ class RecentBooksPage {
} else { } else {
if (await this.$root.stdDialog.confirm('Подтвердите удаление книги из архива:', ' ')) { if (await this.$root.stdDialog.confirm('Подтвердите удаление книги из архива:', ' ')) {
await bookManager.delRecentBooks([{key: item.key}], 2); await bookManager.delRecentBooks([{key: item.key}], 2);
this.$root.notify.info('Книга удалено безвозвратно'); this.$root.notify.info('Книга удалена безвозвратно');
} }
} }
} }
} }
async handleRestore(item) { async handleRestore(item) {
if (item.group) { if (item.group?.length) {
const keys = [{key: item.key}]; const keys = [{key: item.key}];
for (const book of item.group) for (const book of item.group)
keys.push({key: book.key}); keys.push({key: book.key});
@@ -637,7 +637,7 @@ class RecentBooksPage {
async loadBook(item, force = false) { async loadBook(item, force = false) {
if (item.deleted) if (item.deleted)
await this.handleRestore(item.key); await this.handleRestore(item);
this.$emit('load-book', {url: item.url, path: item.path, force}); this.$emit('load-book', {url: item.url, path: item.path, force});
this.close(); this.close();

View File

@@ -14,6 +14,11 @@ export default class DrawHelper {
return this.context.measureText(text).width; return this.context.measureText(text).width;
} }
measureTextMetrics(text, style) {// eslint-disable-line no-unused-vars
this.context.font = this.fontByStyle(style);
return this.context.measureText(text);
}
measureTextFont(text, font) {// eslint-disable-line no-unused-vars measureTextFont(text, font) {// eslint-disable-line no-unused-vars
this.context.font = font; this.context.font = font;
return this.context.measureText(text).width; return this.context.measureText(text).width;
@@ -46,7 +51,22 @@ export default class DrawHelper {
tOpen += (part.style.italic ? '<i>' : ''); 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.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">' : ''); tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
if (part.style.note) {
const t = part.text;
const m = this.measureTextMetrics(t, part.style);
const d = this.fontSize - 1.1*m.fontBoundingBoxAscent;
const w = m.width;
const size = (this.fontSize > 18 ? this.fontSize : 18);
const pad = size/2;
const btnW = (w >= size ? w : size) + pad*2;
tOpen += `<span style="position: relative;">` +
`<span style="position: absolute; background-color: ${this.textColor}; opacity: 0.1; cursor: pointer; pointer-events: auto; ` +
`height: ${this.fontSize + pad*2}px; padding: ${pad}px; left: -${(btnW - w)/2 - pad*0.05}px; top: -${pad + d}px; width: ${btnW}px; border-radius: ${size}px;" ` +
`onclick="onNoteClickLiberama('${part.style.note.id}', ${part.style.note.orig ? 1 : 0})"><span style="visibility: hidden;" class="dborder">${t}</span></span>`;
}
let tClose = ''; let tClose = '';
tClose += (part.style.note ? '</span>' : '');
tClose += (part.style.sub ? '</span>' : ''); tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : ''); tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : ''); tClose += (part.style.italic ? '</i>' : '');

View File

@@ -4,12 +4,12 @@
<div class="absolute" v-html="background"></div> <div class="absolute" v-html="background"></div>
<div class="absolute" v-html="pageDivider"></div> <div class="absolute" v-html="pageDivider"></div>
</div> </div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox1" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd"> <div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div @copy.prevent="copyText" v-html="page1"></div> <div @copy.prevent="copyText" v-html="page1"></div>
</div> </div>
</div> </div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox2" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd"> <div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div @copy.prevent="copyText" v-html="page2"></div> <div @copy.prevent="copyText" v-html="page2"></div>
</div> </div>
@@ -21,17 +21,13 @@
v-show="clickControl" ref="layoutEvents" class="layout events" v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;" oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@mouseover.prevent.stop="onMouseEvent" @mouseout.prevent.stop="onMouseEvent" @mousemove.prevent.stop="onMouseEvent"
@wheel.prevent.stop="onMouseWheel" @wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel" @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
> >
<div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div> </div>
<div <div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-show="showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick" @click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable" v-html="statusBarClickable"
@@ -40,6 +36,29 @@
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты --> <!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas> <canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div> <div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
<!-- Примечание -->
<Dialog ref="dialog1" v-model="noteDialogVisible">
<template #header>
{{ noteTitle }}
</template>
<div class="column col" style="line-height: 20px; max-width: 400px; max-height: 200px; overflow-x: hidden; overflow-y: auto">
<div v-html="noteHtml"></div>
</div>
<template #footer>
<div class="row col">
<q-btn class="q-px-md q-mr-md" color="btn2" text-color="app" dense no-caps @click="goToNotes">
В примечания
</q-btn>
</div>
<q-btn class="q-px-md" color="btn2" text-color="app" dense no-caps @click="noteDialogVisible = false">
OK
</q-btn>
</template>
</Dialog>
</div> </div>
</template> </template>
@@ -51,6 +70,7 @@ import {loadCSS} from 'fg-loadcss';
import _ from 'lodash'; import _ from 'lodash';
import he from 'he'; import he from 'he';
import Dialog from '../../share/Dialog.vue';
import './TextPage.css'; import './TextPage.css';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
@@ -62,7 +82,19 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100; const minLayoutWidth = 100;
//обработчик кликов по примечаниям, см. DrawHelper
//коряво, но иначе придется сильно усложнять рендеринг страниц (через Vue)
window.onNoteClickLiberama = (noteId, orig) => {
const textPage = window.textPageLiberama;
if (textPage) {
textPage.showNote(noteId, orig);
}
}
const componentOptions = { const componentOptions = {
components: {
Dialog
},
watch: { watch: {
bookPos: function() { bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -90,6 +122,7 @@ class TextPage {
_options = componentOptions; _options = componentOptions;
showStatusBar = false; showStatusBar = false;
statusBarClickOpen = false;
clickControl = true; clickControl = true;
background = null; background = null;
@@ -114,6 +147,11 @@ class TextPage {
meta = null; meta = null;
noteDialogVisible = false;
noteId = '';
noteTitle = '';
noteHtml = '';
created() { created() {
this.drawHelper = new DrawHelper(); this.drawHelper = new DrawHelper();
@@ -153,6 +191,8 @@ class TextPage {
await utils.sleep(200); await utils.sleep(200);
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
}); });
window.textPageLiberama = this;
} }
mounted() { mounted() {
@@ -297,6 +337,8 @@ class TextPage {
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0); top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
let page1 = this.$refs.scrollBox1.style; let page1 = this.$refs.scrollBox1.style;
let page2 = this.$refs.scrollBox2.style; let page2 = this.$refs.scrollBox2.style;
page1.pointerEvents = page2.pointerEvents = (this.clickControl ? 'none' : 'auto');
page1.perspective = page2.perspective = '3072px'; page1.perspective = page2.perspective = '3072px';
@@ -913,6 +955,22 @@ class TextPage {
} }
} }
doPara(paraIndex) {
const para = this.parsed.para[paraIndex];
if (para && this.pageLineCount > 0) {
const lines = this.parsed.getLines(para.offset, this.pageLineCount);
if (lines.length >= this.pageLineCount) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = lines[0].begin;
} else
this.doEnd();
}
}
doToolBarToggle(event) { doToolBarToggle(event) {
this.$emit('do-action', {action: 'switchToolbar', event}); this.$emit('do-action', {action: 'switchToolbar', event});
} }
@@ -1016,6 +1074,7 @@ class TextPage {
if (this.startTouch) { if (this.startTouch) {
event.preventDefault(); event.preventDefault();
} }
this.endClickRepeat();
} }
onTouchEnd(event) { onTouchEnd(event) {
@@ -1100,6 +1159,9 @@ class TextPage {
onMouseWheel(event) { onMouseWheel(event) {
if (this.$root.isMobileDevice) if (this.$root.isMobileDevice)
return; return;
this.endClickRepeat();
if (event.deltaY > 0) { if (event.deltaY > 0) {
this.doDown(); this.doDown();
} else if (event.deltaY < 0) { } else if (event.deltaY < 0) {
@@ -1107,6 +1169,12 @@ class TextPage {
} }
} }
onMouseEvent() {
if (this.$root.isMobileDevice)
return;
this.endClickRepeat();
}
onStatusBarClick() { onStatusBarClick() {
const url = this.meta.url; const url = this.meta.url;
if (url && url.indexOf('disk://') != 0) { if (url && url.indexOf('disk://') != 0) {
@@ -1209,6 +1277,43 @@ class TextPage {
event.clipboardData.setData('text/plain', filtered); event.clipboardData.setData('text/plain', filtered);
} }
showNote(noteId, orig) {
const note = this.parsed.notes[noteId];
if (note) {
if (orig) {//show dialog
this.noteId = noteId;
this.noteTitle = `[${note.title?.trim()}]`;
this.noteHtml = note.xml
.replace(/<p>/g, '<p class="note-para">')
.replace(/<stanza>/g, '<br>').replace(/<\/stanza>/g, '')
.replace(/<v>/g, '<p style="margin: 0">').replace(/<\/v>/g, '</p>')
.replace(/<emphasis>/g, '<em>').replace(/<\/emphasis>/g, '</em>')
.replace(/<text-author>/g, '<br>').replace(/<\/text-author>/g, '')
;
this.noteDialogVisible = true;
} else {//go to orig
this.goToOrigNote(noteId);
}
}
}
goToNotes() {
const note = this.parsed.notes[this.noteId];
if (note && note.noteParaIndex >= 0) {
this.doPara(note.noteParaIndex);
this.noteDialogVisible = false;
}
}
goToOrigNote(noteId) {
const note = this.parsed.notes[noteId];
if (note && note.linkParaIndex >= 0) {
this.doPara(note.linkParaIndex);
this.noteDialogVisible = false;
}
}
} }
export default vueComponent(TextPage); export default vueComponent(TextPage);
@@ -1244,8 +1349,15 @@ export default vueComponent(TextPage);
} }
.events { .events {
z-index: 20; z-index: 9;
background-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);
} }
</style> </style>
<style>
.note-para {
margin: 0;
padding: 0;
margin-bottom: 10px;
}
</style>

View File

@@ -86,17 +86,24 @@ export default class BookParser {
let binaryType = ''; let binaryType = '';
let dimPromises = []; let dimPromises = [];
this.coverPageId = ''; this.coverPageId = '';
this.images = [];
let imageNum = 0;
//примечания
this.notes = {};
let inNote = false;
let noteId = '';
let inNotesBody = false;
const noteTags = new Set(['p', 'poem', 'stanza', 'v', 'text-author', 'emphasis']);
//оглавление //оглавление
this.contents = []; this.contents = [];
this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []}; let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''}; let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false; let inTitle = false;
let inSubtitle = false; let inSubtitle = false;
let sectionLevel = 0; let sectionLevel = 0;
let bodyIndex = 0; let bodyIndex = 0;
let imageNum = 0;
let paraIndex = -1; let paraIndex = -1;
let paraOffset = 0; let paraOffset = 0;
@@ -289,7 +296,7 @@ export default class BookParser {
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
const href = attrs.href.value; const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : ''); const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href); const {id, local} = this.hrefToId(href);
if (local) {//local if (local) {//local
imageNum++; imageNum++;
@@ -322,6 +329,23 @@ export default class BookParser {
} }
} }
if (tag == 'a') {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value && attrs.type && attrs.type.value === 'note') {//note
const href = attrs.href.value;
const {id, local} = this.hrefToId(href);
if (local) {
inNote = true;
growParagraph(`<note href="${id}" orig="1">`, 0);
if (!this.notes[id]) {
this.notes[id] = {id, linkParaIndex: paraIndex};
}
}
}
}
if (path == '/fictionbook/description/title-info/author') { if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author) if (!fb2.author)
fb2.author = []; fb2.author = [];
@@ -350,6 +374,11 @@ export default class BookParser {
if (path.indexOf('/fictionbook/body') == 0) { if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') { if (tag == 'body') {
let attrs = sax.getAttrsSync(tail);
if (attrs.name && attrs.name.value === 'notes') {//notes
inNotesBody = true;
}
if (isFirstBody && fb2.annotation) { if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v)); const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => { ann.forEach(a => {
@@ -373,6 +402,31 @@ export default class BookParser {
bodyIndex++; bodyIndex++;
} }
if (tag == 'section') {
if (!isFirstSection)
newParagraph();
isFirstSection = false;
sectionLevel++;
if (inNotesBody) {
let attrs = sax.getAttrsSync(tail);
if (attrs.id && attrs.id.value) {//notes
const id = attrs.id.value;
let note = this.notes[id];
if (!note) {
note = {id};
this.notes[id] = note;
}
note.noteParaIndex = paraIndex;
note.xml = '';
note.title = '';
noteId = id;
}
}
}
if (tag == 'title') { if (tag == 'title') {
newParagraph(); newParagraph();
isFirstTitlePara = true; isFirstTitlePara = true;
@@ -384,13 +438,6 @@ export default class BookParser {
this.contents.push(curTitle); this.contents.push(curTitle);
} }
if (tag == 'section') {
if (!isFirstSection)
newParagraph();
isFirstSection = false;
sectionLevel++;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') { if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`<${tag}>`, 0); growParagraph(`<${tag}>`, 0);
} }
@@ -401,6 +448,10 @@ export default class BookParser {
if (tag == 'p') { if (tag == 'p') {
inPara = true; inPara = true;
isFirstTitlePara = false; isFirstTitlePara = false;
if (inTitle && inNotesBody && noteId) {
growParagraph(`<note href="${noteId}">`, 0);
}
} }
} }
@@ -434,17 +485,30 @@ export default class BookParser {
bold = true; bold = true;
space += 1; space += 1;
} }
if (!inTitle && inNotesBody && noteId && noteTags.has(tag)) {
this.notes[noteId].xml += `<${tag}>`;
}
} }
}; };
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
tag = elemName; tag = elemName;
if (tag == 'a' && inNote) {
growParagraph('</note>', 0);
inNote = false;
}
if (tag == 'binary') { if (tag == 'binary') {
binaryId = ''; binaryId = '';
} }
if (path.indexOf('/fictionbook/body') == 0) { if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
inNotesBody = false;
}
if (tag == 'title') { if (tag == 'title') {
isFirstTitlePara = false; isFirstTitlePara = false;
bold = false; bold = false;
@@ -462,6 +526,10 @@ export default class BookParser {
if (tag == 'p') { if (tag == 'p') {
inPara = false; inPara = false;
if (inTitle && inNotesBody && noteId) {
growParagraph('</note>', 0);
}
} }
if (tag == 'subtitle') { if (tag == 'subtitle') {
@@ -485,6 +553,10 @@ export default class BookParser {
bold = false; bold = false;
space -= 1; space -= 1;
} }
if (!inTitle && inNotesBody && noteId && noteTags.has(tag)) {
this.notes[noteId].xml += `</${tag}>`;
}
} }
let i = path.lastIndexOf(tag); let i = path.lastIndexOf(tag);
@@ -570,6 +642,14 @@ export default class BookParser {
growParagraph(`${tOpen}${text}${tClose}`, text.length, text); growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else else
growParagraph(' ', 1); growParagraph(' ', 1);
if (inNotesBody && noteId) {
if (inTitle) {
this.notes[noteId].title += text;
} else {
this.notes[noteId].xml += text;
}
}
} }
}; };
@@ -602,7 +682,7 @@ export default class BookParser {
return {fb2}; return {fb2};
} }
imageHrefToId(id) { hrefToId(id) {
let local = false; let local = false;
if (id[0] == '#') { if (id[0] == '#') {
id = id.substr(1); id = id.substr(1);
@@ -635,7 +715,7 @@ export default class BookParser {
splitToStyle(s) { splitToStyle(s) {
let result = [];/*array of { let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number}, style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number, note: Object},
image: {local: Boolean, inline: Boolean, id: String}, image: {local: Boolean, inline: Boolean, id: String},
text: String, text: String,
}*/ }*/
@@ -686,7 +766,7 @@ export default class BookParser {
case 'image': { case 'image': {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
image = this.imageHrefToId(attrs.href.value); image = this.hrefToId(attrs.href.value);
image.inline = false; image.inline = false;
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0); image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
} }
@@ -695,7 +775,7 @@ export default class BookParser {
case 'image-inline': { case 'image-inline': {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
const img = this.imageHrefToId(attrs.href.value); const img = this.hrefToId(attrs.href.value);
img.inline = true; img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0); img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({ result.push({
@@ -706,6 +786,13 @@ export default class BookParser {
} }
break; break;
} }
case 'note': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
style.note = {id: attrs.href.value, orig: attrs.orig?.value};
}
break;
}
} }
}; };
@@ -734,6 +821,9 @@ export default class BookParser {
break; break;
case 'image-inline': case 'image-inline':
break; break;
case 'note':
style.note = false;
break;
} }
}; };

View File

@@ -1,4 +1,31 @@
export const versionHistory = [ export const versionHistory = [
{
version: '1.2.3',
releaseDate: '2024-08-02',
showUntil: '2024-08-01',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.2',
releaseDate: '2024-07-28',
showUntil: '2024-07-27',
content:
`
<ul>
<li>добавлено отображение примечаний на месте, по клику на сноске (#50)</li>
<li>исправление багов</li>
</ul>
`
},
{ {
version: '1.2.0', version: '1.2.0',
releaseDate: '2024-03-25', releaseDate: '2024-03-25',
@@ -7,7 +34,7 @@ export const versionHistory = [
` `
<ul> <ul>
<li>в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий</li> <li>в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий</li>
<li>добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параматр networkLibraryLink (#47)</li> <li>добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47)</li>
</ul> </ul>
` `

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "liberama", "name": "liberama",
"version": "1.1.3", "version": "1.2.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "liberama", "name": "liberama",
"version": "1.1.3", "version": "1.2.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "CC0-1.0", "license": "CC0-1.0",
"dependencies": { "dependencies": {
@@ -3364,9 +3364,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001566", "version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -13709,9 +13709,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001566", "version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"dev": true "dev": true
}, },
"chalk": { "chalk": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "liberama", "name": "liberama",
"version": "1.2.0", "version": "1.2.3",
"author": "Book Pauk <bookpauk@gmail.com>", "author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0", "license": "CC0-1.0",
"repository": "bookpauk/liberama", "repository": "bookpauk/liberama",

View File

@@ -56,6 +56,9 @@ module.exports = {
ip: '0.0.0.0', ip: '0.0.0.0',
port: '33443', port: '33443',
accessToken: '', accessToken: '',
shciForHost: {
'samlib.ru': 300000
},
}*/ }*/
], ],

View File

@@ -27,8 +27,8 @@ class BUCServer {
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
this.oldQueryInterval = 14*dayMs;//интервал устаревания запроса на обновление this.oldQueryInterval = 14*dayMs;//интервал устаревания запроса на обновление
this.checkingInterval = 5*hourMs;//интервал проверки обновления одного и того же файла this.checkingInterval = 1*dayMs;//интервал проверки обновления одного и того же файла
this.sameHostCheckInterval = 1000;//интервал проверки файла на том же сайте, не менее this.sameHostCheckInterval = 10*1000;//интервал проверки файла на том же сайте, не менее
} else { } else {
this.maxCheckQueueLength = 10;//максимальная длина checkQueue this.maxCheckQueueLength = 10;//максимальная длина checkQueue
this.fillCheckQueuePeriod = 10*1000;//период пополнения очереди this.fillCheckQueuePeriod = 10*1000;//период пополнения очереди
@@ -51,6 +51,7 @@ class BUCServer {
this.checkQueue = []; this.checkQueue = [];
this.hostChecking = {}; this.hostChecking = {};
this.shciForHost = this.config.shciForHost || {};//sameHostCheckInterval for host
this.main(); //no await this.main(); //no await
@@ -262,7 +263,7 @@ class BUCServer {
let unchanged = true; let unchanged = true;
let hash = ''; let hash = '';
const headers = await this.down.head(row.id); const headers = await this.down.head(row.id, {timeout: 10*1000});
const etag = headers['etag'] || ''; const etag = headers['etag'] || '';
const modTime = headers['last-modified'] || ''; const modTime = headers['last-modified'] || '';
@@ -276,7 +277,7 @@ class BUCServer {
&& (!size || !row.size || (size !== row.size)) && (!size || !row.size || (size !== row.size))
) { ) {
const downdata = await this.down.load(row.id); const downdata = await this.down.load(row.id, {timeout: 10*1000});
size = downdata.length; size = downdata.length;
hash = await utils.getBufHash(downdata, 'sha256', 'hex'); hash = await utils.getBufHash(downdata, 'sha256', 'hex');
@@ -316,7 +317,12 @@ class BUCServer {
log(LM_ERR, `error ${row.id} > ${e.stack ? e.stack : e.message}`); log(LM_ERR, `error ${row.id} > ${e.stack ? e.stack : e.message}`);
} finally { } finally {
(async() => { (async() => {
await utils.sleep(this.sameHostCheckInterval); let sameHostCheckInterval = this.shciForHost[url.hostname] || this.sameHostCheckInterval;
sameHostCheckInterval = Math.round((Math.random() - 0.5)*(sameHostCheckInterval*0.2) + sameHostCheckInterval);
log(`delay ${sameHostCheckInterval}ms for host '${url.hostname}'`);
await utils.sleep(sameHostCheckInterval);
this.hostChecking[url.hostname] = false; this.hostChecking[url.hostname] = false;
})(); })();
} }
@@ -327,7 +333,7 @@ class BUCServer {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
} }
await utils.sleep(10); await utils.sleep(100);
} }
} }

View File

@@ -2,7 +2,7 @@ const https = require('https');
const axios = require('axios'); const axios = require('axios');
const utils = require('./utils'); const utils = require('./utils');
const userAgent = 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'; const userAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0';
class FileDownloader { class FileDownloader {
constructor(limitDownloadSize = 0) { constructor(limitDownloadSize = 0) {
@@ -16,7 +16,6 @@ class FileDownloader {
headers: { headers: {
'accept-encoding': 'gzip, compress, deflate', 'accept-encoding': 'gzip, compress, deflate',
'user-agent': userAgent, 'user-agent': userAgent,
timeout: 300*1000,
}, },
httpsAgent: new https.Agent({ httpsAgent: new https.Agent({
rejectUnauthorized: false // решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом rejectUnauthorized: false // решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом
@@ -26,6 +25,9 @@ class FileDownloader {
if (opts) if (opts)
options = Object.assign({}, opts, options); options = Object.assign({}, opts, options);
if (!options.timeout)
options.timeout = 300*1000;//5 min
try { try {
const res = await axios.get(url, options); const res = await axios.get(url, options);
@@ -77,8 +79,8 @@ class FileDownloader {
const options = { const options = {
headers: { headers: {
'user-agent': userAgent, 'user-agent': userAgent,
timeout: 10*1000,
}, },
timeout: 10*1000,
}; };
const res = await axios.head(url, options); const res = await axios.head(url, options);

View File

@@ -1,4 +1,5 @@
require('tls').DEFAULT_MIN_VERSION = 'TLSv1'; require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const fs = require('fs-extra'); const fs = require('fs-extra');
const express = require('express'); const express = require('express');