浏览代码

Merge branch 'release/0.9.2'

Book Pauk 5 年之前
父节点
当前提交
34c7a33576

+ 8 - 2
build/webpack.prod.config.js

@@ -9,7 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
-const AppCachePlugin = require('appcache-webpack-plugin');
+const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
 
 
 const publicDir = path.resolve(__dirname, '../dist/tmp/public');
 const publicDir = path.resolve(__dirname, '../dist/tmp/public');
 const clientDir = path.resolve(__dirname, '../client');
 const clientDir = path.resolve(__dirname, '../client');
@@ -55,6 +55,12 @@ module.exports = merge(baseWpConfig, {
             filename: `${publicDir}/index.html`
             filename: `${publicDir}/index.html`
         }),
         }),
         new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
         new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
-        new AppCachePlugin({exclude: ['../index.html']})
+        new SWPrecacheWebpackPlugin({
+            cacheId: 'liberama',
+            filepath: `${publicDir}/service-worker.js`,
+            minify: true,
+            navigateFallback: '/index.html',
+            stripPrefix: publicDir,
+        }),        
     ]
     ]
 });
 });

+ 5 - 0
client/assets/sw-register.js

@@ -0,0 +1,5 @@
+(function() {
+    if('serviceWorker' in navigator) {
+        navigator.serviceWorker.register('/service-worker.js');
+    }
+})();

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

