Pārlūkot izejas kodu

Добавлено отображение примечаний на месте, по клику на примечании (#50)

Book Pauk 11 mēneši atpakaļ
vecāks
revīzija
0430105061

+ 9 - 0
client/components/Reader/TextPage/DrawHelper.js

@@ -39,6 +39,7 @@ export default class DrawHelper {
         let center = false;
         let center = false;
         let space = 0;
         let space = 0;
         let j = 0;
         let j = 0;
+        const pad = this.fontSize/2;
         //формируем строку
         //формируем строку
         for (const part of line.parts) {
         for (const part of line.parts) {
             let tOpen = '';
             let tOpen = '';
@@ -46,7 +47,12 @@ 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">' : '');
+            tOpen += (part.style.note ? `<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: -${pad}px; top: -${pad*0.9}px; border-radius: ${this.fontSize}px;" ` +
+                `onclick="onNoteClickLiberama('${part.style.note.id}', ${part.style.note.orig ? 1 : 0})"><span style="visibility: hidden;">__TEXT</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>' : '');
@@ -64,6 +70,9 @@ export default class DrawHelper {
             if (text && text.trim() == '')
             if (text && text.trim() == '')
                 text = `<span style="white-space: pre">${text}</span>`;
                 text = `<span style="white-space: pre">${text}</span>`;
 
 
+            if (part.style.note)
+                tOpen = tOpen.replace('__TEXT', text);
+
             lineText += `${tOpen}${text}${tClose}`;
             lineText += `${tOpen}${text}${tClose}`;
 
 
             center = center || part.style.center;
             center = center || part.style.center;

+ 89 - 9
client/components/Reader/TextPage/TextPage.vue

@@ -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>
@@ -24,14 +24,9 @@
             @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 +35,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>
+                Примечание
+            </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-sm" 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 +69,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 +81,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 +121,7 @@ class TextPage {
     _options = componentOptions;
     _options = componentOptions;
 
 
     showStatusBar = false;
     showStatusBar = false;
+    statusBarClickOpen = false;
     clickControl = true;
     clickControl = true;
 
 
     background = null;
     background = null;
@@ -114,6 +146,10 @@ class TextPage {
 
 
     meta = null;
     meta = null;
 
 
+    noteDialogVisible = false;
+    noteId = '';
+    noteHtml = '';
+
     created() {
     created() {
         this.drawHelper = new DrawHelper();
         this.drawHelper = new DrawHelper();
 
 
@@ -153,6 +189,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 +335,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';
 
 
@@ -1209,6 +1249,46 @@ 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;
+                const pad = (note.para.length > 1 ? 20 : 0);
+                this.noteHtml = note.para.map(p => `<p style="margin: 0; padding-left: ${pad}px">${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) {
+
+            const para = this.parsed.parsePara(note.noteParaIndex);
+
+            this.userBookPosChange = true;
+            this.bookPos = para.lines[0].begin;
+
+            this.noteDialogVisible = false;
+        }
+    }
+
+    goToOrigNote(noteId) {
+        const note = this.parsed.notes[noteId];
+        if (note && note.noteParaIndex >= 0) {
+
+            const para = this.parsed.parsePara(note.linkParaIndex);
+
+            this.userBookPosChange = true;
+            this.bookPos = para.lines[0].begin;
+
+            this.noteDialogVisible = false;
+        }
+    }
 }
 }
 
 
 export default vueComponent(TextPage);
 export default vueComponent(TextPage);
@@ -1244,7 +1324,7 @@ 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);
 }
 }
 
 

+ 89 - 7
client/components/Reader/share/BookParser.js

@@ -86,17 +86,23 @@ 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;
 
 
         //оглавление
         //оглавление
         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 +295,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 +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(`<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 +373,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 => {
@@ -389,6 +417,23 @@ export default class BookParser {
                         newParagraph();
                         newParagraph();
                     isFirstSection = false;
                     isFirstSection = false;
                     sectionLevel++;
                     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') {
                 if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
@@ -401,6 +446,14 @@ export default class BookParser {
                     if (tag == 'p') {
                     if (tag == 'p') {
                         inPara = true;
                         inPara = true;
                         isFirstTitlePara = false;
                         isFirstTitlePara = false;
+
+                        if (inNotesBody && noteId) {
+                            if (!inTitle) {
+                                this.notes[noteId].para.push('');
+                            } else {
+                                growParagraph(`<note href="${noteId}">`, 0);
+                            }
+                        }
                     }
                     }
                 }
                 }
 
 
@@ -440,11 +493,20 @@ export default class BookParser {
         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 +524,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') {
@@ -570,6 +636,12 @@ 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 (!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};
         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 +707,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 +758,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 +767,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 +778,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 +813,9 @@ export default class BookParser {
                     break;
                     break;
                 case 'image-inline':
                 case 'image-inline':
                     break;
                     break;
+                case 'note':
+                    style.note = false;
+                    break;
             }
             }
         };
         };