diff --git a/client/components/Reader/TextPage/DrawHelper.js b/client/components/Reader/TextPage/DrawHelper.js index 2bcf2583..f3bf9e77 100644 --- a/client/components/Reader/TextPage/DrawHelper.js +++ b/client/components/Reader/TextPage/DrawHelper.js @@ -39,6 +39,7 @@ export default class DrawHelper { let center = false; let space = 0; let j = 0; + const pad = this.fontSize/2; //формируем строку for (const part of line.parts) { let tOpen = ''; @@ -46,7 +47,12 @@ export default class DrawHelper { tOpen += (part.style.italic ? '' : ''); tOpen += (part.style.sup ? '' : ''); tOpen += (part.style.sub ? '' : ''); + tOpen += (part.style.note ? `` + + `__TEXT` : ''); let tClose = ''; + tClose += (part.style.note ? '' : ''); tClose += (part.style.sub ? '' : ''); tClose += (part.style.sup ? '' : ''); tClose += (part.style.italic ? '' : ''); @@ -64,6 +70,9 @@ export default class DrawHelper { if (text && text.trim() == '') text = `${text}`; + if (part.style.note) + tOpen = tOpen.replace('__TEXT', text); + lineText += `${tOpen}${text}${tClose}`; center = center || part.style.center; diff --git a/client/components/Reader/TextPage/TextPage.vue b/client/components/Reader/TextPage/TextPage.vue index 5568a501..8d6ab4f8 100644 --- a/client/components/Reader/TextPage/TextPage.vue +++ b/client/components/Reader/TextPage/TextPage.vue @@ -4,12 +4,12 @@
-
+
-
+
@@ -24,14 +24,9 @@ @wheel.prevent.stop="onMouseWheel" @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel" > -
+ + + + + +
+
+
+ + +
@@ -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');