@@ -91,7 +91,7 @@ class CopyTextPage extends Vue {
 
 
     close() {
     close() {
         this.stopInit = true;
         this.stopInit = true;
-        this.$emit('copy-text-toggle');
+        this.$emit('do-action', {action: 'copyText'});
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {

+ 2 - 2
client/components/Reader/HelpPage/HelpPage.vue

@@ -43,8 +43,8 @@ const pages = {
 
 
 const tabs = [
 const tabs = [
     ['CommonHelpPage', 'Общее'],
     ['CommonHelpPage', 'Общее'],
-    ['HotkeysHelpPage', 'Клавиатура'],
     ['MouseHelpPage', 'Мышь/тачскрин'],
     ['MouseHelpPage', 'Мышь/тачскрин'],
+    ['HotkeysHelpPage', 'Клавиатура'],
     ['VersionHistoryPage', 'История версий'],
     ['VersionHistoryPage', 'История версий'],
     ['DonateHelpPage', 'Помочь проекту'],
     ['DonateHelpPage', 'Помочь проекту'],
 ];
 ];
@@ -56,7 +56,7 @@ class HelpPage extends Vue {
     selectedTab = 'CommonHelpPage';
     selectedTab = 'CommonHelpPage';
 
 
     close() {
     close() {
-        this.$emit('help-toggle');
+        this.$emit('do-action', {action: 'help'});
     }
     }
 
 
     get activePage() {
     get activePage() {

+ 22 - 25
client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue

@@ -1,28 +1,13 @@
 <template>
 <template>
     <div class="page">
     <div class="page">
-        <span class="text-h6 text-bold">Управление с помощью горячих клавиш:</span>
-        <ul>
-            <li><b>F1, H</b> - открыть справку</li>
-            <li><b>Escape</b> - показать/скрыть страницу загрузки</li>
-            <li><b>Tab, Q</b> - показать/скрыть панель управления</li>
-            <li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
-            <li><b>PageDown, Right, Space</b> - страницу вперед</li>
-            <li><b>Home</b> - в начало книги</li>
-            <li><b>End</b> - в конец книги</li>
-            <li><b>Up</b> - строчку назад</li>
-            <li><b>Down</b> - строчку вперёд</li>
-            <li><b>A, Shift+A</b> - изменить размер шрифта</li>
-            <li><b>Enter, F, F11, ` (апостроф)</b> - вкл./выкл. полный экран</li>
-            <li><b>Z</b> - вкл./выкл. плавный скроллинг текста</li>
-            <li><b>Shift+Down/Shift+Up</b> - увеличить/уменьшить скорость скроллинга
-            <li><b>P</b> - установить страницу</li>
-            <li><b>Ctrl+F</b> - найти в тексте</li>            
-            <li><b>Ctrl+C</b> - скопировать текст со страницы</li>            
-            <li><b>R</b> - принудительно обновить книгу в обход кэша</li>
-            <li><b>X</b> - открыть недавние</li>
-            <li><b>O</b> - автономный режим</li>
-            <li><b>S</b> - открыть окно настроек</li>
-        </ul>
+        <div style="font-size: 120%">
+            <div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
+            <br>
+        </div>
+        <div class="q-mb-md" style="width: 550px">
+            <div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div>
+            <UserHotKeys v-model="userHotKeys" readonly/>
+        </div>
     </div>
     </div>
 </template>
 </template>
 
 
@@ -31,11 +16,25 @@
 import Vue from 'vue';
 import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
 
 
+import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
+
 export default @Component({
 export default @Component({
+    components: {
+        UserHotKeys,
+    },
 })
 })
 class HotkeysHelpPage extends Vue {
 class HotkeysHelpPage extends Vue {
     created() {
     created() {
     }
     }
+
+    get userHotKeys() {
+        return this.$store.state.reader.settings.userHotKeys;
+    }
+
+    set userHotKeys(value) {
+        //no setter
+    }
+
 }
 }
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 </script>
 </script>
@@ -44,7 +43,5 @@ class HotkeysHelpPage extends Vue {
 .page {
 .page {
     padding: 15px;
     padding: 15px;
     overflow-y: auto;
     overflow-y: auto;
-    font-size: 120%;
-    line-height: 130%;
 }
 }
 </style>
 </style>

+ 12 - 14
client/components/Reader/LoaderPage/LoaderPage.vue

@@ -148,12 +148,12 @@ class LoaderPage extends Vue {
         this.pasteTextActive = !this.pasteTextActive;
         this.pasteTextActive = !this.pasteTextActive;
     }
     }
 
 
-    openHelp() {
-        this.$emit('help-toggle');
+    openHelp(event) {
+        this.$emit('do-action', {action: 'help', event});
     }
     }
 
 
     openDonate() {
     openDonate() {
-        this.$emit('donate-toggle');
+        this.$emit('do-action', {action: 'donate'});
     }
     }
     
     
     openComments() {
     openComments() {
@@ -173,21 +173,19 @@ class LoaderPage extends Vue {
         const input = this.$refs.input.$refs.input;
         const input = this.$refs.input.$refs.input;
         if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
         if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
             this.submitUrl();
             this.submitUrl();
-        }
-
-        if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
-            this.$emit('help-toggle');
-            event.preventDefault();
-            event.stopPropagation();
             return true;
             return true;
         }
         }
 
 
-        if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
-            this.$emit('tool-bar-toggle');
-            event.preventDefault();
-            event.stopPropagation();
-            return true;
+        if (event.type == 'keydown' && document.activeElement !== input) {
+            const action = this.$root.readerActionByKeyEvent(event);
+            switch (action) {
+                case 'help':
+                    this.openHelp(event);
+                    return true;
+            }
         }
         }
+
+        return false;
     }
     }
 }
 }
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------

+ 168 - 123
client/components/Reader/Reader.vue

@@ -4,57 +4,57 @@
             <div ref="buttons" class="row justify-between no-wrap">
             <div ref="buttons" class="row justify-between no-wrap">
                 <button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
                 <button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
                     <q-icon name="la la-arrow-left" size="32px"/>
                     <q-icon name="la la-arrow-left" size="32px"/>
-                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">Загрузить книгу</q-tooltip>
+                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
                 </button>
                 </button>
 
 
                 <div>
                 <div>
                     <button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
                     <button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
                         <q-icon name="la la-angle-left" size="32px"/>
                         <q-icon name="la la-angle-left" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Действие назад</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['undoAction'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
                     <button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
                         <q-icon name="la la-angle-right" size="32px"/>
                         <q-icon name="la la-angle-right" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Действие вперед</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
                     </button>
                     </button>
                     <div class="space"></div>
                     <div class="space"></div>
                     <button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
                     <button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
                         <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
                         <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['fullScreen'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
                     <button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
                         <q-icon name="la la-film" size="32px"/>
                         <q-icon name="la la-film" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Плавный скроллинг</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['scrolling'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
                     <button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
                         <q-icon name="la la-angle-double-right" size="32px"/>
                         <q-icon name="la la-angle-double-right" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На страницу</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['setPosition'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
                     <button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
                         <q-icon name="la la-search" size="32px"/>
                         <q-icon name="la la-search" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Найти в тексте</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['search'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
                     <button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
                         <q-icon name="la la-copy" size="32px"/>
                         <q-icon name="la la-copy" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Скопировать текст со страницы</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
                     <button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
                         <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
                         <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Принудительно обновить книгу в обход кэша</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="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
                     <button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
                         <q-icon name="la la-unlink" size="32px"/>
                         <q-icon name="la la-unlink" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Автономный режим (без интернета)</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
                     </button>
                     </button>
                     <button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
                     <button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
                         <q-icon name="la la-book-open" size="32px"/>
                         <q-icon name="la la-book-open" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть недавние</q-tooltip>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
                     </button>
                     </button>
                 </div>
                 </div>
 
 
                 <button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
                 <button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
                     <q-icon name="la la-cog" size="32px"/>
                     <q-icon name="la la-cog" size="32px"/>
-                    <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">Настроить</q-tooltip>
+                    <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
                 </button>
                 </button>
             </div>
             </div>
         </div>
         </div>
@@ -65,26 +65,21 @@
                     @load-book="loadBook"
                     @load-book="loadBook"
                     @load-file="loadFile"
                     @load-file="loadFile"
                     @book-pos-changed="bookPosChanged"
                     @book-pos-changed="bookPosChanged"
-                    @tool-bar-toggle="toolBarToggle"
-                    @full-screen-toogle="fullScreenToggle"
-                    @stop-scrolling="stopScrolling"
-                    @scrolling-toggle="scrollingToggle"
-                    @help-toggle="helpToggle"
-                    @donate-toggle="donateToggle"
+                    @do-action="doAction"
                 ></component>
                 ></component>
             </keep-alive>
             </keep-alive>
 
 
             <SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
             <SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
             <SearchPage v-show="searchActive" ref="searchPage" 
             <SearchPage v-show="searchActive" ref="searchPage" 
-                @search-toggle="searchToggle" 
+                @do-action="doAction"
                 @book-pos-changed="bookPosChanged"
                 @book-pos-changed="bookPosChanged"
                 @start-text-search="startTextSearch"
                 @start-text-search="startTextSearch"
                 @stop-text-search="stopTextSearch">
                 @stop-text-search="stopTextSearch">
             </SearchPage>
             </SearchPage>
-            <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
+            <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
             <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" @settings-toggle="settingsToggle"></SettingsPage>
-            <HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
+            <SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
+            <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>
 
 
@@ -169,12 +164,13 @@ 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 Dialog from '../share/Dialog.vue';
 
 
 import bookManager from './share/bookManager';
 import bookManager from './share/bookManager';
+import rstore from '../../store/modules/reader';
 import readerApi from '../../api/reader';
 import readerApi from '../../api/reader';
 import * as utils from '../../share/utils';
 import * as utils from '../../share/utils';
 import {versionHistory} from './versionHistory';
 import {versionHistory} from './versionHistory';
-import Dialog from '../share/Dialog.vue';
 
 
 export default @Component({
 export default @Component({
     components: {
     components: {
@@ -232,6 +228,7 @@ export default @Component({
     },
     },
 })
 })
 class Reader extends Vue {
 class Reader extends Vue {
+    rstore = {};
     loaderActive = false;
     loaderActive = false;
     progressActive = false;
     progressActive = false;
     fullScreenActive = false;
     fullScreenActive = false;
@@ -261,6 +258,7 @@ class Reader extends Vue {
     donationVisible = false;
     donationVisible = false;
 
 
     created() {
     created() {
+        this.rstore = rstore;
         this.loading = true;
         this.loading = true;
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
         this.dispatch = this.$store.dispatch;
         this.dispatch = this.$store.dispatch;
@@ -336,6 +334,11 @@ class Reader extends Vue {
         this.showToolButton = settings.showToolButton;
         this.showToolButton = settings.showToolButton;
         this.enableSitesFilter = settings.enableSitesFilter;
         this.enableSitesFilter = settings.enableSitesFilter;
 
 
+        this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
+        this.$root.readerActionByKeyEvent = (event) => {
+            return this.readerActionByKeyCode[utils.keyEventToCode(event)];
+        }
+
         this.updateHeaderMinWidth();
         this.updateHeaderMinWidth();
     }
     }
 
 
@@ -750,66 +753,37 @@ class Reader extends Vue {
         }
         }
     }
     }
 
 
-    buttonClick(button) {
-        const activeClass = this.buttonActiveClass(button);
+    undoAction() {
+        if (this.actionCur > 0) {
+            this.actionCur--;
+            this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
+        }
+    }
 
 
-        this.$refs[button].blur();
+    redoAction() {
+        if (this.actionCur < this.actionList.length - 1) {
+            this.actionCur++;
+            this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
+        }
+    }
+
+    buttonClick(action) {
+        const activeClass = this.buttonActiveClass(action);
+
+        this.$refs[action].blur();
 
 
         if (activeClass['tool-button-disabled'])
         if (activeClass['tool-button-disabled'])
             return;
             return;
         
         
-        switch (button) {
-            case 'loader':
-                this.loaderToggle();
-                break;
-            case 'undoAction':
-                if (this.actionCur > 0) {
-                    this.actionCur--;
-                    this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
-                }
-                break;
-            case 'redoAction':
-                if (this.actionCur < this.actionList.length - 1) {
-                    this.actionCur++;
-                    this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
-                }
-                break;
-            case 'fullScreen':
-                this.fullScreenToggle();
-                break;
-            case 'setPosition':
-                this.setPositionToggle();
-                break;
-            case 'scrolling':
-                this.scrollingToggle();
-                break;
-            case 'search':
-                this.searchToggle();
-                break;
-            case 'copyText':
-                this.copyTextToggle();
-                break;
-            case 'refresh':
-                this.refreshBook();
-                break;
-            case 'recentBooks':
-                this.recentBooksToggle();
-                break;
-            case 'offlineMode':
-                this.offlineModeToggle();
-                break;
-            case 'settings':
-                this.settingsToggle();
-                break;
-        }
+        this.doAction({action});
     }
     }
 
 
-    buttonActiveClass(button) {
+    buttonActiveClass(action) {
         const classActive = { 'tool-button-active': true, 'tool-button-active:hover': true };
         const classActive = { 'tool-button-active': true, 'tool-button-active:hover': true };
         const classDisabled = { 'tool-button-disabled': true, 'tool-button-disabled:hover': true };
         const classDisabled = { 'tool-button-disabled': true, 'tool-button-disabled:hover': true };
         let classResult = {};
         let classResult = {};
 
 
-        switch (button) {
+        switch (action) {
             case 'loader':
             case 'loader':
             case 'fullScreen':
             case 'fullScreen':
             case 'setPosition':
             case 'setPosition':
@@ -822,7 +796,7 @@ class Reader extends Vue {
             case 'settings':
             case 'settings':
                 if (this.progressActive) {
                 if (this.progressActive) {
                     classResult = classDisabled;
                     classResult = classDisabled;
-                } else if (this[`${button}Active`]) {
+                } else if (this[`${action}Active`]) {
                     classResult = classActive;
                     classResult = classActive;
                 }
                 }
                 break;
                 break;
@@ -837,7 +811,7 @@ class Reader extends Vue {
         }
         }
 
 
         if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
         if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
-            switch (button) {
+            switch (action) {
                 case 'undoAction':
                 case 'undoAction':
                 case 'redoAction':
                 case 'redoAction':
                 case 'setPosition':
                 case 'setPosition':
@@ -1030,7 +1004,7 @@ class Reader extends Vue {
         } catch (e) {
         } catch (e) {
             progress.hide(); this.progressActive = false;
             progress.hide(); this.progressActive = false;
             this.loaderActive = true;
             this.loaderActive = true;
-            this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
+            this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
         }
         }
     }
     }
 
 
@@ -1054,7 +1028,7 @@ class Reader extends Vue {
         } catch (e) {
         } catch (e) {
             progress.hide(); this.progressActive = false;
             progress.hide(); this.progressActive = false;
             this.loaderActive = true;
             this.loaderActive = true;
-            this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
+            this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
         }
         }
     }
     }
 
 
@@ -1086,10 +1060,118 @@ class Reader extends Vue {
         }
         }
     }
     }
 
 
+    doAction(opts) {
+        let result = true;
+        let {action = '', event = false} = opts;
+
+        switch (action) {
+            case 'loader':
+                this.loaderToggle();
+                break;
+            case 'help':
+                this.helpToggle();
+                break;
+            case 'settings':
+                this.settingsToggle();
+                break;
+            case 'undoAction':
+                this.undoAction();
+                break;
+            case 'redoAction':
+                this.redoAction();
+                break;
+            case 'fullScreen':
+                this.fullScreenToggle();
+                break;
+            case 'scrolling':
+                this.scrollingToggle();
+                break;
+            case 'stopScrolling':
+                this.stopScrolling();
+                break;
+            case 'setPosition':
+                this.setPositionToggle();
+                break;
+            case 'search':
+                this.searchToggle();
+                break;
+            case 'copyText':
+                this.copyTextToggle();
+                break;
+            case 'refresh':
+                this.refreshBook();
+                break;
+            case 'offlineMode':
+                this.offlineModeToggle();
+                break;
+            case 'recentBooks':
+                this.recentBooksToggle();
+                break;
+            case 'switchToolbar':
+                this.toolBarToggle();
+                break;
+            case 'donate':
+                this.donateToggle();
+                break;
+            default:
+                result = false;
+                break;
+        }
+
+        if (!result && this.activePage == 'TextPage' && this.$refs.page) {
+            result = true;
+            const textPage = this.$refs.page;
+
+            switch (action) {
+                case 'bookBegin':
+                    textPage.doHome();
+                    break;
+                case 'bookEnd':
+                    textPage.doEnd();
+                    break;
+                case 'pageBack':
+                    textPage.doPageUp();
+                    break;
+                case 'pageForward':
+                    textPage.doPageDown();
+                    break;
+                case 'lineBack':
+                    textPage.doUp();
+                    break;
+                case 'lineForward':
+                    textPage.doDown();
+                    break;
+                case 'incFontSize':
+                    textPage.doFontSizeInc();
+                    break;
+                case 'decFontSize':
+                    textPage.doFontSizeDec();
+                    break;
+                case 'scrollingSpeedUp':
+                    textPage.doScrollingSpeedUp();
+                    break;
+                case 'scrollingSpeedDown':
+                    textPage.doScrollingSpeedDown();
+                    break;
+                default:
+                    result = false;
+                    break;
+            }
+        }
+
+        if (result && event) {
+            event.preventDefault();
+            event.stopPropagation();
+        }
+
+        return result;
+    }
+
     keyHook(event) {
     keyHook(event) {
+        let result = false;
         if (this.$root.rootRoute() == '/reader') {
         if (this.$root.rootRoute() == '/reader') {
             if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
             if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
-                return;
+                return result;
 
 
             let handled = false;
             let handled = false;
             if (!handled && this.helpActive)
             if (!handled && this.helpActive)
@@ -1114,55 +1196,18 @@ class Reader extends Vue {
                 handled = this.$refs.page.keyHook(event);
                 handled = this.$refs.page.keyHook(event);
 
 
             if (!handled && event.type == 'keydown') {
             if (!handled && event.type == 'keydown') {
-                if (event.code == 'Escape')
-                    this.loaderToggle();
-
-                if (this.activePage == 'TextPage') {
-                    switch (event.code) {
-                        case 'KeyH':
-                        case 'F1':
-                            this.helpToggle();
-                            event.preventDefault();
-                            event.stopPropagation();
-                            break;
-                        case 'KeyZ':
-                            this.scrollingToggle();
-                            break;
-                        case 'KeyP':
-                            this.setPositionToggle();
-                            break;
-                        case 'KeyF':
-                            if (event.ctrlKey) {
-                                this.searchToggle();
-                                event.preventDefault();
-                                event.stopPropagation();
-                            }
-                            break;
-                        case 'KeyC':
-                            if (event.ctrlKey) {
-                                this.copyTextToggle();
-                                event.preventDefault();
-                                event.stopPropagation();
-                            }
-                            break;
-                        case 'KeyR':
-                            this.refreshBook();
-                            break;
-                        case 'KeyX':
-                            this.recentBooksToggle();
-                            event.preventDefault();
-                            event.stopPropagation();
-                            break;
-                        case 'KeyO':
-                            this.offlineModeToggle();
-                            break;
-                        case 'KeyS':
-                            this.settingsToggle();
-                            break;
-                    }
+                const action = this.$root.readerActionByKeyEvent(event);
+
+                if (action == 'loader') {
+                    result = this.doAction({action, event});
+                }
+
+                if (!result && this.activePage == 'TextPage') {
+                    result = this.doAction({action, event});
                 }
                 }
             }
             }
         }
         }
+        return result;
     }
     }
 }
 }
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------

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

@@ -303,7 +303,7 @@ class RecentBooksPage extends Vue {
             let errMes = e.message;
             let errMes = e.message;
             if (errMes.indexOf('404') >= 0)
             if (errMes.indexOf('404') >= 0)
                 errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
                 errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
-            this.$root.stdDialog.alert(errMes, 'Ошибка', {type: 'negative'});
+            this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
         }
         }
     }
     }
 
 

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

@@ -164,7 +164,7 @@ class SearchPage extends Vue {
 
 
     close() {
     close() {
         this.stopInit = true;
         this.stopInit = true;
-        this.$emit('search-toggle');
+        this.$emit('do-action', {action: 'search'});
     }
     }
 
 
     inputKeyDown(event) {
     inputKeyDown(event) {

+ 5 - 2
client/components/Reader/SetPositionPage/SetPositionPage.vue

@@ -58,8 +58,11 @@ class SetPositionPage extends Vue {
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {
-        if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
-            this.close();
+        if (event.type == 'keydown') {
+            const action = this.$root.readerActionByKeyEvent(event);
+            if (event.code == 'Escape' || action == 'setPosition') {
+                this.close();
+            }
         }
         }
         return true;
         return true;
     }
     }

+ 8 - 2
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -46,7 +46,7 @@
                     @@include('./include/ButtonsTab.inc');
                     @@include('./include/ButtonsTab.inc');
                 </div>
                 </div>
                 <!-- Управление ------------------------------------------------------------------>
                 <!-- Управление ------------------------------------------------------------------>
-                <div v-if="selectedTab == 'keys'" class="fit tab-panel">
+                <div v-if="selectedTab == 'keys'" class="fit column">
                     @@include('./include/KeysTab.inc');
                     @@include('./include/KeysTab.inc');
                 </div>
                 </div>
                 <!-- Листание -------------------------------------------------------------------->
                 <!-- Листание -------------------------------------------------------------------->
@@ -76,6 +76,8 @@ import _ from 'lodash';
 import * as utils from '../../../share/utils';
 import * as utils from '../../../share/utils';
 import Window from '../../share/Window.vue';
 import Window from '../../share/Window.vue';
 import NumInput from '../../share/NumInput.vue';
 import NumInput from '../../share/NumInput.vue';
+import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
+
 import rstore from '../../../store/modules/reader';
 import rstore from '../../../store/modules/reader';
 import defPalette from './defPalette';
 import defPalette from './defPalette';
 
 
@@ -85,6 +87,7 @@ export default @Component({
     components: {
     components: {
         Window,
         Window,
         NumInput,
         NumInput,
+        UserHotKeys,
     },
     },
     data: function() {
     data: function() {
         return Object.assign({}, rstore.settingDefaults);
         return Object.assign({}, rstore.settingDefaults);
@@ -139,6 +142,7 @@ export default @Component({
 class SettingsPage extends Vue {
 class SettingsPage extends Vue {
     selectedTab = 'profiles';
     selectedTab = 'profiles';
     selectedViewTab = 'color';
     selectedViewTab = 'color';
+    selectedKeysTab = 'mouse';
     form = {};
     form = {};
     fontBold = false;
     fontBold = false;
     fontItalic = false;
     fontItalic = false;
@@ -152,12 +156,14 @@ class SettingsPage extends Vue {
 
 
     serverStorageKeyVisible = false;
     serverStorageKeyVisible = false;
     toolButtons = [];
     toolButtons = [];
+    rstore = {};
 
 
     created() {
     created() {
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
         this.reader = this.$store.state.reader;
         this.reader = this.$store.state.reader;
 
 
         this.form = {};
         this.form = {};
+        this.rstore = rstore;
         this.toolButtons = rstore.toolButtons;
         this.toolButtons = rstore.toolButtons;
         this.settingsChanged();
         this.settingsChanged();
     }
     }
@@ -339,7 +345,7 @@ class SettingsPage extends Vue {
     }
     }
 
 
     close() {
     close() {
-        this.$emit('settings-toggle');
+        this.$emit('do-action', {action: 'settings'});
     }
     }
 
 
     async setDefaults() {
     async setDefaults() {

+ 248 - 0
client/components/Reader/SettingsPage/UserHotKeys/UserHotKeys.vue

@@ -0,0 +1,248 @@
+<template>
+    <div class="table col column no-wrap">
+        <!-- header -->
+        <div class="table-row row">
+            <div class="desc q-pa-sm bg-blue-2">Команда</div>
+            <div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
+                <div style="width: 80px">Сочетание клавиш</div>
+                <q-input ref="input" class="q-ml-sm col"
+                    outlined dense rounded
+                    bg-color="grey-4"
+                    placeholder="Найти"
+                    v-model="search"
+                    @click.stop
+                />
+                <div v-show="!readonly" class="q-ml-sm column justify-center">
+                    <q-btn class="bg-grey-4 text-grey-6" style="height: 35px; width: 35px" rounded flat icon="la la-broom" @click="defaultHotKeyAll">
+                        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                            Установить все сочетания по умолчанию
+                        </q-tooltip>
+                    </q-btn>
+                </div>
+            </div>
+        </div>
+
+        <!-- body -->
+        <div class="table-row row" v-for="(action, index) in tableData" :key="index">
+            <div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div>
+            <div class="hotKeys col q-pa-sm">
+                <q-chip
+                    :color="collisions[code] ? 'red' : 'grey-7'"
+                    :removable="!readonly" :clickable="collisions[code] ? true : false"
+                    text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)"
+                    @click="collisionWarning(code)"
+                    >
+                    {{ code }}
+                </q-chip>
+            </div>
+            <div v-show="!readonly" class="column q-pa-xs">
+                <q-icon
+                    name="la la-plus-circle"
+                    class="button bg-green-8 text-white"
+                    @click="addHotKey(action)"
+                    v-ripple
+                    :disabled="value[action].length >= maxCodesLength"
+                >
+                    <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                        Добавить сочетание клавиш
+                    </q-tooltip>
+                </q-icon>
+                <q-icon
+                    name="la la-broom"
+                    class="button text-grey-5"
+                    @click="defaultHotKey(action)"
+                    v-ripple
+                >
+                    <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                        По умолчанию
+                    </q-tooltip>
+                </q-icon>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+import rstore from '../../../../store/modules/reader';
+//import * as utils from '../../share/utils';
+
+const UserHotKeysProps = Vue.extend({
+    props: {
+        value: Object,
+        readonly: Boolean,
+    }
+});
+
+export default @Component({
+    watch: {
+        search: function() {
+            this.updateTableData();
+        },
+        value: function() {
+            this.checkCollisions();
+            this.updateTableData();
+        }
+    },
+})
+class UserHotKeys extends UserHotKeysProps {
+    search = '';
+    rstore = {};
+    tableData = [];
+    collisions = {};
+    maxCodesLength = 10;
+
+    created() {
+        this.rstore = rstore;
+    }
+
+    mounted() {
+        this.checkCollisions();
+        this.updateTableData();
+    }
+
+    updateTableData() {
+        let result = rstore.hotKeys.map(hk => hk.name);
+
+        const search = this.search.toLowerCase();
+        const codesIncludeSearch = (action) => {
+            for (const code of this.value[action]) {
+                if (code.toLowerCase().includes(search))
+                    return true;
+            }
+            return false;
+        };
+
+        result = result.filter(item => {
+            return !search ||
+                rstore.readerActions[item].toLowerCase().includes(search) ||
+                codesIncludeSearch(item)
+        });
+
+        this.tableData = result;
+    }
+
+    checkCollisions() {
+        const cols = {};
+        for (const [action, codes] of Object.entries(this.value)) {
+            codes.forEach(code => {
+                if (!cols[code])
+                    cols[code] = [];
+                if (cols[code].indexOf(action) < 0)
+                    cols[code].push(action);
+            });
+        }
+
+        const result = {};
+        for (const [code, actions] of Object.entries(cols)) {
+            if (actions.length > 1)
+                result[code] = actions;
+        }
+
+        this.collisions = result;
+    }
+
+    collisionWarning(code) {
+        if (this.collisions[code]) {
+            const descs = this.collisions[code].map(action => `<b>${rstore.readerActions[action]}</b>`);
+            this.$root.stdDialog.alert(`Сочетание '${code}' одновременно назначено<br>следующим командам:<br>${descs.join('<br>')}<br><br>
+Возможно неожиданное поведение.`, 'Предупреждение');
+        }
+    }
+
+    removeCode(action, code) {
+        let codes = Array.from(this.value[action]);
+        const index = codes.indexOf(code);
+        if (index >= 0) {
+            codes.splice(index, 1);
+            const newValue = Object.assign({}, this.value, {[action]: codes});
+            this.$emit('input', newValue);
+        }
+    }
+
+    async addHotKey(action) {
+        if (this.value[action].length >= this.maxCodesLength)
+            return;
+        try {
+            const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
+            if (result) {
+                let codes = Array.from(this.value[action]);
+                if (codes.indexOf(result) < 0) {
+                    codes.push(result);
+                    const newValue = Object.assign({}, this.value, {[action]: codes});
+                    this.$emit('input', newValue);
+                    this.$nextTick(() => {
+                        this.collisionWarning(result);
+                    });
+                }
+            }
+        } catch (e) {
+            //
+        }
+    }
+
+    async defaultHotKey(action) {
+        try {
+            if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
+                const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
+                const newValue = Object.assign({}, this.value, {[action]: codes});
+                this.$emit('input', newValue);
+            }
+        } catch (e) {
+            //
+        }
+    }
+
+    async defaultHotKeyAll() {
+        try {
+            if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
+                const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
+                this.$emit('input', newValue);
+            }
+        } catch (e) {
+            //
+        }
+    }
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.table {
+    border-left: 1px solid grey;
+    border-top: 1px solid grey;
+}
+
+.table-row {
+    border-right: 1px solid grey;
+    border-bottom: 1px solid grey;
+}
+
+.table-row:nth-child(even) {
+    background-color: #f7f7f7;
+}
+
+.table-row:hover {
+    background-color: #f0f0f0;
+}
+
+.desc {
+    width: 130px;
+    overflow-wrap: break-word;
+    word-wrap: break-word;
+    white-space: normal;
+}
+
+.hotKeys {
+    border-left: 1px solid grey;    
+}
+
+.button {
+    font-size: 25px;
+    border-radius: 25px;
+    cursor: pointer;
+}
+</style>

+ 1 - 1
client/components/Reader/SettingsPage/include/ButtonsTab.inc

@@ -3,6 +3,6 @@
 <div class="item row" v-for="item in toolButtons" :key="item.name">
 <div class="item row" v-for="item in toolButtons" :key="item.name">
     <div class="label-3"></div>
     <div class="label-3"></div>
     <div class="col row">
     <div class="col row">
-        <q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="item.text" />
+        <q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="rstore.readerActions[item.name]" />
     </div>
     </div>
 </div>
 </div>

+ 30 - 5
client/components/Reader/SettingsPage/include/KeysTab.inc

@@ -1,8 +1,33 @@
-<div class="part-header">Управление</div>
+<div class="bg-grey-3 row">
+    <q-tabs
+        v-model="selectedKeysTab"
+        active-color="black"
+        active-bg-color="white"
+        indicator-color="white"
+        dense
+        no-caps
+        class="no-mp bg-grey-4 text-grey-7"
+    >
+        <q-tab name="mouse" label="Мышь/тачскрин" />
+        <q-tab name="keyboard" label="Клавиатура" />
+    </q-tabs>
+</div>
+
+<div class="q-mb-sm"/>
+
+<div class="col tab-panel">
+    <div v-if="selectedKeysTab == 'mouse'">
+        <div class="item row">
+            <div class="label-4"></div>
+            <div class="col row">
+                <q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
+            </div>
+        </div>
+    </div>
 
 
-<div class="item row">
-    <div class="label-4"></div>
-    <div class="col row">
-        <q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
+    <div v-if="selectedKeysTab == 'keyboard'">
+        <div class="item row">
+            <UserHotKeys v-model="userHotKeys" />
+        </div>
     </div>
     </div>
 </div>
 </div>

+ 15 - 74
client/components/Reader/TextPage/TextPage.vue

@@ -423,7 +423,7 @@ class TextPage extends Vue {
                     if (this.lazyParseEnabled)
                     if (this.lazyParseEnabled)
                         this.lazyParsePara();
                         this.lazyParsePara();
                 } catch (e) {
                 } catch (e) {
-                    this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
+                    this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
                 }
                 }
             })();
             })();
         }
         }
@@ -504,7 +504,7 @@ class TextPage extends Vue {
     async startTextScrolling() {
     async startTextScrolling() {
         if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
         if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
             this.linesDown.length <= this.pageLineCount) {
             this.linesDown.length <= this.pageLineCount) {
-            this.$emit('stop-scrolling');
+            this.doStopScrolling();
             return;
             return;
         }
         }
 
 
@@ -545,7 +545,7 @@ class TextPage extends Vue {
         }
         }
         this.resolveTransition1Finish = null;
         this.resolveTransition1Finish = null;
         this.doingScrolling = false;
         this.doingScrolling = false;
-        this.$emit('stop-scrolling');
+        this.doStopScrolling();
         this.draw();
         this.draw();
     }
     }
 
 
@@ -884,22 +884,26 @@ class TextPage extends Vue {
         }
         }
     }
     }
 
 
-    doToolBarToggle() {
-        this.$emit('tool-bar-toggle');
+    doToolBarToggle(event) {
+        this.$emit('do-action', {action: 'switchToolbar', event});
     }
     }
 
 
     doScrollingToggle() {
     doScrollingToggle() {
-        this.$emit('scrolling-toggle');
+        this.$emit('do-action', {action: 'scrolling', event});
     }
     }
 
 
     doFullScreenToggle() {
     doFullScreenToggle() {
-        this.$emit('full-screen-toogle');
+        this.$emit('do-action', {action: 'fullScreen', event});
+    }
+
+    doStopScrolling() {
+        this.$emit('do-action', {action: 'stopScrolling', event});
     }
     }
 
 
     async doFontSizeInc() {
     async doFontSizeInc() {
         if (!this.settingsChanging) {
         if (!this.settingsChanging) {
             this.settingsChanging = true;
             this.settingsChanging = true;
-            const newSize = (this.settings.fontSize + 1 < 100 ? this.settings.fontSize + 1 : 100);
+            const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
             const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
             const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
             this.commit('reader/setSettings', newSettings);
             this.commit('reader/setSettings', newSettings);
             await sleep(50);
             await sleep(50);
@@ -940,69 +944,6 @@ class TextPage extends Vue {
         }
         }
     }
     }
 
 
-    keyHook(event) {
-        let result = false;
-        if (event.type == 'keydown' && !event.ctrlKey && !event.altKey) {
-            result = true;
-            switch (event.code) {
-                case 'ArrowDown':
-                    if (event.shiftKey)
-                        this.doScrollingSpeedUp();
-                    else
-                        this.doDown();
-                    break;
-                case 'ArrowUp':
-                    if (event.shiftKey)
-                        this.doScrollingSpeedDown();
-                    else
-                        this.doUp();
-                    break;
-                case 'PageDown':
-                case 'ArrowRight':
-                    this.doPageDown();
-                    break;
-                case 'Space':
-                    if (event.shiftKey)
-                        this.doPageUp();
-                    else
-                        this.doPageDown();
-                    break;
-                case 'PageUp':
-                case 'ArrowLeft':
-                case 'Backspace':
-                    this.doPageUp();
-                    break;
-                case 'Home':
-                    this.doHome();
-                    break;
-                case 'End':
-                    this.doEnd();
-                    break;
-                case 'KeyA':
-                    if (event.shiftKey)
-                        this.doFontSizeDec();
-                    else
-                        this.doFontSizeInc();
-                    break;
-                case 'Enter':
-                case 'Backquote'://`
-                case 'KeyF':
-                    this.doFullScreenToggle();
-                    break;
-                case 'Tab':
-                case 'KeyQ':
-                    this.doToolBarToggle();
-                    event.preventDefault();
-                    event.stopPropagation();
-                    break;
-                default:
-                    result = false;
-                    break;
-            }
-        }
-        return result;
-    }
-
     async startClickRepeat(pointX, pointY) {
     async startClickRepeat(pointX, pointY) {
         this.repX = pointX;
         this.repX = pointX;
         this.repY = pointY;
         this.repY = pointY;
@@ -1080,7 +1021,7 @@ class TextPage extends Vue {
                     //движение вправо
                     //движение вправо
                     this.doScrollingSpeedUp();
                     this.doScrollingSpeedUp();
                 } else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
                 } else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
-                    this.doToolBarToggle();
+                    this.doToolBarToggle(event);
                 }
                 }
 
 
                 this.startTouch = null;
                 this.startTouch = null;
@@ -1107,7 +1048,7 @@ class TextPage extends Vue {
         } else if (event.button == 1) {
         } else if (event.button == 1) {
             this.doScrollingToggle();
             this.doScrollingToggle();
         } else if (event.button == 2) {
         } else if (event.button == 2) {
-            this.doToolBarToggle();
+            this.doToolBarToggle(event);
         }
         }
     }
     }
 
 
@@ -1132,7 +1073,7 @@ class TextPage extends Vue {
         if (url && url.indexOf('file://') != 0) {
         if (url && url.indexOf('file://') != 0) {
             window.open(url, '_blank');
             window.open(url, '_blank');
         } else {
         } else {
-            this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {type: 'info'});
+            this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
         }
         }
     }
     }
 
 

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

