Browse Source

Merge branch 'release/0.9.8'

Book Pauk 4 năm trước cách đây
mục cha
commit
b387f4a0db

+ 207 - 0
client/components/Reader/ContentsPage/ContentsPage.vue

@@ -0,0 +1,207 @@
+<template>
+    <Window width="600px" ref="window" @close="close">
+        <template slot="header">
+            Оглавление/закладки
+        </template>
+
+    <div class="bg-grey-3 row">
+        <q-tabs
+            v-model="selectedTab"
+            active-color="black"
+            active-bg-color="white"
+            indicator-color="white"
+            dense
+            no-caps
+            inline-label
+            class="no-mp bg-grey-4 text-grey-7"
+        >
+            <q-tab name="contents" icon="la la-list" label="Оглавление" />
+            <q-tab name="bookmarks"  icon="la la-bookmark" label="Закладки" />
+        </q-tabs>
+    </div>
+
+    <div class="q-mb-sm"/>
+
+    <div class="tab-panel" v-show="selectedTab == 'contents'">
+        <div>
+            <div class="row" v-for="item in contents" :key="item.key">                
+                <q-expansion-item v-if="item.list.length"
+                    class="item separator-bottom"
+                    expand-icon-toggle
+                    switch-toggle-side
+                    expand-icon="la la-arrow-circle-down"
+                >
+                    <template slot="header">
+                        <div class="row no-wrap clickable" style="width: 465px" @click="setBookPos(item.offset)">
+                            <div :style="item.style"></div>
+                            <div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
+                            <div class="column justify-center">{{ item.perc }}%</div>
+                        </div>
+                    </template>
+
+                    <q-item class="subitem separator-top column justify-center" v-for="subitem in item.list" :key="subitem.key">
+                        <div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(subitem.offset)">
+                            <div :style="subitem.style"></div>
+                            <div class="q-mr-sm col overflow-hidden column justify-center"  v-html="subitem.label"></div>
+                            <div class="column justify-center">{{ subitem.perc }}%</div>
+                        </div>
+                    </q-item>
+                </q-expansion-item>
+                <q-item v-else class="item separator-bottom">
+                    <div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(item.offset)">
+                        <div :style="item.style"></div>
+                        <div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
+                        <div class="column justify-center">{{ item.perc }}%</div>
+                    </div>
+                </q-item>
+            </div>
+        </div>
+    </div>
+
+    <div class="tab-panel" v-show="selectedTab == 'bookmarks'">
+        <div class="column justify-center items-center" style="height: 100px">
+            Раздел находится в разработке
+        </div>
+    </div>
+
+    </Window>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+//import _ from 'lodash';
+
+import Window from '../../share/Window.vue';
+//import * as utils from '../../../share/utils';
+
+export default @Component({
+    components: {
+        Window,
+    },
+    watch: {
+    },
+})
+class ContentsPage extends Vue {
+    selectedTab = 'contents';
+    contents = [];
+
+    created() {
+    }
+
+    async init(currentBook, parsed) {
+        this.$refs.window.init();
+
+        if (this.parsed != parsed) {
+            this.contents = [];
+            await this.$nextTick();
+            this.parsed = parsed;
+        }
+
+        const prepareLabel = (title, bolder = false) => {
+            let titleParts = title.split('<p>');
+            const textParts = titleParts.filter(v => v).map(v => `<div>${v.replace(/(<([^>]+)>)/ig, '')}</div>`);
+            if (bolder && textParts.length > 1)
+                textParts[0] = `<b>${textParts[0]}</b>`;
+            return textParts.join('');
+        }
+
+        const insetStyle = inset => `width: ${inset*20}px`;
+        const pc = parsed.contents;
+        const newpc = [];
+
+        //преобразуем не первые разделы body в title-subtitle
+        let curSubtitles = [];
+        let prevBodyIndex = -1;
+        for (let i = 0; i < pc.length; i++) {
+            const cont = pc[i];
+            if (prevBodyIndex != cont.bodyIndex)
+                curSubtitles = [];
+
+            prevBodyIndex = cont.bodyIndex;
+
+            if (cont.bodyIndex > 1) {
+                if (cont.inset < 1) {
+                    newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
+                } else {
+                    curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
+                }
+            } else {
+                newpc.push(cont);
+            }
+        }
+
+        //формируем newContents
+        let i = 0;
+        const newContents = [];
+        newpc.forEach((cont) => {
+            const label = prepareLabel(cont.title, true);
+            const style = insetStyle(cont.inset);
+
+            let j = 0;
+            const list = [];
+            cont.subtitles.forEach((sub) => {
+                const l = prepareLabel(sub.title);
+                const s = insetStyle(sub.inset + 1);
+                const p = parsed.para[sub.paraIndex];
+                list.push({perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, style: s});
+                j++;
+            });
+
+            const p = parsed.para[cont.paraIndex];
+            newContents.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, style, list});
+
+            i++;
+        });
+
+        this.contents = newContents;
+    }
+
+    async setBookPos(newValue) {
+        this.$emit('book-pos-changed', {bookPos: newValue});
+        await this.$nextTick();
+        this.close();
+    }
+
+    close() {
+        this.$emit('do-action', {action: 'contents'});
+    }
+
+    keyHook(event) {
+        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
+            this.close();
+        }
+        return true;
+    }
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.tab-panel {
+    overflow-x: hidden;
+    overflow-y: auto;
+    font-size: 90%;
+    padding: 0 10px 0px 10px;
+}
+
+.clickable {
+    cursor: pointer;
+}
+
+.item:hover {
+    background-color: #f0f0f0;
+}
+
+.subitem:hover {
+    background-color: #e0e0e0;
+}
+
+.separator-top {
+    border-top: 1px solid #e0e0e0;
+}
+.separator-bottom {
+    border-top: 1px solid #e0e0e0;
+}
+</style>

