+
+
+
+
@@ -51,6 +69,7 @@ import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import he from 'he';
+import Dialog from '../../share/Dialog.vue';
import './TextPage.css';
import * as utils from '../../../share/utils';
@@ -62,7 +81,19 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100;
+//обработчик кликов по примечаниям, см. DrawHelper
+//коряво, но иначе придется сильно усложнять рендеринг страниц (через Vue)
+window.onNoteClickLiberama = (noteId, orig) => {
+ const textPage = window.textPageLiberama;
+ if (textPage) {
+ textPage.showNote(noteId, orig);
+ }
+}
+
const componentOptions = {
+ components: {
+ Dialog
+ },
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -90,6 +121,7 @@ class TextPage {
_options = componentOptions;
showStatusBar = false;
+ statusBarClickOpen = false;
clickControl = true;
background = null;
@@ -114,6 +146,10 @@ class TextPage {
meta = null;
+ noteDialogVisible = false;
+ noteId = '';
+ noteHtml = '';
+
created() {
this.drawHelper = new DrawHelper();
@@ -153,6 +189,8 @@ class TextPage {
await utils.sleep(200);
this.$nextTick(this.onResize);
});
+
+ window.textPageLiberama = this;
}
mounted() {
@@ -297,6 +335,8 @@ class TextPage {
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
let page1 = this.$refs.scrollBox1.style;
let page2 = this.$refs.scrollBox2.style;
+
+ page1.pointerEvents = page2.pointerEvents = (this.clickControl ? 'none' : 'auto');
page1.perspective = page2.perspective = '3072px';
@@ -913,6 +953,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) {
this.$emit('do-action', {action: 'switchToolbar', event});
}
@@ -1209,6 +1265,36 @@ class TextPage {
event.clipboardData.setData('text/plain', filtered);
}
+
+ showNote(noteId, orig) {
+ const note = this.parsed.notes[noteId];
+ if (note) {
+ if (orig) {//show dialog
+ this.noteId = noteId;
+ const pad = (note.para.length > 1 ? 20 : 0);
+ this.noteHtml = note.para.map(p => `
${p}
`).join('');
+ 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);
@@ -1244,7 +1330,7 @@ export default vueComponent(TextPage);
}
.events {
- z-index: 20;
+ z-index: 9;
background-color: rgba(0,0,0,0);
}
diff --git a/client/components/Reader/share/BookParser.js b/client/components/Reader/share/BookParser.js
index cfceea3e..54f6236d 100644
--- a/client/components/Reader/share/BookParser.js
+++ b/client/components/Reader/share/BookParser.js
@@ -86,17 +86,23 @@ export default class BookParser {
let binaryType = '';
let dimPromises = [];
this.coverPageId = '';
+ this.images = [];
+ let imageNum = 0;
+
+ //примечания
+ this.notes = {};
+ let inNote = false;
+ let noteId = '';
+ let inNotesBody = false;
//оглавление
this.contents = [];
- this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
- let imageNum = 0;
let paraIndex = -1;
let paraOffset = 0;
@@ -289,7 +295,7 @@ export default class BookParser {
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);
+ const {id, local} = this.hrefToId(href);
if (local) {//local
imageNum++;
@@ -322,6 +328,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(`
`, 0);
+
+ if (!this.notes[id]) {
+ this.notes[id] = {id, linkParaIndex: paraIndex};
+ }
+ }
+ }
+ }
+
if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author)
fb2.author = [];
@@ -350,6 +373,11 @@ export default class BookParser {
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
+ let attrs = sax.getAttrsSync(tail);
+ if (attrs.name && attrs.name.value === 'notes') {//notes
+ inNotesBody = true;
+ }
+
if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => {
@@ -389,6 +417,23 @@ export default class BookParser {
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.para = [];
+ noteId = id;
+ }
+
+ }
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
@@ -401,6 +446,14 @@ export default class BookParser {
if (tag == 'p') {
inPara = true;
isFirstTitlePara = false;
+
+ if (inNotesBody && noteId) {
+ if (!inTitle) {
+ this.notes[noteId].para.push('');
+ } else {
+ growParagraph(``, 0);
+ }
+ }
}
}
@@ -440,11 +493,20 @@ export default class BookParser {
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
tag = elemName;
+ if (tag == 'a' && inNote) {
+ growParagraph('', 0);
+ inNote = false;
+ }
+
if (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
+ if (tag == 'body') {
+ inNotesBody = false;
+ }
+
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
@@ -462,6 +524,10 @@ export default class BookParser {
if (tag == 'p') {
inPara = false;
+
+ if (inTitle && inNotesBody && noteId) {
+ growParagraph('
', 0);
+ }
}
if (tag == 'subtitle') {
@@ -570,6 +636,12 @@ export default class BookParser {
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
+
+ if (!inTitle && inPara && inNotesBody && noteId) {
+ const p = this.notes[noteId].para;
+ if (p.length)
+ p[p.length - 1] = p[p.length - 1] + text;
+ }
}
};
@@ -602,7 +674,7 @@ export default class BookParser {
return {fb2};
}
- imageHrefToId(id) {
+ hrefToId(id) {
let local = false;
if (id[0] == '#') {
id = id.substr(1);
@@ -635,7 +707,7 @@ export default class BookParser {
splitToStyle(s) {
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},
text: String,
}*/
@@ -686,7 +758,7 @@ export default class BookParser {
case 'image': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
- image = this.imageHrefToId(attrs.href.value);
+ image = this.hrefToId(attrs.href.value);
image.inline = false;
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
}
@@ -695,7 +767,7 @@ export default class BookParser {
case 'image-inline': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
- const img = this.imageHrefToId(attrs.href.value);
+ const img = this.hrefToId(attrs.href.value);
img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({
@@ -706,6 +778,13 @@ export default class BookParser {
}
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 +813,9 @@ export default class BookParser {
break;
case 'image-inline':
break;
+ case 'note':
+ style.note = false;
+ break;
}
};
diff --git a/client/components/Reader/versionHistory.js b/client/components/Reader/versionHistory.js
index 69e756e6..d236b44b 100644
--- a/client/components/Reader/versionHistory.js
+++ b/client/components/Reader/versionHistory.js
@@ -1,4 +1,18 @@
export const versionHistory = [
+{
+ version: '1.2.1',
+ releaseDate: '2024-07-28',
+ showUntil: '2024-08-04',
+ content:
+`
+
+ - добавлено отображение примечаний на месте, по клику на сноске (#50)
+ - исправление багов
+
+
+`
+},
+
{
version: '1.2.0',
releaseDate: '2024-03-25',
@@ -7,7 +21,7 @@ export const versionHistory = [
`
- в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий
- - добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параматр networkLibraryLink (#47)
+ - добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47)
`
diff --git a/package-lock.json b/package-lock.json
index cf809269..165a27be 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "liberama",
- "version": "1.1.3",
+ "version": "1.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "liberama",
- "version": "1.1.3",
+ "version": "1.2.0",
"hasInstallScript": true,
"license": "CC0-1.0",
"dependencies": {
@@ -3364,9 +3364,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001566",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz",
- "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==",
+ "version": "1.0.30001643",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
+ "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"dev": true,
"funding": [
{
@@ -13709,9 +13709,9 @@
}
},
"caniuse-lite": {
- "version": "1.0.30001566",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz",
- "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==",
+ "version": "1.0.30001643",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
+ "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"dev": true
},
"chalk": {
diff --git a/package.json b/package.json
index d0958b2c..5a44327e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "liberama",
- "version": "1.2.0",
+ "version": "1.2.1",
"author": "Book Pauk
",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
diff --git a/server/config/base.js b/server/config/base.js
index c1045377..7f9daa00 100644
--- a/server/config/base.js
+++ b/server/config/base.js
@@ -56,6 +56,9 @@ module.exports = {
ip: '0.0.0.0',
port: '33443',
accessToken: '',
+ shciForHost: {
+ 'samlib.ru': 300000
+ },
}*/
],
diff --git a/server/core/BookUpdateChecker/BUCServer.js b/server/core/BookUpdateChecker/BUCServer.js
index d78c15b0..91525978 100644
--- a/server/core/BookUpdateChecker/BUCServer.js
+++ b/server/core/BookUpdateChecker/BUCServer.js
@@ -27,8 +27,8 @@ class BUCServer {
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
this.oldQueryInterval = 14*dayMs;//интервал устаревания запроса на обновление
- this.checkingInterval = 5*hourMs;//интервал проверки обновления одного и того же файла
- this.sameHostCheckInterval = 1000;//интервал проверки файла на том же сайте, не менее
+ this.checkingInterval = 1*dayMs;//интервал проверки обновления одного и того же файла
+ this.sameHostCheckInterval = 10*1000;//интервал проверки файла на том же сайте, не менее
} else {
this.maxCheckQueueLength = 10;//максимальная длина checkQueue
this.fillCheckQueuePeriod = 10*1000;//период пополнения очереди
@@ -51,6 +51,7 @@ class BUCServer {
this.checkQueue = [];
this.hostChecking = {};
+ this.shciForHost = this.config.shciForHost || {};//sameHostCheckInterval for host
this.main(); //no await
@@ -262,7 +263,7 @@ class BUCServer {
let unchanged = true;
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 modTime = headers['last-modified'] || '';
@@ -276,7 +277,7 @@ class BUCServer {
&& (!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;
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}`);
} finally {
(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;
})();
}
@@ -327,7 +333,7 @@ class BUCServer {
log(LM_ERR, e.stack);
}
- await utils.sleep(10);
+ await utils.sleep(100);
}
}
diff --git a/server/core/FileDownloader.js b/server/core/FileDownloader.js
index 6fa06111..327c2837 100644
--- a/server/core/FileDownloader.js
+++ b/server/core/FileDownloader.js
@@ -2,7 +2,7 @@ const https = require('https');
const axios = require('axios');
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 {
constructor(limitDownloadSize = 0) {
@@ -16,7 +16,6 @@ class FileDownloader {
headers: {
'accept-encoding': 'gzip, compress, deflate',
'user-agent': userAgent,
- timeout: 300*1000,
},
httpsAgent: new https.Agent({
rejectUnauthorized: false // решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом
@@ -26,6 +25,9 @@ class FileDownloader {
if (opts)
options = Object.assign({}, opts, options);
+ if (!options.timeout)
+ options.timeout = 300*1000;//5 min
+
try {
const res = await axios.get(url, options);
@@ -77,8 +79,8 @@ class FileDownloader {
const options = {
headers: {
'user-agent': userAgent,
- timeout: 10*1000,
},
+ timeout: 10*1000,
};
const res = await axios.head(url, options);
diff --git a/server/index.js b/server/index.js
index 2bc41cdb..03bef2bb 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,4 +1,5 @@
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const fs = require('fs-extra');
const express = require('express');