@@ -1,4 +1,16 @@
 export const versionHistory = [
 export const versionHistory = [
+{
+    showUntil: '2020-04-25',
+    header: '0.9.2 (2020-03-15)',
+    content:
+`
+<ul>
+    <li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
+    <li>переход на Service Worker вместо AppCache для автономного режима работы</li>
+</ul>
+`
+},
+
 {
 {
     showUntil: '2020-03-02',
     showUntil: '2020-03-02',
     header: '0.9.1 (2020-03-03)',
     header: '0.9.1 (2020-03-03)',

+ 80 - 11
client/components/share/StdDialog.vue

@@ -3,7 +3,7 @@
         <slot></slot>
         <slot></slot>
 
 
         <!--------------------------------------------------->
         <!--------------------------------------------------->
-        <div v-show="type == 'alert'" class="dialog column bg-white no-wrap" style="min-height: 150px">
+        <div v-show="type == 'alert'" class="bg-white no-wrap">
             <div class="header row">
             <div class="header row">
                 <div class="caption col row items-center q-ml-md">
                 <div class="caption col row items-center q-ml-md">
                     <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
                     <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
@@ -16,7 +16,7 @@
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <div class="col q-mx-md">
+            <div class="q-mx-md">
                 <div v-html="message"></div>
                 <div v-html="message"></div>
             </div>
             </div>
 
 
@@ -26,7 +26,7 @@
         </div>
         </div>
 
 
         <!--------------------------------------------------->
         <!--------------------------------------------------->
-        <div v-show="type == 'confirm'" class="dialog column bg-white no-wrap" style="min-height: 150px">
+        <div v-show="type == 'confirm'" class="bg-white no-wrap">
             <div class="header row">
             <div class="header row">
                 <div class="caption col row items-center q-ml-md">
                 <div class="caption col row items-center q-ml-md">
                     <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
                     <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
@@ -39,7 +39,7 @@
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <div class="col q-mx-md">
+            <div class="q-mx-md">
                 <div v-html="message"></div>
                 <div v-html="message"></div>
             </div>
             </div>
 
 
@@ -50,7 +50,7 @@
         </div>
         </div>
 
 
         <!--------------------------------------------------->
         <!--------------------------------------------------->
-        <div v-show="type == 'prompt'" class="dialog column bg-white no-wrap" style="min-height: 250px">
+        <div v-show="type == 'prompt'" class="bg-white no-wrap">
             <div class="header row">
             <div class="header row">
                 <div class="caption col row items-center q-ml-md">
                 <div class="caption col row items-center q-ml-md">
                     <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
                     <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
@@ -63,7 +63,7 @@
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <div class="col q-mx-md">
+            <div class="q-mx-md">
                 <div v-html="message"></div>
                 <div v-html="message"></div>
                 <q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
                 <q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
                 <div class="error"><span v-show="error != ''">{{ error }}</span></div>
                 <div class="error"><span v-show="error != ''">{{ error }}</span></div>
@@ -74,6 +74,34 @@
                 <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
                 <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
             </div>
             </div>
         </div>
         </div>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'hotKey'" class="bg-white no-wrap">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn flat round dense v-close-popup>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="q-mx-md">
+                <div v-html="message"></div>
+                <div class="q-my-md text-center">
+                    <div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
+                    <div>{{ hotKeyCode }}</div>
+                </div>
+            </div>
+
+            <div class="buttons row justify-end q-pa-md">
+                <q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
+            </div>
+        </div>
     </q-dialog>
     </q-dialog>
 </template>
 </template>
 
 
@@ -82,7 +110,7 @@
 import Vue from 'vue';
 import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
 
 
-//import * as utils from '../../share/utils';
+import * as utils from '../../share/utils';
 
 
 export default @Component({
 export default @Component({
     watch: {
     watch: {
@@ -99,6 +127,7 @@ class StdDialog extends Vue {
     inputValue = '';
     inputValue = '';
     error = '';
     error = '';
     iconColor = '';
     iconColor = '';
+    hotKeyCode = '';
 
 
     created() {
     created() {
         if (this.$root.addKeyHook) {
         if (this.$root.addKeyHook) {
@@ -117,8 +146,13 @@ class StdDialog extends Vue {
         this.error = '';
         this.error = '';
 
 
         this.iconColor = 'text-warning';
         this.iconColor = 'text-warning';
-        if (opts && opts.type) {
-            this.iconColor = `text-${opts.type}`;
+        if (opts && opts.color) {
+            this.iconColor = `text-${opts.color}`;
+        }
+
+        this.hotKeyCode = '';
+        if (opts && opts.hotKeyCode) {
+            this.hotKeyCode = opts.hotKeyCode;
         }
         }
     }
     }
 
 
@@ -158,6 +192,12 @@ class StdDialog extends Vue {
             this.$refs.dialog.shake();
             this.$refs.dialog.shake();
             return;
             return;
         }
         }
+
+        if (this.type == 'hotKey' && this.hotKeyCode == '') {
+            this.$refs.dialog.shake();
+            return;
+        }
+
         this.ok = true;
         this.ok = true;
         this.$refs.dialog.hide();
         this.$refs.dialog.hide();
     }
     }
@@ -218,9 +258,38 @@ class StdDialog extends Vue {
         });
         });
     }
     }
 
 
+    getHotKey(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve(this.hotKeyCode);
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'hotKey';
+            this.active = true;
+        });
+    }
+
     keyHook(event) {
     keyHook(event) {
-        if (this.active && event.code == 'Enter') {
-            this.okClick();
+        if (this.active) {
+            if (this.type == 'hotKey') {
+                if (event.type == 'keydown') {
+                    this.hotKeyCode = utils.keyEventToCode(event);
+                }
+            } else {
+                if (event.code == 'Enter')
+                    this.okClick();
+                if (event.code == 'Escape') {
+                    this.$nextTick(() => {
+                        this.$refs.dialog.hide();
+                    });
+                }
+            }
             event.stopPropagation();
             event.stopPropagation();
             event.preventDefault();
             event.preventDefault();
         }
         }

+ 3 - 2
client/index.html.template

@@ -1,11 +1,12 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
-<html manifest="/app/manifest.appcache">
+<html>
   <head>
   <head>
+    <title></title>
     <meta charset="utf-8">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
     <meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
     <meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
     <meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
-    <title></title>
+    <script src="/sw-register.js"></script>
   </head>
   </head>
   <body>
   <body>
     <div id="app"></div>
     <div id="app"></div>

+ 2 - 0
client/quasar.js

@@ -30,6 +30,7 @@ import {QSelect} from 'quasar/src/components/select';
 import {QColor} from 'quasar/src/components/color';
 import {QColor} from 'quasar/src/components/color';
 import {QPopupProxy} from 'quasar/src/components/popup-proxy';
 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';
 
 
 const components = {
 const components = {
     //QLayout,
     //QLayout,
@@ -55,6 +56,7 @@ const components = {
     QColor,
     QColor,
     QPopupProxy,
     QPopupProxy,
     QDialog,
     QDialog,
+    QChip,
 };
 };
 
 
 //directives 
 //directives 

+ 35 - 1
client/share/utils.js

@@ -202,4 +202,38 @@ export function escapeXml(str) {
         .replace(/"/g, '&quot;')
         .replace(/"/g, '&quot;')
         .replace(/'/g, '&apos;')
         .replace(/'/g, '&apos;')
     ;
     ;
-}
+}
+
+export function keyEventToCode(event) {
+    let result = [];
+    let code = event.code;
+
+    const modCode = code.substring(0, 3);
+    if (event.metaKey && modCode != 'Met')
+        result.push('Meta');
+    if (event.ctrlKey && modCode != 'Con')
+        result.push('Ctrl');
+    if (event.shiftKey && modCode != 'Shi')
+        result.push('Shift');
+    if (event.altKey && modCode != 'Alt')
+        result.push('Alt');
+    
+    if (modCode == 'Dig') {
+        code = code.substring(5, 6);
+    } else if (modCode == 'Key') {
+        code = code.substring(3, 4);
+    }
+    result.push(code);
+
+    return result.join('+');
+}
+
+export function userHotKeysObjectSwap(userHotKeys) {
+    let result = {};
+    for (const [name, codes] of Object.entries(userHotKeys)) {
+        for (const code of codes) {
+            result[code] = name;
+        }
+    }
+    return result;
+}

+ 7 - 7
client/store/index.js

@@ -12,11 +12,11 @@ Vue.use(Vuex);
 const debug = process.env.NODE_ENV !== 'production';
 const debug = process.env.NODE_ENV !== 'production';
 
 
 export default new Vuex.Store(Object.assign({}, root, {
 export default new Vuex.Store(Object.assign({}, root, {
-  modules: {
-    uistate,
-    config,
-    reader,
-  },
-  strict: debug,
-  plugins: [createPersistedState()]
+    modules: {
+        uistate,
+        config,
+        reader,
+    },
+    strict: debug,
+    plugins: [createPersistedState()]
 }));
 }));

+ 75 - 11
client/store/modules/reader.js

@@ -1,15 +1,73 @@
-//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
+const readerActions = {
+    'help': 'Вызвать cправку',
+    'loader': 'На страницу загрузки',
+    'settings': 'Настроить',
+    'undoAction': 'Действие назад',
+    'redoAction': 'Действие вперед',
+    'fullScreen': 'На весь экран',
+    'scrolling': 'Плавный скроллинг',
+    'stopScrolling': '',
+    'setPosition': 'Установить позицию',
+    'search': 'Найти в тексте',
+    'copyText': 'Скопировать текст со страницы',
+    'refresh': 'Принудительно обновить книгу',
+    'offlineMode': 'Автономный режим (без интернета)',
+    'recentBooks': 'Открыть недавние',
+    'switchToolbar': 'Показать/скрыть панель управления',
+    'donate': '',
+    'bookBegin': 'В начало книги',
+    'bookEnd': 'В конец книги',
+    'pageBack': 'Страницу назад',
+    'pageForward': 'Страницу вперед',
+    'lineBack': 'Строчку назад',
+    'lineForward': 'Строчку вперед',
+    'incFontSize': 'Увеличить размер шрифта',
+    'decFontSize': 'Уменьшить размер шрифта',
+    'scrollingSpeedUp': 'Увеличить скорость скроллинга',
+    'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
+};
+
+//readerActions[name]
 const toolButtons = [
 const toolButtons = [
-    {name: 'undoAction',  show: true, text: 'Действие назад'},
-    {name: 'redoAction',  show: true, text: 'Действие вперед'},
-    {name: 'fullScreen',  show: true, text: 'На весь экран'},
-    {name: 'scrolling',   show: false, text: 'Плавный скроллинг'},
-    {name: 'setPosition', show: true, text: 'На страницу'},
-    {name: 'search',      show: true, text: 'Найти в тексте'},
-    {name: 'copyText',    show: false, text: 'Скопировать текст со страницы'},
-    {name: 'refresh',     show: true, text: 'Принудительно обновить книгу'},
-    {name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
-    {name: 'recentBooks', show: true, text: 'Открыть недавние'},
+    {name: 'undoAction',  show: true},
+    {name: 'redoAction',  show: true},
+    {name: 'fullScreen',  show: true},
+    {name: 'scrolling',   show: false},
+    {name: 'setPosition', show: true},
+    {name: 'search',      show: true},
+    {name: 'copyText',    show: false},
+    {name: 'refresh',     show: true},
+    {name: 'offlineMode', show: false},
+    {name: 'recentBooks', show: true},
+];
+
+//readerActions[name]
+const hotKeys = [
+    {name: 'help', codes: ['F1', 'H']},
+    {name: 'loader', codes: ['Escape']},
+    {name: 'settings', codes: ['S']},
+    {name: 'undoAction', codes: ['Ctrl+BracketLeft']},
+    {name: 'redoAction', codes: ['Ctrl+BracketRight']},
+    {name: 'fullScreen', codes: ['Enter', 'Backquote', 'F']},
+    {name: 'scrolling', codes: ['Z']},
+    {name: 'setPosition', codes: ['P']},
+    {name: 'search', codes: ['Ctrl+F']},
+    {name: 'copyText', codes: ['Ctrl+C']},
+    {name: 'refresh', codes: ['R']},
+    {name: 'offlineMode', codes: ['O']},
+    {name: 'recentBooks', codes: ['X']},
+
+    {name: 'switchToolbar', codes: ['Tab', 'Q']},
+    {name: 'bookBegin', codes: ['Home']},
+    {name: 'bookEnd', codes: ['End']},
+    {name: 'pageBack', codes: ['PageUp', 'ArrowLeft', 'Backspace', 'Shift+Space']},
+    {name: 'pageForward', codes: ['PageDown', 'ArrowRight', 'Space']},
+    {name: 'lineBack', codes: ['ArrowUp']},
+    {name: 'lineForward', codes: ['ArrowDown']},
+    {name: 'incFontSize', codes: ['A']},
+    {name: 'decFontSize', codes: ['Shift+A']},
+    {name: 'scrollingSpeedUp', codes: ['Shift+ArrowDown']},
+    {name: 'scrollingSpeedDown', codes: ['Shift+ArrowUp']},
 ];
 ];
 
 
 const fonts = [
 const fonts = [
@@ -136,6 +194,7 @@ const webFonts = [
 
 
 ];
 ];
 
 
+//----------------------------------------------------------------------------------------------------------
 const settingDefaults = {
 const settingDefaults = {
     textColor: '#000000',
     textColor: '#000000',
     backgroundColor: '#EBE2C9',
     backgroundColor: '#EBE2C9',
@@ -188,6 +247,7 @@ const settingDefaults = {
 
 
     fontShifts: {},
     fontShifts: {},
     showToolButton: {},
     showToolButton: {},
+    userHotKeys: {},
 };
 };
 
 
 for (const font of fonts)
 for (const font of fonts)
@@ -196,6 +256,8 @@ for (const font of webFonts)
     settingDefaults.fontShifts[font.name] = font.fontVertShift;
     settingDefaults.fontShifts[font.name] = font.fontVertShift;
 for (const button of toolButtons)
 for (const button of toolButtons)
     settingDefaults.showToolButton[button.name] = button.show;
     settingDefaults.showToolButton[button.name] = button.show;
+for (const hotKey of hotKeys)
+    settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
 
 
 // initial state
 // initial state
 const state = {
 const state = {
@@ -256,7 +318,9 @@ const mutations = {
 };
 };
 
 
 export default {
 export default {
+    readerActions,
     toolButtons,
     toolButtons,
+    hotKeys,
     fonts,
     fonts,
     webFonts,
     webFonts,
     settingDefaults,
     settingDefaults,

+ 48 - 0
docs/beta.omnireader/beta.omnireader

@@ -0,0 +1,48 @@
+server {
+  listen 443 ssl; # managed by Certbot
+  ssl_certificate /etc/letsencrypt/live/beta.omnireader.ru/fullchain.pem; # managed by Certbot
+  ssl_certificate_key /etc/letsencrypt/live/beta.omnireader.ru/privkey.pem; # managed by Certbot
+  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
+  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
+
+  server_name beta.omnireader.ru;
+
+  client_max_body_size 50m;
+  proxy_read_timeout 1h;
+
+  gzip on;
+  gzip_min_length 1024;
+  gzip_proxied expired no-cache no-store private auth;
+  gzip_types *;
+
+  location /api {
+    proxy_pass http://127.0.0.1:34081;
+  }
+
+  location /ws {
+    proxy_pass http://127.0.0.1:34081;
+    proxy_http_version 1.1;
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+  }
+
+  location / {
+    root /home/beta.liberama/public;
+
+    location /tmp {
+      add_header Content-Type text/xml;
+      add_header Content-Encoding gzip;
+    }
+
+    location ~* \.(?:manifest|appcache|html)$ {
+      expires -1;
+    }
+  }
+}
+
+server {
+  listen 80;
+  server_name beta.omnireader.ru;
+
+  return 301 https://$host$request_uri;
+}

+ 4 - 0
docs/beta.omnireader/deploy.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+npm run build:linux
+sudo -u www-data cp -r ../../dist/linux/* /home/beta.liberama

+ 11 - 0
docs/beta.omnireader/run_server.sh

@@ -0,0 +1,11 @@
+#!/bin/bash
+
+sudo -H -u www-data bash -c "\
+while true; do\
+  trap '' 2;\
+  cd /var/www;\
+  /home/beta.liberama/liberama;\
+  trap 2;\
+  echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
+  sleep 5;\
+done;"

文件差异内容过多而无法显示
+ 692 - 9
package-lock.json


+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Liberama",
   "name": "Liberama",
-  "version": "0.9.1",
+  "version": "0.9.2",
   "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",
@@ -41,6 +41,7 @@
     "mini-css-extract-plugin": "^0.5.0",
     "mini-css-extract-plugin": "^0.5.0",
     "optimize-css-assets-webpack-plugin": "^5.0.3",
     "optimize-css-assets-webpack-plugin": "^5.0.3",
     "pkg": "^4.4.4",
     "pkg": "^4.4.4",
+    "sw-precache-webpack-plugin": "^1.0.0",
     "terser-webpack-plugin": "^1.4.1",
     "terser-webpack-plugin": "^1.4.1",
     "url-loader": "^1.1.2",
     "url-loader": "^1.1.2",
     "vue-class-component": "^6.3.2",
     "vue-class-component": "^6.3.2",
@@ -55,7 +56,6 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@quasar/extras": "^1.5.2",
     "@quasar/extras": "^1.5.2",
-    "appcache-webpack-plugin": "^1.4.0",
     "axios": "^0.18.1",
     "axios": "^0.18.1",
     "base-x": "^3.0.8",
     "base-x": "^3.0.8",
     "chardet": "^0.7.0",
     "chardet": "^0.7.0",
@@ -72,7 +72,7 @@
     "multer": "^1.4.2",
     "multer": "^1.4.2",
     "pako": "^1.0.11",
     "pako": "^1.0.11",
     "path-browserify": "^1.0.0",
     "path-browserify": "^1.0.0",
-    "quasar": "^1.9.6",
+    "quasar": "^1.9.7",
     "safe-buffer": "^5.2.0",
     "safe-buffer": "^5.2.0",
     "sjcl": "^1.0.8",
     "sjcl": "^1.0.8",
     "sql-template-strings": "^2.2.2",
     "sql-template-strings": "^2.2.2",

部分文件因为文件数量过多而无法显示