+ 35 - 1
client/components/Reader/Reader.vue

@@ -48,6 +48,10 @@
                         <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
                         <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
                     </button>
                     </button>
                     <div class="space"></div>
                     <div class="space"></div>
+                    <button ref="contents" v-show="showToolButton['contents']" class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')" v-ripple>
+                        <q-icon name="la la-list" size="32px"/>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['contents'] }}</q-tooltip>
+                    </button>
                     <button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
                     <button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
                         <q-icon name="la la-sitemap" size="32px"/>
                         <q-icon name="la la-sitemap" size="32px"/>
                         <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
                         <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
@@ -89,12 +93,13 @@
                 @stop-text-search="stopTextSearch">
                 @stop-text-search="stopTextSearch">
             </SearchPage>
             </SearchPage>
             <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
             <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
-            <LibsPage v-show="libsActive" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
+            <LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
             <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
             <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
             <SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
             <SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
             <HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
             <HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
             <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
             <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
             <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
             <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
+            <ContentsPage v-show="contentsActive" ref="contentsPage" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
 
 
             <ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
             <ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
         </div>
         </div>
@@ -121,6 +126,8 @@ import SettingsPage from './SettingsPage/SettingsPage.vue';
 import HelpPage from './HelpPage/HelpPage.vue';
 import HelpPage from './HelpPage/HelpPage.vue';
 import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
 import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
 import ServerStorage from './ServerStorage/ServerStorage.vue';
 import ServerStorage from './ServerStorage/ServerStorage.vue';
+import ContentsPage from './ContentsPage/ContentsPage.vue';
+
 import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
 import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
 
 
 import bookManager from './share/bookManager';
 import bookManager from './share/bookManager';
