diff --git a/client/components/Reader/Reader.vue b/client/components/Reader/Reader.vue index dc14f10e..efb833a2 100644 --- a/client/components/Reader/Reader.vue +++ b/client/components/Reader/Reader.vue @@ -340,8 +340,8 @@ class Reader extends Vue { this.setPositionActive = true; this.$nextTick(() => { - this.$refs.setPositionPage.sliderMax = this.mostRecentBook().textLength - 1; - this.$refs.setPositionPage.sliderValue = this.mostRecentBook().bookPos; + const recent = this.mostRecentBook(); + this.$refs.setPositionPage.init(recent.bookPos, recent.textLength - 1); }); } else { this.setPositionActive = false; diff --git a/client/components/Reader/SetPositionPage/SetPositionPage.vue b/client/components/Reader/SetPositionPage/SetPositionPage.vue index 52206a4a..b652241d 100644 --- a/client/components/Reader/SetPositionPage/SetPositionPage.vue +++ b/client/components/Reader/SetPositionPage/SetPositionPage.vue @@ -18,7 +18,6 @@ //----------------------------------------------------------------------------- import Vue from 'vue'; import Component from 'vue-class-component'; -import _ from 'lodash'; import Window from '../../share/Window.vue'; @@ -28,7 +27,8 @@ export default @Component({ }, watch: { sliderValue: function(newValue) { - this.$emit('book-pos-changed', {bookPos: newValue}); + if (this.initialized) + this.$emit('book-pos-changed', {bookPos: newValue}); }, }, }) @@ -39,6 +39,13 @@ class SetPositionPage extends Vue { created() { this.commit = this.$store.commit; this.reader = this.$store.state.reader; + this.initialized = false; + } + + init(sliderValue, sliderMax) { + this.sliderMax = sliderMax; + this.sliderValue = sliderValue; + this.initialized = true; } formatTooltip(val) { diff --git a/client/components/Reader/share/bookManager.js b/client/components/Reader/share/bookManager.js index a1bfa326..a4c9e3e5 100644 --- a/client/components/Reader/share/bookManager.js +++ b/client/components/Reader/share/bookManager.js @@ -25,6 +25,7 @@ class BookManager { async init(settings) { this.settings = settings; + //bmCacheStore нужен только для ускорения загрузки читалки this.booksCached = await bmCacheStore.getItem('books'); if (!this.booksCached) this.booksCached = {}; @@ -47,9 +48,9 @@ class BookManager { } } - //долгая загрузка из хранилища - //bmMetaStore и bmRecentStore в будущем можно будет убрать - //bmCacheStore достаточно + //долгая загрузка из хранилища, + //хранение в отдельных записях дает относительно + //нормальное поведение при нескольких вкладках с читалкой в браузере async loadMeta(immediate) { if (!immediate) await utils.sleep(2000); diff --git a/package.json b/package.json index 021feee9..5e422b4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Liberama", - "version": "0.5.3", + "version": "0.5.4", "engines": { "node": ">=10.0.0" }, diff --git a/server/core/BookConverter/ConvertHtml.js b/server/core/BookConverter/ConvertHtml.js index ce28f9d4..eb9f6e73 100644 --- a/server/core/BookConverter/ConvertHtml.js +++ b/server/core/BookConverter/ConvertHtml.js @@ -34,10 +34,15 @@ class ConvertHtml extends ConvertBase { let desc = {_n: 'description', 'title-info': titleInfo}; let pars = []; let body = {_n: 'body', section: {_a: []}}; - let fb2 = [desc, body]; + let binary = []; + let fb2 = [desc, body, binary]; let title = ''; let inTitle = false; + let inImage = false; + let image = {}; + let bold = false; + let italic = false; let spaceCounter = []; @@ -71,37 +76,93 @@ class ConvertHtml extends ConvertBase { } }; - const newPara = new Set(['tr', 'br', 'br/', 'dd', 'p', 'title', '/title', 'h1', 'h2', 'h3', '/h1', '/h2', '/h3']); + const newPara = new Set(['tr', '/table', 'hr', 'br', 'br/', 'li', 'dt', 'dd', 'p', 'title', '/title', 'h1', 'h2', 'h3', '/h1', '/h2', '/h3']); const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars if (!cutCounter && !(cutTitle && inTitle)) { - growParagraph(text); + let tOpen = (bold ? '' : ''); + tOpen += (italic ? '' : ''); + let tClose = (italic ? '' : ''); + tClose += (bold ? '' : ''); + + growParagraph(`${tOpen}${text}${tClose}`); } if (inTitle && !title) title = text; + + if (inImage) { + image._t = text; + binary.push(image); + + pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''}); + newParagraph(); + } + }; const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars if (!cutCounter) { if (newPara.has(tag)) newParagraph(); + + switch (tag) { + case 'i': + case 'em': + italic = true; + break; + case 'b': + case 'strong': + case 'h1': + case 'h2': + case 'h3': + bold = true; + break; + } } if (tag == 'title') inTitle = true; + + if (tag == 'fb2-image') { + inImage = true; + const attrs = sax.getAttrsSync(tail); + image = {_n: 'binary', _attrs: {id: attrs.name.value, 'content-type': attrs.type.value}, _t: ''}; + } }; const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars + if (!cutCounter) { + if (newPara.has('/' + tag)) + newParagraph(); + + switch (tag) { + case 'i': + case 'em': + italic = false; + break; + case 'b': + case 'strong': + case 'h1': + case 'h2': + case 'h3': + bold = false; + break; + } + } + if (tag == 'title') inTitle = false; + + if (tag == 'fb2-image') + inImage = false; }; let buf = this.decode(data).toString(); sax.parseSync(buf, { onStartNode, onEndNode, onTextNode, - innerCut: new Set(['head', 'script', 'style', 'binary']) + innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image']) }); titleInfo['book-title'] = title; @@ -148,10 +209,16 @@ class ConvertHtml extends ConvertBase { i = 0; for (const par of pars) { + if (par._n != 'p') { + newPars.push(par); + continue; + } + if (i > 0) newPar(); i++; + let j = 0; const lines = par._t.split('\n'); for (let line of lines) { line = repCrLfTab(line); @@ -161,8 +228,11 @@ class ConvertHtml extends ConvertBase { l++; } - if (l >= parIndent) - newPar(); + if (l >= parIndent) { + if (j > 0) + newPar(); + j++; + } growPar(line.trim() + ' '); } } @@ -173,6 +243,7 @@ class ConvertHtml extends ConvertBase { } //убираем лишнее + pars = body.section._a[0]; for (let i = 0; i < pars.length; i++) pars[i]._t = this.repSpaces(pars[i]._t).trim(); diff --git a/server/core/BookConverter/ConvertPdf.js b/server/core/BookConverter/ConvertPdf.js index 236d0a1a..967bcdaa 100644 --- a/server/core/BookConverter/ConvertPdf.js +++ b/server/core/BookConverter/ConvertPdf.js @@ -1,4 +1,5 @@ const fs = require('fs-extra'); +const path = require('path'); const sax = require('./sax'); const utils = require('../utils'); @@ -34,14 +35,47 @@ class ConvertPdf extends ConvertHtml { //парсим xml let lines = []; + let images = []; + let loading = []; let inText = false; + let bold = false; + let italic = false; let title = ''; let prevTop = 0; let i = -1; + const loadImage = async(image) => { + const src = path.parse(image.src); + let type = 'unknown'; + switch (src.ext) { + case '.jpg': type = 'image/jpeg'; break; + case '.png': type = 'image/png'; break; + } + if (type != 'unknown') { + image.data = (await fs.readFile(image.src)).toString('base64'); + image.type = type; + image.name = src.base; + } + } + + const putImage = (curTop) => { + if (!isNaN(curTop) && images.length) { + while (images.length && images[0].top < curTop) { + i++; + lines[i] = images[0]; + images.shift(); + } + } + } + const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars if (!cutCounter && inText) { - lines[i].text += text + ' '; + let tOpen = (bold ? '' : ''); + tOpen += (italic ? '' : ''); + let tClose = (italic ? '' : ''); + tClose += (bold ? '' : ''); + + lines[i].text += `${tOpen}${text}${tClose} `; if (i < 2) title += text + ' '; } @@ -49,6 +83,17 @@ class ConvertPdf extends ConvertHtml { const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars if (!cutCounter) { + if (inText) { + switch (tag) { + case 'i': + italic = true; + break; + case 'b': + bold = true; + break; + } + } + if (tag == 'text' && !inText) { let attrs = sax.getAttrsSync(tail); const line = { @@ -59,19 +104,52 @@ class ConvertPdf extends ConvertHtml { height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10), }; - if (line.width !== '0' || line.height !== '0') { + if (line.width != 0 || line.height != 0) { inText = true; if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) { + putImage(line.top); i++; lines[i] = line; } prevTop = line.top; } } + + if (tag == 'image') { + const attrs = sax.getAttrsSync(tail); + const src = (attrs.src && attrs.src.value ? attrs.src.value : ''); + if (src) { + const image = { + isImage: true, + src, + data: '', + type: '', + top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0, + }; + loading.push(loadImage(image)); + images.push(image); + images.sort((a, b) => a.top - b.top) + } + } + + if (tag == 'page') { + putImage(100000); + } } }; const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars + if (inText) { + switch (tag) { + case 'i': + italic = false; + break; + case 'b': + bold = false; + break; + } + } + if (tag == 'text') inText = false; }; @@ -81,9 +159,15 @@ class ConvertPdf extends ConvertHtml { onStartNode, onEndNode, onTextNode }); + putImage(100000); + + await Promise.all(loading); + //найдем параграфы и отступы const indents = []; for (const line of lines) { + if (line.isImage) + continue; if (!isNaN(line.left)) { indents[line.left] = 1; } @@ -103,6 +187,11 @@ class ConvertPdf extends ConvertHtml { let concat = ''; let sp = ''; for (const line of lines) { + if (line.isImage) { + text += `${line.data}`; + continue; + } + if (concat == '') { const left = line.left || 0; sp = ' '.repeat(indents[left]); diff --git a/server/core/BookConverter/ConvertSamlib.js b/server/core/BookConverter/ConvertSamlib.js index 474fe0a1..30eda273 100644 --- a/server/core/BookConverter/ConvertSamlib.js +++ b/server/core/BookConverter/ConvertSamlib.js @@ -135,7 +135,7 @@ class ConvertSamlib extends ConvertBase { let href = attrs.src.value; if (href[0] == '/') href = `http://${hostname}${href}`; - openTag('image', {href}); + openTag('image', {'l:href': href}); inImage = true; } break;