@@ -143,6 +150,8 @@ export default @Component({
         HelpPage,
         HelpPage,
         ClickMapPage,
         ClickMapPage,
         ServerStorage,
         ServerStorage,
+        ContentsPage,
+
         ReaderDialogs,
         ReaderDialogs,
     },
     },
     watch: {
     watch: {
@@ -200,6 +209,7 @@ class Reader extends Vue {
     settingsActive = false;
     settingsActive = false;
     helpActive = false;
     helpActive = false;
     clickMapActive = false;
     clickMapActive = false;
+    contentsActive = false;
 
 
     bookPos = null;
     bookPos = null;
     allowUrlParamBookPos = false;
     allowUrlParamBookPos = false;
@@ -490,6 +500,7 @@ class Reader extends Vue {
         this.stopScrolling();
         this.stopScrolling();
         this.stopSearch();
         this.stopSearch();
         this.helpActive = false;
         this.helpActive = false;
+        this.contentsActive = false;
     }
     }
 
 
     loaderToggle() {
     loaderToggle() {
@@ -603,6 +614,21 @@ class Reader extends Vue {
         }
         }
     }
     }
 
 
+    contentsPageToggle() {
+        this.contentsActive = !this.contentsActive;
+        const page = this.$refs.page;
+        if (this.contentsActive && this.activePage == 'TextPage' && page.parsed) {
+            this.closeAllWindows();
+            this.contentsActive = true;
+
+            this.$nextTick(() => {
+                this.$refs.contentsPage.init(this.mostRecentBook(), page.parsed);
+            });
+        } else {
+            this.contentsActive = false;
+        }
+    }
+
     libsClose() {
     libsClose() {
         if (this.libsActive)
         if (this.libsActive)
             this.libsToogle();
             this.libsToogle();
@@ -707,6 +733,7 @@ class Reader extends Vue {
             case 'copyText':
             case 'copyText':
             case 'splitToPara':
             case 'splitToPara':
             case 'refresh':
             case 'refresh':
+            case 'contents':
             case 'libs':
             case 'libs':
             case 'recentBooks':
             case 'recentBooks':
             case 'offlineMode':
             case 'offlineMode':
@@ -735,6 +762,7 @@ class Reader extends Vue {
                 case 'scrolling':
                 case 'scrolling':
                 case 'search':
                 case 'search':
                 case 'copyText':
                 case 'copyText':
+                case 'contents':
                     classResult = classDisabled;
                     classResult = classDisabled;
                     break;
                     break;
                 case 'splitToPara':
                 case 'splitToPara':
@@ -1026,6 +1054,9 @@ class Reader extends Vue {
             case 'refresh':
             case 'refresh':
                 this.refreshBook();
                 this.refreshBook();
                 break;
                 break;
+            case 'contents':
+                this.contentsPageToggle();
+                break;
             case 'libs':
             case 'libs':
                 this.libsToogle();
                 this.libsToogle();
                 break;
                 break;
@@ -1125,6 +1156,9 @@ class Reader extends Vue {
             if (!result && this.copyTextActive)
             if (!result && this.copyTextActive)
                 result = this.$refs.copyTextPage.keyHook(event);
                 result = this.$refs.copyTextPage.keyHook(event);
 
 
+            if (!result && this.contentsActive)
+                result = this.$refs.contentsPage.keyHook(event);
+
             if (!result && this.$refs.page && this.$refs.page.keyHook)
             if (!result && this.$refs.page && this.$refs.page.keyHook)
                 result = this.$refs.page.keyHook(event);
                 result = this.$refs.page.keyHook(event);
 
 

+ 51 - 4
client/components/Reader/share/BookParser.js

@@ -46,11 +46,21 @@ export default class BookParser {
         let isFirstSection = true;
         let isFirstSection = true;
         let isFirstTitlePara = false;
         let isFirstTitlePara = false;
 
 
+        //изображения
         this.binary = {};
         this.binary = {};
         let binaryId = '';
         let binaryId = '';
         let binaryType = '';
         let binaryType = '';
         let dimPromises = [];
         let dimPromises = [];
 
 
+        //оглавление
+        this.contents = [];
+        let curTitle = {paraIndex: -1, title: '', subtitles: []};
+        let curSubtitle = {paraIndex: -1, title: ''};
+        let inTitle = false;
+        let inSubtitle = false;
+        let sectionLevel = 0;
+        let bodyIndex = 0;
+
         let paraIndex = -1;
         let paraIndex = -1;
         let paraOffset = 0;
         let paraOffset = 0;
         let para = []; /*array of
         let para = []; /*array of
@@ -118,6 +128,12 @@ export default class BookParser {
                 addIndex: (addIndex ? addIndex : 0),
                 addIndex: (addIndex ? addIndex : 0),
             };
             };
 
 
+            if (inSubtitle) {
+                curSubtitle.title += '<p>';
+            } else if (inTitle) {
+                curTitle.title += '<p>';
+            }
+
             para[paraIndex] = p;
             para[paraIndex] = p;
             paraOffset += p.length;
             paraOffset += p.length;
         };
         };
@@ -129,6 +145,7 @@ export default class BookParser {
                 return;
                 return;
             }
             }
 
 
+            const prevParaIndex = paraIndex;
             let p = para[paraIndex];
             let p = para[paraIndex];
             paraOffset -= p.length;
             paraOffset -= p.length;
             //добавление пустых (addEmptyParagraphs) параграфов перед текущим
             //добавление пустых (addEmptyParagraphs) параграфов перед текущим
@@ -143,6 +160,11 @@ export default class BookParser {
                 p.offset = paraOffset;
                 p.offset = paraOffset;
                 para[paraIndex] = p;
                 para[paraIndex] = p;
 
 
+                if (curTitle.paraIndex == prevParaIndex)
+                    curTitle.paraIndex = paraIndex;
+                if (curSubtitle.paraIndex == prevParaIndex)
+                    curSubtitle.paraIndex = paraIndex;
+
                 //уберем начальный пробел
                 //уберем начальный пробел
                 p.length = 0;
                 p.length = 0;
                 p.text = p.text.substr(1);
                 p.text = p.text.substr(1);
@@ -151,6 +173,13 @@ export default class BookParser {
             p.length += len;
             p.length += len;
             p.text += text;
             p.text += text;
 
 
+            
+            if (inSubtitle) {
+                curSubtitle.title += text;
+            } else if (inTitle) {
+                curTitle.title += text;
+            }
+
             para[paraIndex] = p;
             para[paraIndex] = p;
             paraOffset += p.length;
             paraOffset += p.length;
         };
         };
@@ -160,7 +189,7 @@ export default class BookParser {
                 return;
                 return;
 
 
             tag = elemName;
             tag = elemName;
-            path += '/' + elemName;
+            path += '/' + tag;
 
 
             if (tag == 'binary') {
             if (tag == 'binary') {
                 let attrs = sax.getAttrsSync(tail);
                 let attrs = sax.getAttrsSync(tail);
@@ -187,7 +216,7 @@ export default class BookParser {
                 }
                 }
             }
             }
 
 
-            if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
+            if (tag == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
                 if (!fb2.author)
                 if (!fb2.author)
                     fb2.author = [];
                     fb2.author = [];
                 fb2.author.push({});
                 fb2.author.push({});
@@ -198,6 +227,7 @@ export default class BookParser {
                     if (!isFirstBody)
                     if (!isFirstBody)
                         newParagraph(' ', 1);
                         newParagraph(' ', 1);
                     isFirstBody = false;
                     isFirstBody = false;
+                    bodyIndex++;
                 }
                 }
 
 
                 if (tag == 'title') {
                 if (tag == 'title') {
@@ -205,12 +235,17 @@ export default class BookParser {
                     isFirstTitlePara = true;
                     isFirstTitlePara = true;
                     bold = true;
                     bold = true;
                     center = true;
                     center = true;
+
+                    inTitle = true;
+                    curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
+                    this.contents.push(curTitle);
                 }
                 }
 
 
                 if (tag == 'section') {
                 if (tag == 'section') {
                     if (!isFirstSection)
                     if (!isFirstSection)
                         newParagraph(' ', 1);
                         newParagraph(' ', 1);
                     isFirstSection = false;
                     isFirstSection = false;
+                    sectionLevel++;
                 }
                 }
 
 
                 if (tag == 'emphasis' || tag == 'strong') {
                 if (tag == 'emphasis' || tag == 'strong') {
@@ -231,9 +266,13 @@ export default class BookParser {
                     isFirstTitlePara = true;
                     isFirstTitlePara = true;
                     bold = true;
                     bold = true;
                     center = true;
                     center = true;
+
+                    inSubtitle = true;
+                    curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
+                    curTitle.subtitles.push(curSubtitle);
                 }
                 }
 
 
-                if (tag == 'epigraph') {
+                if (tag == 'epigraph' || tag == 'annotation') {
                     italic = true;
                     italic = true;
                     space += 1;
                     space += 1;
                 }
                 }
@@ -260,6 +299,11 @@ export default class BookParser {
                         isFirstTitlePara = false;
                         isFirstTitlePara = false;
                         bold = false;
                         bold = false;
                         center = false;
                         center = false;
+                        inTitle = false;
+                    }
+
+                    if (tag == 'section') {
+                        sectionLevel--;
                     }
                     }
 
 
                     if (tag == 'emphasis' || tag == 'strong') {
                     if (tag == 'emphasis' || tag == 'strong') {
@@ -274,11 +318,14 @@ export default class BookParser {
                         isFirstTitlePara = false;
                         isFirstTitlePara = false;
                         bold = false;
                         bold = false;
                         center = false;
                         center = false;
+                        inSubtitle = false;
                     }
                     }
 
 
-                    if (tag == 'epigraph') {
+                    if (tag == 'epigraph' || tag == 'annotation') {
                         italic = false;
                         italic = false;
                         space -= 1;
                         space -= 1;
+                        if (tag == 'annotation')
+                            newParagraph(' ', 1);
                     }
                     }
 
 
                     if (tag == 'stanza') {
                     if (tag == 'stanza') {

+ 11 - 0
client/components/Reader/versionHistory.js

@@ -1,4 +1,15 @@
 export const versionHistory = [
 export const versionHistory = [
+{
+    showUntil: '2020-11-12',
+    header: '0.9.8 (2020-11-13)',
+    content:
+`
+<ul>
+    <li>добавлено окно "Оглавление/закладки"</li>
+</ul>
+`
+},
+
 {
 {
     showUntil: '2020-11-11',
     showUntil: '2020-11-11',
     header: '0.9.7 (2020-11-12)',
     header: '0.9.7 (2020-11-12)',

+ 4 - 1
client/quasar.js

@@ -32,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
 import {QDialog} from 'quasar/src/components/dialog';
 import {QDialog} from 'quasar/src/components/dialog';
 import {QChip} from 'quasar/src/components/chip';
 import {QChip} from 'quasar/src/components/chip';
 import {QTree} from 'quasar/src/components/tree';
 import {QTree} from 'quasar/src/components/tree';
+import {QExpansionItem} from 'quasar/src/components/expansion-item';
+
 
 
 const components = {
 const components = {
     //QLayout,
     //QLayout,
@@ -58,7 +60,8 @@ const components = {
     QPopupProxy,
     QPopupProxy,
     QDialog,
     QDialog,
     QChip,
     QChip,
-    QTree
+    QTree,
+    QExpansionItem,
 };
 };
 
 
 //directives 
 //directives 

+ 4 - 1
client/store/modules/reader.js

@@ -15,6 +15,7 @@ const readerActions = {
     'splitToPara': 'Обновить с разбиением на параграфы',
     'splitToPara': 'Обновить с разбиением на параграфы',
     'refresh': 'Принудительно обновить книгу',
     'refresh': 'Принудительно обновить книгу',
     'offlineMode': 'Автономный режим (без интернета)',
     'offlineMode': 'Автономный режим (без интернета)',
+    'contents': 'Оглавление/закладки',
     'libs': 'Библиотека',
     'libs': 'Библиотека',
     'recentBooks': 'Открыть недавние',
     'recentBooks': 'Открыть недавние',
     'switchToolbar': 'Показать/скрыть панель управления',
     'switchToolbar': 'Показать/скрыть панель управления',
@@ -42,6 +43,7 @@ const toolButtons = [
     {name: 'copyText',    show: false},
     {name: 'copyText',    show: false},
     {name: 'splitToPara', show: false},
     {name: 'splitToPara', show: false},
     {name: 'refresh',     show: true},
     {name: 'refresh',     show: true},
+    {name: 'contents',    show: true},
     {name: 'libs',        show: true},
     {name: 'libs',        show: true},
     {name: 'recentBooks', show: true},
     {name: 'recentBooks', show: true},
     {name: 'offlineMode', show: false},
     {name: 'offlineMode', show: false},
@@ -61,9 +63,10 @@ const hotKeys = [
     {name: 'copyText', codes: ['Ctrl+C']},
     {name: 'copyText', codes: ['Ctrl+C']},
     {name: 'splitToPara', codes: ['Shift+R']},
     {name: 'splitToPara', codes: ['Shift+R']},
     {name: 'refresh', codes: ['R']},
     {name: 'refresh', codes: ['R']},
-    {name: 'offlineMode', codes: ['O']},
+    {name: 'contents', codes: ['C']},
     {name: 'libs', codes: ['L']},
     {name: 'libs', codes: ['L']},
     {name: 'recentBooks', codes: ['X']},
     {name: 'recentBooks', codes: ['X']},
+    {name: 'offlineMode', codes: ['O']},
 
 
     {name: 'switchToolbar', codes: ['Tab', 'Q']},
     {name: 'switchToolbar', codes: ['Tab', 'Q']},
     {name: 'bookBegin', codes: ['Home']},
     {name: 'bookBegin', codes: ['Home']},

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Liberama",
   "name": "Liberama",
-  "version": "0.9.7",
+  "version": "0.9.8",
   "lockfileVersion": 1,
   "lockfileVersion": 1,
   "requires": true,
   "requires": true,
   "dependencies": {
   "dependencies": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Liberama",
   "name": "Liberama",
-  "version": "0.9.7",
+  "version": "0.9.8",
   "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",