Book Pauk 3 лет назад
Родитель
Сommit
e6f6cd4ff3
75 измененных файлов с 13959 добавлено и 4934 удалено
  1. 6 0
      .babelrc
  2. 12 5
      .eslintrc
  3. 2 2
      README.md
  4. 46 10
      build/webpack.base.config.js
  5. 3 3
      build/webpack.dev.config.js
  6. 9 8
      build/webpack.prod.config.js
  7. 49 39
      client/components/App.vue
  8. 4 5
      client/components/CardIndex/Book/Book.vue
  9. 4 5
      client/components/CardIndex/Card/Card.vue
  10. 34 11
      client/components/CardIndex/CardIndex.vue
  11. 4 5
      client/components/CardIndex/History/History.vue
  12. 4 5
      client/components/CardIndex/Search/Search.vue
  13. 54 37
      client/components/ExternalLibs/BookmarkSettings/BookmarkSettings.vue
  14. 143 94
      client/components/ExternalLibs/ExternalLibs.vue
  15. 4 5
      client/components/Help/Help.vue
  16. 4 5
      client/components/Income/Income.vue
  17. 4 5
      client/components/NotFound404/NotFound404.vue
  18. 4 5
      client/components/Reader/ClickMapPage/ClickMapPage.vue
  19. 115 99
      client/components/Reader/ContentsPage/ContentsPage.vue
  20. 9 6
      client/components/Reader/CopyTextPage/CopyTextPage.vue
  21. 13 9
      client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue
  22. 36 17
      client/components/Reader/HelpPage/DonateHelpPage/DonateHelpPage.vue
  23. 10 8
      client/components/Reader/HelpPage/HelpPage.vue
  24. 15 8
      client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue
  25. 23 13
      client/components/Reader/HelpPage/MouseHelpPage/MouseHelpPage.vue
  26. 7 7
      client/components/Reader/HelpPage/VersionHistoryPage/VersionHistoryPage.vue
  27. 10 6
      client/components/Reader/LibsPage/LibsPage.vue
  28. 21 17
      client/components/Reader/LoaderPage/LoaderPage.vue
  29. 20 13
      client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue
  30. 6 6
      client/components/Reader/ProgressPage/ProgressPage.vue
  31. 123 71
      client/components/Reader/Reader.vue
  32. 16 9
      client/components/Reader/ReaderDialogs/ReaderDialogs.vue
  33. 42 30
      client/components/Reader/RecentBooksPage/RecentBooksPage.vue
  34. 21 11
      client/components/Reader/SearchPage/SearchPage.vue
  35. 10 6
      client/components/Reader/ServerStorage/ServerStorage.vue
  36. 12 8
      client/components/Reader/SetPositionPage/SetPositionPage.vue
  37. 1 2
      client/components/Reader/SettingsPage/ButtonsTab.inc
  38. 0 0
      client/components/Reader/SettingsPage/ConvertTab.inc
  39. 0 0
      client/components/Reader/SettingsPage/KeysTab.inc
  40. 0 0
      client/components/Reader/SettingsPage/OthersTab.inc
  41. 0 0
      client/components/Reader/SettingsPage/PageMoveTab.inc
  42. 0 0
      client/components/Reader/SettingsPage/ProfilesTab.inc
  43. 0 0
      client/components/Reader/SettingsPage/ResetTab.inc
  44. 84 41
      client/components/Reader/SettingsPage/SettingsPage.vue
  45. 47 38
      client/components/Reader/SettingsPage/UserHotKeys/UserHotKeys.vue
  46. 116 0
      client/components/Reader/SettingsPage/ViewTab/Color.inc
  47. 0 0
      client/components/Reader/SettingsPage/ViewTab/Font.inc
  48. 0 0
      client/components/Reader/SettingsPage/ViewTab/Mode.inc
  49. 0 0
      client/components/Reader/SettingsPage/ViewTab/Status.inc
  50. 0 0
      client/components/Reader/SettingsPage/ViewTab/Text.inc
  51. 0 39
      client/components/Reader/SettingsPage/include/ViewTab.inc
  52. 0 95
      client/components/Reader/SettingsPage/include/ViewTab/Color.inc
  53. 31 20
      client/components/Reader/TextPage/TextPage.vue
  54. 11 0
      client/components/Reader/versionHistory.js
  55. 4 5
      client/components/Settings/Settings.vue
  56. 4 5
      client/components/Sources/Sources.vue
  57. 30 14
      client/components/share/Dialog.vue
  58. 4 5
      client/components/share/Notify.vue
  59. 33 30
      client/components/share/NumInput.vue
  60. 42 23
      client/components/share/StdDialog.vue
  61. 20 14
      client/components/share/Window.vue
  62. 52 0
      client/components/vueComponent.js
  63. 10 12
      client/main.js
  64. 12 11
      client/quasar.js
  65. 4 6
      client/router.js
  66. 2 5
      client/store/index.js
  67. 43 0
      docs/beta/beta.omnireader_http
  68. 3 3
      docs/omnireader.ru/README.md
  69. 12454 3917
      package-lock.json
  70. 43 49
      package.json
  71. 1 1
      server/controllers/WebSocketController.js
  72. 2 2
      server/core/FileDownloader.js
  73. 1 1
      server/core/Reader/BookConverter/textUtils.js
  74. 2 1
      server/core/WebSocketConnection.js
  75. 4 2
      server/core/Zip/ZipStreamer.js

+ 6 - 0
.babelrc

@@ -0,0 +1,6 @@
+{
+    "presets": [['@babel/preset-env', { "targets": { "esmodules": true } }]],
+    "plugins": [
+        ["@babel/plugin-proposal-decorators", { "legacy": true }]
+    ]
+}

+ 12 - 5
.eslintrc

@@ -1,15 +1,15 @@
 {
+  "parser": "vue-eslint-parser",
   "parserOptions": {
-    "parser": "babel-eslint"
+    "parser": "@babel/eslint-parser",
+    "sourceType": "module"
   },
   "extends": [
     "eslint:recommended",
-    "plugin:vue/essential"
+    "plugin:vue/recommended"
   ],
   "plugins": [
-    "vue",
-    "html",
-    "node"
+    "@babel"
   ],
   "env": {
     "browser": true,
@@ -24,6 +24,13 @@
     "LM_TOTAL": false
   },
   "rules": {
+    "vue/html-indent": ["warn", 4, {
+      "alignAttributesVertically": false
+    }],
+    "vue/max-attributes-per-line": "off",
+    "vue/html-self-closing": "off",
+    "vue/no-v-html": "off",
+
     "strict": 0,
     "indent": [0, 4, {
       "SwitchCase": 1

+ 2 - 2
README.md

@@ -2,7 +2,7 @@
 
 Браузерная онлайн-читалка книг и децентрализованная библиотека.
 
-Читалка ![](https://omnireader.ru/favicon.ico)[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
+Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
 
 ![](docs/assets/face.jpg)
 ![](docs/assets/reader.jpg)
@@ -11,7 +11,7 @@
 Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
 
 ## Сборка проекта
-Необходима версия node.js не ниже 10.
+Необходима версия node.js не ниже 14.
 
 ```
 $ git clone https://github.com/bookpauk/liberama

+ 46 - 10
build/webpack.base.config.js

@@ -1,10 +1,15 @@
 const path = require('path');
-//const webpack = require('webpack');
-const VueLoaderPlugin = require('vue-loader/lib/plugin');
+const DefinePlugin = require('webpack').DefinePlugin;
+const { VueLoaderPlugin } = require('vue-loader');
 
 const clientDir = path.resolve(__dirname, '../client');
 
 module.exports = {
+    /*resolve: {
+        alias: {
+            vue: '@vue/compat'
+        }
+    },*/    
     entry: [`${clientDir}/main.js`],
     output: {
         publicPath: '/app/',
@@ -14,10 +19,16 @@ module.exports = {
         rules: [
             {
                 test: /\.vue$/,
-                loader: "vue-loader"
+                loader: 'vue-loader',
+                /*options: {
+                    compilerOptions: {
+                        compatConfig: {
+                            MODE: 2
+                        }
+                    }
+                }*/
             },
             {
-                test: /\.includer$/,
                 resourceQuery: /^\?vue/,
                 use: path.resolve('build/includer.js')
             },
@@ -25,16 +36,33 @@ module.exports = {
                 test: /\.js$/,
                 loader: 'babel-loader',
                 exclude: /node_modules/,
-                query: {
+                options: {
+                    presets: [['@babel/preset-env', { targets: { esmodules: true } }]],
                     plugins: [
-                        'syntax-dynamic-import',
-                        'transform-decorators-legacy',
-                        'transform-class-properties',
-//                        ["component", { "libraryName": "element-ui", "styleLibraryName": `~${clientDir}/theme` } ]
+                        ['@babel/plugin-proposal-decorators', { legacy: true }]
                     ]
                 }
             },
             {
+                test: /\.(gif|png)$/,
+                type: 'asset/inline',
+            },
+            {
+                test: /\.jpg$/,
+                type: 'asset/resource',
+                generator: {
+                    filename: 'images/[name]-[hash:6][ext]'
+                },
+            },
+
+            {
+                test: /\.(ttf|eot|woff|woff2)$/,
+                type: 'asset/resource',
+                generator: {
+                    filename: 'fonts/[name]-[hash:6][ext]'
+                },
+            },
+            /*{
                 test: /\.gif$/,
                 loader: "url-loader",
                 options: {
@@ -61,11 +89,19 @@ module.exports = {
                 options: {
                     name: "fonts/[name]-[hash:6].[ext]"
                 }
-            },
+            },*/
         ]
     },
 
     plugins: [
+        new DefinePlugin({
+            __VUE_OPTIONS_API__: true,
+            __VUE_PROD_DEVTOOLS__: false,
+            __QUASAR_SSR__: false,
+            __QUASAR_SSR_SERVER__: false,
+            __QUASAR_SSR_CLIENT__: false,
+            __QUASAR_VERSION__: false,
+        }),
         new VueLoaderPlugin(),
     ]
 };

+ 3 - 3
build/webpack.dev.config.js

@@ -1,7 +1,7 @@
 const path = require('path');
 const webpack = require('webpack');
 
-const merge = require('webpack-merge');
+const { merge } = require('webpack-merge');
 const baseWpConfig = require('./webpack.base.config');
 
 baseWpConfig.entry.unshift('webpack-hot-middleware/client');
@@ -13,7 +13,7 @@ const clientDir = path.resolve(__dirname, '../client');
 
 module.exports = merge(baseWpConfig, {
     mode: 'development',
-    devtool: "#inline-source-map",
+    devtool: 'inline-source-map',
     output: {
         path: `${publicDir}/app`,
         filename: 'bundle.js'
@@ -38,6 +38,6 @@ module.exports = merge(baseWpConfig, {
             template: `${clientDir}/index.html.template`,
             filename: `${publicDir}/index.html`
         }),
-        new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
+        new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
     ]
 });

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

@@ -1,12 +1,12 @@
 const path = require('path');
 //const webpack = require('webpack');
 
-const merge = require('webpack-merge');
+const { merge } = require('webpack-merge');
 const baseWpConfig = require('./webpack.base.config');
 const TerserPlugin = require('terser-webpack-plugin');
-const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
 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 CopyWebpackPlugin = require('copy-webpack-plugin');
 const {GenerateSW} = require('workbox-webpack-plugin');
@@ -34,19 +34,18 @@ module.exports = merge(baseWpConfig, {
     optimization: {
         minimizer: [
             new TerserPlugin({
-                cache: true,
                 parallel: true,
                 terserOptions: {
-                    output: {
+                    format: {
                         comments: false,
                     },
                 },
             }),
-            new OptimizeCSSAssetsPlugin()
+            new CssMinimizerWebpackPlugin()
         ]
     },
     plugins: [
-        new CleanWebpackPlugin([publicDir], {root: path.resolve(__dirname, '..')}),
+        //new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
         new MiniCssExtractPlugin({
             filename: "[name].[contenthash].css"
         }),
@@ -54,7 +53,9 @@ module.exports = merge(baseWpConfig, {
             template: `${clientDir}/index.html.template`,
             filename: `${publicDir}/index.html`
         }),
-        new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
+        new CopyWebpackPlugin({patterns: 
+            [{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
+        }),
         new GenerateSW({
             cacheId: 'liberama',
             swDest: `${publicDir}/service-worker.js`,

+ 49 - 39
client/components/App.vue

@@ -1,25 +1,28 @@
 <template>
     <div class="fit row">
-        <Notify ref="notify"/>
-        <StdDialog ref="stdDialog"/>
-        <keep-alive v-if="showPage">
-            <router-view class="col"></router-view>
-        </keep-alive>
+        <Notify ref="notify" />
+        <StdDialog ref="stdDialog" />
+
+        <router-view v-slot="{ Component }">
+            <keep-alive v-if="showPage">
+                <component :is="Component" class="col" />
+            </keep-alive>
+        </router-view>
     </div>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from './vueComponent.js';
 
 import Notify from './share/Notify.vue';
 import StdDialog from './share/StdDialog.vue';
+import sanitizeHtml from 'sanitize-html';
 
 import miscApi from '../api/misc';
 import * as utils from '../share/utils';
 
-export default @Component({
+const componentOptions = {
     components: {
         Notify,
         StdDialog,
@@ -31,8 +34,9 @@ export default @Component({
         }
     },
 
-})
-class App extends Vue {
+};
+class App {
+    _options = componentOptions;
     showPage = false;
 
     itemRuText = {
@@ -54,7 +58,7 @@ class App extends Vue {
         //root route
         let cachedRoute = '';
         let cachedPath = '';
-        this.$root.rootRoute = () => {
+        this.$root.getRootRoute = () => {
             if (this.$route.path != cachedPath) {
                 cachedPath = this.$route.path;
                 const m = cachedPath.match(/^(\/[^/]*).*$/i);
@@ -73,46 +77,50 @@ class App extends Vue {
             }
         });
 
-        // set-app-title
-        this.$root.$on('set-app-title', this.setAppTitle);
+        this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
 
-        //global keyHooks
-        this.keyHooks = [];
-        this.keyHook = (event) => {
-            for (const hook of this.keyHooks)
+        // setAppTitle
+        this.$root.setAppTitle = this.setAppTitle;
+
+        //sanitize
+        this.$root.sanitize = sanitizeHtml;
+
+        //global event hooks
+        this.eventHooks = {};
+        this.$root.eventHook = (hookName, event) => {
+            if (!this.eventHooks[hookName])
+                return;
+            for (const hook of this.eventHooks[hookName])
                 hook(event);
         }
 
-        this.$root.addKeyHook = (hook) => {
-            if (this.keyHooks.indexOf(hook) < 0)
-                this.keyHooks.push(hook);
+        this.$root.addEventHook = (hookName, hook) => {
+            if (!this.eventHooks[hookName])
+                this.eventHooks[hookName] = [];
+            if (this.eventHooks[hookName].indexOf(hook) < 0)
+                this.eventHooks[hookName].push(hook);
         }
 
-        this.$root.removeKeyHook = (hook) => {
-            const i = this.keyHooks.indexOf(hook);
+        this.$root.removeEventHook = (hookName, hook) => {
+            if (!this.eventHooks[hookName])
+                return;
+            const i = this.eventHooks[hookName].indexOf(hook);
             if (i >= 0)
-                this.keyHooks.splice(i, 1);
+                this.eventHooks[hookName].splice(i, 1);
         }
 
         document.addEventListener('keyup', (event) => {
-            this.keyHook(event);
+            this.$root.eventHook('key', event);
         });
         document.addEventListener('keypress', (event) => {
-            this.keyHook(event);
+            this.$root.eventHook('key', event);
         });
         document.addEventListener('keydown', (event) => {
-            this.keyHook(event);
-        });
-        window.addEventListener('resize', () => {
-            this.$root.$emit('resize');
+            this.$root.eventHook('key', event);
         });
-    }
 
-    routerReady() {
-        return new Promise ((resolve) => {
-            this.$router.onReady(() => {
-                resolve();
-            });
+        window.addEventListener('resize', (event) => {
+            this.$root.eventHook('resize', event);
         });
     }
 
@@ -142,14 +150,14 @@ class App extends Vue {
             if (navigator.storage && navigator.storage.persist) {
                 navigator.storage.persist();
             }
-            await this.routerReady();
+            await this.$router.isReady();
             this.redirectIfNeeded();
         })();
     }
 
     toggleCollapse() {
         this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
-        this.$root.$emit('resize');
+        this.$root.eventHook('resize');
     }
 
     get isCollapse() {
@@ -184,7 +192,7 @@ class App extends Vue {
     }
 
     get rootRoute() {
-        return this.$root.rootRoute();
+        return this.$root.getRootRoute();
     }
 
     setAppTitle(title) {
@@ -194,7 +202,7 @@ class App extends Vue {
             } else if (this.mode == 'omnireader') {
                 document.title = `Omni Reader - всегда с вами`;
             } else if (this.config && this.mode !== null) {
-                document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
+                document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
             }
         } else {
             document.title = title;
@@ -239,6 +247,8 @@ class App extends Vue {
         }
     }
 }
+
+export default vueComponent(App);
 //-----------------------------------------------------------------------------
 </script>
 

+ 4 - 5
client/components/CardIndex/Book/Book.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
-export default @Component({
-})
-class Book extends Vue {
+class Book {
     created() {
     }
 
 }
+
+export default vueComponent(Book);
 //-----------------------------------------------------------------------------
 </script>

+ 4 - 5
client/components/CardIndex/Card/Card.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
-export default @Component({
-})
-class Card extends Vue {
+class Card {
     created() {
     }
 
 }
+
+export default vueComponent(Card);
 //-----------------------------------------------------------------------------
 </script>

+ 34 - 11
client/components/CardIndex/CardIndex.vue

@@ -1,15 +1,16 @@
 <template>
     <div>
-        <keep-alive>
-            <router-view></router-view>
-        </keep-alive>
+        <router-view v-slot="{ Component }">
+            <keep-alive>
+                <component :is="Component" />
+            </keep-alive>
+        </router-view>        
     </div>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 import _ from 'lodash';
 
 const selfRoute = '/cardindex';
@@ -21,20 +22,32 @@ const tab2Route = [
 ];
 let lastActiveTab = null;
 
-export default @Component({
+const componentOptions = {
     watch: {
-        selectedTab: function(newValue, oldValue) {
+        selectedTab: function(newValue) {
             lastActiveTab = newValue;
             this.setRouteByTab(newValue);
         },
-        curRoute: function(newValue, oldValue) {
+        curRoute: function(newValue) {
             this.setTabByRoute(newValue);
         },
     },
-})
-class CardIndex extends Vue {
+};
+class CardIndex {
+    _options = componentOptions;
     selectedTab = null;
 
+    created() {
+        this.$watch(
+            () => this.$route.path,
+            (newValue) => {
+                if (newValue == '/cardindex' && this.isReader) {
+                    this.$router.replace({ path: '/reader' });
+                }
+            }
+        )
+    }
+
     mounted() {
         this.setTabByRoute(this.curRoute);
     }
@@ -57,12 +70,22 @@ class CardIndex extends Vue {
         }
     }
 
+    get mode() {
+        return this.$store.state.config.mode;
+    }
+
     get curRoute() {
-        const m = this.$route.path.match(/^(\/[^\/]*\/[^\/]*).*$/i);
+        const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
         return (m ? m[1] : this.$route.path);
     }
 
+    get isReader() {
+        return (this.mode !== null && (this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top'));
+    }
+
 }
+
+export default vueComponent(CardIndex);
 //-----------------------------------------------------------------------------
 </script>
 

+ 4 - 5
client/components/CardIndex/History/History.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
-export default @Component({
-})
-class History extends Vue {
+class History {
     created() {
     }
 
 }
+
+export default vueComponent(History);
 //-----------------------------------------------------------------------------
 </script>

+ 4 - 5
client/components/CardIndex/Search/Search.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
-export default @Component({
-})
-class Search extends Vue {
+class Search {
     created() {
     }
 
 }
+
+export default vueComponent(Search);
 //-----------------------------------------------------------------------------
 </script>

+ 54 - 37
client/components/ExternalLibs/BookmarkSettings/BookmarkSettings.vue

@@ -1,63 +1,79 @@
 <template>
     <Window ref="window" width="600px" height="95%" @close="close">
-        <template slot="header">
+        <template #header>
             Настроить закладки
         </template>
 
         <div class="col column fit">
             <div class="row items-center top-panel bg-grey-3">
-                <q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected">
-                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip>
+                <q-btn :disabled="!selected" class="q-mr-md" round dense color="blue" icon="la la-check" size="16px" @click.stop="openSelected">
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        Открыть выбранную закладку
+                    </q-tooltip>
                 </q-btn>
-                <q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search">
-                    <template v-slot:append>
-                        <q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch"/>
+                <q-input ref="search" v-model="search" class="col" rounded outlined dense bg-color="white" placeholder="Найти">
+                    <template #append>
+                        <q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
                     </template>
                 </q-input>
             </div>
 
             <div class="col row">
                 <div class="left-panel column items-center no-wrap bg-grey-3">
-                    <q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px">
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
+                    <q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" size="14px" @click.stop="addBookmark">
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            Добавить закладку
+                        </q-tooltip>
                     </q-btn>
-                    <q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length">
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip>
+                    <q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-minus" size="14px" @click.stop="delBookmark">
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            Удалить отмеченные закладки
+                        </q-tooltip>
                     </q-btn>
-                    <q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0">
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip>
+                    <q-btn :disabled="!selected || selected.indexOf('r-') == 0" class="q-mb-sm" round dense color="blue" icon="la la-edit" size="14px" @click.stop="editBookmark">
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            Редактировать закладку
+                        </q-tooltip>
                     </q-btn>
-                    <q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length">
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip>
+                    <q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" size="14px" @click.stop="moveBookmark(false)">
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            Переместить отмеченные вверх
+                        </q-tooltip>
                     </q-btn>
-                    <q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length">
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip>
+                    <q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" size="14px" @click.stop="moveBookmark(true)">
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            Переместить отмеченные вниз
+                        </q-tooltip>
                     </q-btn>
-                    <q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px">
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip>
+                    <q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" size="14px" @click.stop="setDefaultBookmarks">
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            Установить по умолчанию
+                        </q-tooltip>
                     </q-btn>
-                    <div class="space"/>
+                    <div class="space" />
                 </div>
 
                 <div class="col fit tree">
                     <div v-show="nodes.length" class="checkbox-tick-all">
-                        <q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" />
+                        <q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @input="makeTickAll" />
                     </div>
                     <q-tree
                         class="q-my-xs"
                         :nodes="nodes"
                         node-key="key"
                         tick-strategy="leaf"
-                        :selected.sync="selected"
-                        :ticked.sync="ticked"
-                        :expanded.sync="expanded"
+                        v-model:selected="selected"
+                        v-model:ticked="ticked"
+                        v-model:expanded="expanded"
                         selected-color="black"
                         :filter="search"
                         no-nodes-label="Закладок пока нет"
                         no-results-label="Ничего не найдено"
                     >
-                        <template v-slot:default-header="p">
-                            <div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div>
+                        <template #default-header="p">
+                            <div class="q-px-xs" :class="{selected: selected == p.key}">
+                                {{ p.node.label }}
+                            </div>
                         </template>
                     </q-tree>
                 </div>
@@ -68,22 +84,15 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
+
 import _ from 'lodash';
 
 import Window from '../../share/Window.vue';
 import * as lu from '../linkUtils';
 import rstore from '../../../store/modules/reader';
 
-const BookmarkSettingsProps = Vue.extend({
-    props: {
-        libs: Object,
-        addBookmarkVisible: Boolean,
-    }
-});
-
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
@@ -92,8 +101,14 @@ export default @Component({
             this.checkAllTicked();
         },
     }    
-})
-class BookmarkSettings extends BookmarkSettingsProps {
+};
+class BookmarkSettings {
+    _options = componentOptions;
+    _props = {
+        libs: Object,
+        addBookmarkVisible: Boolean,
+    };
+
     search = '';
     selected = '';
     ticked = [];
@@ -308,6 +323,8 @@ class BookmarkSettings extends BookmarkSettingsProps {
     }
 
 }
+
+export default vueComponent(BookmarkSettings);
 //-----------------------------------------------------------------------------
 </script>
 

+ 143 - 94
client/components/ExternalLibs/ExternalLibs.vue

@@ -1,82 +1,114 @@
 <template>
-    <Window ref="window" @close="close" margin="2px">
-        <template slot="header">
+    <Window ref="window" margin="2px" @close="close">
+        <template #header>
             {{ header }}
         </template>
 
-        <template slot="buttons">
+        <template #buttons>
             <span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
-                <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
+                <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
                 <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
             </span>
             <span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
-                <q-icon name="la la-plus" size="16px"/>
+                <q-icon name="la la-plus" size="16px" />
                 <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
             </span>
             <span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
-                <q-icon name="la la-minus" size="16px"/>
+                <q-icon name="la la-minus" size="16px" />
                 <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
             </span>
             <span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
-                <q-icon name="la la-question-circle" size="16px"/>
+                <q-icon name="la la-question-circle" size="16px" />
                 <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
             </span>
         </template>
 
         <div v-show="ready" class="col column" style="min-width: 600px">
             <div class="row items-center q-px-sm" style="height: 50px">
-                <q-select class="q-mr-sm" ref="rootLink" v-model="rootLink" :options="rootLinkOptions" @input="rootLinkInput"
-                    @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
+                <q-select
+                    ref="rootLink"
+                    v-model="rootLink"
+                    class="q-mr-sm"
+                    :options="rootLinkOptions"
                     style="width: 230px"
                     dropdown-icon="la la-angle-down la-sm"
                     rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
+                    @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
                 >
-                    <template v-slot:prepend>
-                        <q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
-                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
+                    <template #prepend>
+                        <q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" size="12px" @click.stop="addBookmark">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                Добавить закладку
+                            </q-tooltip>
                         </q-btn>
-                        <q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
-                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
+                        <q-btn round dense color="blue" icon="la la-bars" size="12px" @click.stop="bookmarkSettings">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                Настроить закладки
+                            </q-tooltip>
                         </q-btn>
                     </template>
-                    <template v-slot:selected>
-                        <div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
+                    <template #selected>
+                        <div style="overflow: hidden; white-space: nowrap;">
+                            {{ rootLinkWithoutProtocol }}
+                        </div>
                     </template>
                 </q-select>
 
-                <q-select class="q-mr-sm" ref="selectedLink" v-model="selectedLink" :options="selectedLinkOptions" @input="selectedLinkInput" style="width: 50px"
-                    @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
+                <q-select
+                    ref="selectedLink"
+                    v-model="selectedLink"
+                    class="q-mr-sm"
+                    :options="selectedLinkOptions"
+                    style="width: 50px"
                     dropdown-icon="la la-angle-down la-sm"
                     rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
+                    @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
                 >
-                    <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%">
+                        Закладки
+                    </q-tooltip>
                 </q-select>
 
-                <q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги"
+                <q-input
+                    ref="input"
+                    v-model="bookUrl"
+                    class="col q-mr-sm"
+                    rounded outlined dense
+                    bg-color="white"
+                    placeholder="Скопируйте сюда URL книги"
                     @focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
                 >
-                    <template v-slot:prepend>
-                        <q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
-                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
+                    <template #prepend>
+                        <q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" size="12px" @click="goToLink(selectedLink)">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                Вернуться на стартовую страницу
+                            </q-tooltip>
                         </q-btn>
-                        <q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
-                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
+                        <q-btn :disabled="!bookUrl" round dense color="blue" icon="la la-angle-double-down" size="12px" @click="openBookUrlInFrame">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                Загрузить URL во фрейм
+                            </q-tooltip>
                         </q-btn>
                     </template>
-                    <template v-slot:append>
-                        <q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
-                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
+                    <template #append>
+                        <q-btn round dense color="blue" icon="la la-cog" size="12px" @click.stop="optionsVisible = true">
+                            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                Опции
+                            </q-tooltip>
                         </q-btn>
                     </template>
                 </q-input>
 
-                <q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
-                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
+                <q-btn :disabled="!bookUrl" rounded color="green-7" no-caps size="14px" @click="submitUrl">
+                    Открыть
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        Открыть в читалке
+                    </q-tooltip>
                 </q-btn>
             </div>
             <div class="separator"></div>
 
-            <div class="col fit" ref="frameBox" style="position: relative;">
+            <div ref="frameBox" class="col fit" style="position: relative;">
                 <div ref="frameWrap" class="overflow-hidden">
                     <iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
                 </div>
@@ -84,41 +116,68 @@
             </div>
 
             <Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
-                <template slot="header">
+                <template #header>
                     <div class="row items-center">
                         <q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
-                        <div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
-                        <div v-else>Добавить закладку</div>
+                        <div v-if="addBookmarkMode == 'edit'">
+                            Редактировать закладку
+                        </div>
+                        <div v-else>
+                            Добавить закладку
+                        </div>
                     </div>
                 </template>
 
                 <div class="q-mx-md row">
-                    <q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" @keydown="bookmarkLinkKeyDown"
-                        placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus">
+                    <q-input
+                        ref="bookmarkLink"
+                        v-model="bookmarkLink"
+                        class="col q-mr-sm"
+                        outlined dense
+                        bg-color="white"
+                        placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
+                    >
                     </q-input>
 
-                    <q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px"
+                    <q-select
+                        ref="defaultRootLink"
+                        v-model="defaultRootLink"
+                        class="q-mr-sm"
+                        :options="defaultRootLinkOptions"
+                        style="width: 50px"
                         dropdown-icon="la la-angle-down la-sm"
                         outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
                     >
-                        <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%">
+                            Предустановленные ссылки
+                        </q-tooltip>
                     </q-select>
                 </div>
 
                 <div class="q-mx-md q-mt-md">
-                    <q-input class="col q-mr-sm" ref="bookmarkDesc" outlined dense bg-color="white" v-model="bookmarkDesc" @keydown="bookmarkDescKeyDown"
-                        placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus">
+                    <q-input
+                        ref="bookmarkDesc"
+                        v-model="bookmarkDesc"
+                        class="col q-mr-sm"
+                        outlined dense
+                        bg-color="white"
+                        placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
+                    >
                     </q-input>
                 </div>
 
-                <template slot="footer">
-                    <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="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
+                <template #footer>
+                    <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                        Отмена
+                    </q-btn>
+                    <q-btn :disabled="!bookmarkLink" class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark">
+                        OK
+                    </q-btn>
                 </template>
             </Dialog>
 
             <Dialog ref="options" v-model="optionsVisible">
-                <template slot="header">
+                <template #header>
                     <div class="row items-center">
                         <q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
                         Опции
@@ -131,22 +190,29 @@
                     <q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
                 </div>
 
-                <template slot="footer">
-                    <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
+                <template #footer>
+                    <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">
+                        OK
+                    </q-btn>
                 </template>
             </Dialog>
         </div>
 
-        <BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
-            @do-action="doAction" @close="closeBookmarkSettings">
+        <BookmarkSettings
+            v-if="bookmarkSettingsActive"
+            ref="bookmarkSettings"
+            :libs="libs"
+            :add-bookmark-visible="addBookmarkVisible"
+            @do-action="doAction" @close="closeBookmarkSettings"
+        >
         </BookmarkSettings>
     </Window>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
+
 import _ from 'lodash';
 
 import Window from '../share/Window.vue';
@@ -162,20 +228,20 @@ const proxySubst = {
     'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
 };
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
         Dialog,
         BookmarkSettings
     },
     watch: {
-        libs: function() {
+        libs() {
             this.loadLibs();
         },
-        defaultRootLink: function() {
+        defaultRootLink() {
             this.updateBookmarkLink();
         },
-        bookUrl: function(newValue) {
+        bookUrl(newValue) {
             const value = lu.addProtocol(newValue);
             const subst = this.makeProxySubst(value, true);
             if (value != subst) {
@@ -184,7 +250,7 @@ export default @Component({
                 });
             }
         },
-        bookmarkLink: function(newValue) {
+        bookmarkLink(newValue) {
             const value = lu.addProtocol(newValue);
             const subst = this.makeProxySubst(value, true);
             if (value != subst) {
@@ -193,18 +259,26 @@ export default @Component({
                 });
             }
         },
-        closeAfterSubmit: function(newValue) {
+        closeAfterSubmit(newValue) {
             this.commitProp('closeAfterSubmit', newValue);
         },
-        openInFrameOnEnter: function(newValue) {
+        openInFrameOnEnter(newValue) {
             this.commitProp('openInFrameOnEnter', newValue);
         },
-        openInFrameOnAdd: function(newValue) {
+        openInFrameOnAdd(newValue) {
             this.commitProp('openInFrameOnAdd', newValue);
         },
+        rootLink() {
+            this.rootLinkInput();
+        },
+        selectedLink() {
+            this.selectedLinkInput();
+        },
     }    
-})
-class ExternalLibs extends Vue {
+};
+class ExternalLibs {
+    _options = componentOptions;
+
     ready = false;
     frameVisible = false;
     rootLink = '';
@@ -233,9 +307,9 @@ class ExternalLibs extends Vue {
     created() {
         this.oldStartLink = '';
         this.justOpened = true;
-        this.$root.addKeyHook(this.keyHook);
+        this.$root.addEventHook('key', this.keyHook);
 
-        this.$root.$on('resize', async() => {
+        this.$root.addEventHook('resize', async() => {
             await utils.sleep(200);
             this.frameResize();
         });
@@ -252,29 +326,6 @@ class ExternalLibs extends Vue {
     }
 
     mounted() {
-        //Поправка метода toggleOption компонента select фреймворка quasar, необходимо другое поведение
-        //$emit('input'.. вызывается всегда
-        this.toggleOption = function(opt, keepOpen) {
-            if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
-                return;
-            }
-
-            const optValue = this.getOptionValue(opt);
-
-            if (this.multiple !== true) {
-                if (keepOpen !== true) {
-                    this.updateInputValue(this.fillInput === true ? this.getOptionLabel(opt) : '', true, true);
-                    this.hidePopup();
-                }
-
-                this.$refs.target !== void 0 && this.$refs.target.focus();
-                this.$emit('input', this.emitValue === true ? optValue : opt);
-            }
-        };
-
-        this.$refs.rootLink.toggleOption = this.toggleOption;
-        this.$refs.selectedLink.toggleOption = this.toggleOption;
-
         (async() => {
             //подождем this.mode
             let i = 0;
@@ -412,7 +463,7 @@ class ExternalLibs extends Vue {
         if (this.ready && this.selectedLink) {
             result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
         }
-        this.$root.$emit('set-app-title', result);
+        this.$root.setAppTitle(result);
         return result;
     }
 
@@ -621,9 +672,9 @@ class ExternalLibs extends Vue {
 
         this.addBookmarkMode = mode;
         this.addBookmarkVisible = true;
-        this.$nextTick(() => {
+        this.$nextTick(async() => {
+            await this.$refs.dialogAddBookmark.waitShown();
             this.$refs.bookmarkLink.focus();
-            this.$refs.defaultRootLink.toggleOption = this.toggleOption;
         });
     }
 
@@ -638,10 +689,6 @@ class ExternalLibs extends Vue {
         }
     }
 
-    defaultRootLinkInput() {
-        this.updateBookmarkLink();
-    }
-
     bookmarkLinkKeyDown(event) {
         if (event.key == 'Enter') {
             this.$refs.bookmarkDesc.focus();
@@ -704,7 +751,7 @@ class ExternalLibs extends Vue {
                 this.commitLibs(libs);
             } else if (item.c != this.bookmarkDesc) {
                 if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
-                    `Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
+                    `Заменить '${this.$root.sanitize(item.c)}' на '${this.$root.sanitize(this.bookmarkDesc)}'?`, ' ')) {
                     item.c = this.bookmarkDesc;
                     this.commitLibs(libs);                    
                 } else 
@@ -810,7 +857,7 @@ class ExternalLibs extends Vue {
     }
 
     keyHook(event) {
-        if (this.$root.rootRoute() == '/external-libs') {
+        if (this.$root.getRootRoute() == '/external-libs') {
             if (this.$root.stdDialog.active)
                 return false;
 
@@ -836,6 +883,8 @@ class ExternalLibs extends Vue {
         return false;
     }
 }
+
+export default vueComponent(ExternalLibs);
 //-----------------------------------------------------------------------------
 </script>
 

+ 4 - 5
client/components/Help/Help.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-})
-class Help extends Vue {
+class Help {
     created() {
     }
 
 }
+
+export default vueComponent(Help);
 //-----------------------------------------------------------------------------
 </script>

+ 4 - 5
client/components/Income/Income.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-})
-class Income extends Vue {
+class Income {
     created() {
     }
 
 }
+
+export default vueComponent(Income);
 //-----------------------------------------------------------------------------
 </script>

+ 4 - 5
client/components/NotFound404/NotFound404.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-})
-class NotFound404 extends Vue {
+class NotFound404 {
     created() {
     }
 
 }
+
+export default vueComponent(NotFound404);
 //-----------------------------------------------------------------------------
 </script>

+ 4 - 5
client/components/Reader/ClickMapPage/ClickMapPage.vue

@@ -6,15 +6,12 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import {sleep} from '../../../share/utils';
 import {clickMap, clickMapText} from '../share/clickMap';
 
-export default @Component({
-})
-class ClickMapPage extends Vue {
+class ClickMapPage {
     fontSize = '200%';
 
     created() {
@@ -53,6 +50,8 @@ class ClickMapPage extends Vue {
         await sleep(5000);
     }
 }
+
+export default vueComponent(ClickMapPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 115 - 99
client/components/Reader/ContentsPage/ContentsPage.vue

@@ -1,125 +1,139 @@
 <template>
-    <Window width="600px" ref="window" @close="close">
-        <template slot="header">
+    <Window ref="window" width="600px" @close="close">
+        <template #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="images" icon="la la-image" 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 v-for="item in contents" :key="item.key" class="column" style="width: 540px">
-                <div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
-                    <div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
-                        <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
-                    </div>
-                    <div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
-                        <q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
-                    </div>
-                    <div class="col row clickable" @click="setBookPos(item.offset)">
-                        <div :style="item.indentStyle"></div>
-                        <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
-                        <div class="column justify-center">{{ item.perc }}%</div>
+        <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="images" icon="la la-image" label="Изображения" />
+                <q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
+            </q-tabs>
+        </div>
+
+        <div class="q-mb-sm" />
+
+        <div v-show="selectedTab == 'contents'" class="tab-panel">
+            <div>
+                <div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
+                    <div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
+                        <div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
+                            <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" />
+                        </div>
+                        <div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
+                            <q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px" />
+                        </div>
+                        <div class="col row clickable" @click="setBookPos(item.offset)">
+                            <div :style="item.indentStyle"></div>
+                            <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
+                            <div class="column justify-center">
+                                {{ item.perc }}%
+                            </div>
+                        </div>
                     </div>
-                </div>
-                
-                <div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
-                    <div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
-                        <div class="col row clickable" @click="setBookPos(subitem.offset)">
-                            <div class="no-expand-button"></div>
-                            <div :style="subitem.indentStyle"></div>
-                            <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
-                            <div class="column justify-center">{{ subitem.perc }}%</div>
+                    
+                    <div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
+                        <div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
+                            <div class="col row clickable" @click="setBookPos(subitem.offset)">
+                                <div class="no-expand-button"></div>
+                                <div :style="subitem.indentStyle"></div>
+                                <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
+                                <div class="column justify-center">
+                                    {{ subitem.perc }}%
+                                </div>
+                            </div>
                         </div>
                     </div>
                 </div>
-            </div>
-            <div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
-                Оглавление отсутствует
+                <div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
+                    Оглавление отсутствует
+                </div>
             </div>
         </div>
-    </div>
-
-    <div class="tab-panel" v-show="selectedTab == 'images'">
-        <div>
-            <div v-for="item in images" :key="item.key" class="column" style="width: 540px">
-                <div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
-                    <div class="col row clickable" @click="setBookPos(item.offset)">
-                        <div class="image-thumb-box row justify-center items-center">
-                            <div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
-                            <img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
-                        </div>
-                        <div class="no-expand-button column justify-center items-center">
-                            <div class="image-num">{{ item.num }}</div>
-                            <div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
-                            <div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
-                            <div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
+
+        <div v-show="selectedTab == 'images'" class="tab-panel">
+            <div>
+                <div v-for="item in images" :key="item.key" class="column" style="width: 540px">
+                    <div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
+                        <div class="col row clickable" @click="setBookPos(item.offset)">
+                            <div class="image-thumb-box row justify-center items-center">
+                                <div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
+                                    <i class="loading-img-icon la la-images"></i>
+                                </div>
+                                <img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
+                            </div>
+                            <div class="no-expand-button column justify-center items-center">
+                                <div class="image-num">
+                                    {{ item.num }}
+                                </div>
+                                <div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">
+                                    JPG
+                                </div>
+                                <div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">
+                                    PNG
+                                </div>
+                                <div v-show="!item.local" class="image-type it-net-color row justify-center">
+                                    INET
+                                </div>
+                            </div>
+                            <div :style="item.indentStyle"></div>
+                            <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
+                            <div class="column justify-center">
+                                {{ item.perc }}%
+                            </div>
                         </div>
-                        <div :style="item.indentStyle"></div>
-                        <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
-                        <div class="column justify-center">{{ item.perc }}%</div>
                     </div>
                 </div>
-            </div>
-            <div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
-                Изображения отсутствуют
+                <div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
+                    Изображения отсутствуют
+                </div>
             </div>
         </div>
-    </div>
 
-    <div class="tab-panel" v-show="selectedTab == 'bookmarks'">
-        <div class="column justify-center items-center" style="height: 100px">
-            Раздел находится в разработке
+        <div v-show="selectedTab == 'bookmarks'" class="tab-panel">
+            <div class="column justify-center items-center" style="height: 100px">
+                Раздел находится в разработке
+            </div>
         </div>
-    </div>
-
     </Window>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
+
 //import _ from 'lodash';
 
 import Window from '../../share/Window.vue';
 import * as utils from '../../../share/utils';
 
-const ContentsPageProps = Vue.extend({
-    props: {
-        bookPos: Number,
-        isVisible: Boolean,
-    }
-});
-
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
     watch: {
-        bookPos: function() {
+        bookPos() {
             this.updateBookPosSelection();
         }
     },
-})
-class ContentsPage extends ContentsPageProps {
+};
+class ContentsPage {
+    _options = componentOptions;
+    _props = {
+        bookPos: Number,
+        isVisible: Boolean,
+    };
+
     selectedTab = 'contents';
     contents = [];
     images = [];
@@ -255,9 +269,9 @@ class ContentsPage extends ContentsPageProps {
                 const {id, local} = ims[i];
                 const bin = this.parsed.binary[id];
                 if (local)
-                    this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : ''));
+                    this.imageSrc[id] = (bin ? `data:${bin.type};base64,${bin.data}` : '');
                 else
-                    this.$set(this.imageSrc, id, id);
+                    this.imageSrc[id] = id;
                 this.imageLoaded[id] = true;
                 await utils.sleep(5);
             }
@@ -281,17 +295,17 @@ class ContentsPage extends ContentsPageProps {
 
                 if (bp >= subitem.offset && bp < nextSubOffset) {
                     subitem.isBookPos = true;
-                    this.$set(this.contents, i, Object.assign(item, {list: item.list}));
+                    this.contents[i] = Object.assign(item, {list: item.list});
                 } else if (subitem.isBookPos) {
                     subitem.isBookPos = false;
-                    this.$set(this.contents, i, Object.assign(item, {list: item.list}));
+                    this.contents[i] = Object.assign(item, {list: item.list});
                 }
             }
 
             if (bp >= item.offset && bp < nextOffset) {
-                this.$set(this.contents, i, Object.assign(item, {isBookPos: true}));
+                this.contents[i] = Object.assign(item, {isBookPos: true});
             } else if (item.isBookPos) {
-                this.$set(this.contents, i, Object.assign(item, {isBookPos: false}));
+                this.contents[i] = Object.assign(item, {isBookPos: false});
             }
         }
 
@@ -300,9 +314,9 @@ class ContentsPage extends ContentsPageProps {
             const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
 
             if (bp >= img.offset && bp < nextOffset) {
-                this.$set(this.images, i, Object.assign(img, {isBookPos: true}));
+                this.images[i] = Object.assign(img, {isBookPos: true});
             } else if (img.isBookPos) {
-                this.$set(this.images, i, Object.assign(img, {isBookPos: false}));
+                this.images[i] = Object.assign(img, {isBookPos: false});
             }
         }
     }
@@ -312,16 +326,16 @@ class ContentsPage extends ContentsPageProps {
         const expanded = !item.expanded;
 
         if (!expanded) {
-            const subitems = this.$refs[`subitem${key}`][0];
+            const subitems = this.$refs[`subitem${key}`];
             subitems.style.height = '0';
             await utils.sleep(200);
         }
 
-        this.$set(this.contents, key, Object.assign({}, item, {expanded}));
+        this.contents[key] = Object.assign({}, item, {expanded});
 
         if (expanded) {
             await this.$nextTick();
-            const subitems = this.$refs[`subitem${key}`][0];
+            const subitems = this.$refs[`subitem${key}`];
             subitems.style.height = subitems.scrollHeight + 'px';
         }
     }
@@ -342,6 +356,8 @@ class ContentsPage extends ContentsPageProps {
         return true;
     }
 }
+
+export default vueComponent(ContentsPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 9 - 6
client/components/Reader/CopyTextPage/CopyTextPage.vue

@@ -1,6 +1,6 @@
 <template>
     <Window @close="close">
-        <template slot="header">
+        <template #header>
             Скопировать текст
         </template>
 
@@ -12,18 +12,19 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import Window from '../../share/Window.vue';
 import {sleep} from '../../../share/utils';
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
-})
-class CopyTextPage extends Vue {
+};
+class CopyTextPage {
+    _options = componentOptions;
+
     text = null;
     initStep = null;
     initPercentage = 0;
@@ -101,6 +102,8 @@ class CopyTextPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(CopyTextPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 13 - 9
client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue

@@ -18,15 +18,20 @@
             <li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
         </ul>
 
-        <p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку 
-        на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
+        <p>
+            В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку 
+            на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").
+        </p>
         <p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
 
         <div v-show="mode == 'omnireader' || mode == 'liberama.top'">
-            <p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
+            <p>
+                Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
                 <br><strong>{{ bookmarkText }}</strong>
                 <q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
-                    <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                    <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                        Скопировать
+                    </q-tooltip>                    
                 </q-icon>
 
                 <br>или перетащив на панель закладок следующую ссылку:
@@ -41,14 +46,11 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
 
 import {copyTextToClipboard} from '../../../../share/utils';
 
-export default @Component({
-})
-class CommonHelpPage extends Vue {
+class CommonHelpPage {
     created() {
     }
 
@@ -69,6 +71,8 @@ class CommonHelpPage extends Vue {
             this.$root.notify.error(msg);
     }
 }
+
+export default vueComponent(CommonHelpPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 36 - 17
client/components/Reader/HelpPage/DonateHelpPage/DonateHelpPage.vue

@@ -1,49 +1,68 @@
 <template>
     <div class="page">
         <div class="box">
-            <p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
+            <p class="p">
+                Вы можете пожертвовать на развитие проекта любую сумму:
+            </p>
             <div class="address">
                 <img class="logo" src="./assets/yoomoney.png">
-                <q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">Пожертвовать</q-btn><br>
-                <div class="para">{{ yooAddress }}
+                <q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
+                    Пожертвовать
+                </q-btn><br>
+                <div class="para">
+                    {{ yooAddress }}
                     <q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
-                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                            Скопировать
+                        </q-tooltip>                    
                     </q-icon>
                 </div>
             </div>
 
             <div class="address">                
                 <img class="logo" src="./assets/paypal.png">
-                <div class="para">{{ paypalAddress }}
+                <div class="para">
+                    {{ paypalAddress }}
                     <q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
-                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                            Скопировать
+                        </q-tooltip>                    
                     </q-icon>
                 </div>
             </div>
 
             <div class="address">                
                 <img class="logo" src="./assets/bitcoin.png">
-                <div class="para">{{ bitcoinAddress }}
+                <div class="para">
+                    {{ bitcoinAddress }}
                     <q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
-                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                            Скопировать
+                        </q-tooltip>                    
                     </q-icon>
                 </div>
             </div>
 
             <div class="address">                
                 <img class="logo" src="./assets/litecoin.png">
-                <div class="para">{{ litecoinAddress }}
+                <div class="para">
+                    {{ litecoinAddress }}
                     <q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
-                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                            Скопировать
+                        </q-tooltip>                    
                     </q-icon>
                 </div>
             </div>
 
             <div class="address">                
                 <img class="logo" src="./assets/monero.png">
-                <div class="para">{{ moneroAddress }}
+                <div class="para">
+                    {{ moneroAddress }}
                     <q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
-                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                            Скопировать
+                        </q-tooltip>                    
                     </q-icon>
                 </div>
             </div>
@@ -53,13 +72,11 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
+
 import {copyTextToClipboard} from '../../../../share/utils';
 
-export default @Component({
-})
-class DonateHelpPage extends Vue {
+class DonateHelpPage {
     yooAddress = '410018702323056';
     paypalAddress = 'bookpauk@gmail.com';
     bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
@@ -81,6 +98,8 @@ class DonateHelpPage extends Vue {
             this.$root.notify.error('Копирование не удалось');
     }
 }
+
+export default vueComponent(DonateHelpPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 10 - 8
client/components/Reader/HelpPage/HelpPage.vue

@@ -1,6 +1,6 @@
 <template>
     <Window @close="close">
-        <template slot="header">
+        <template #header>
             Справка
         </template>
 
@@ -14,8 +14,7 @@
             <div class="separator"></div>
 
             <keep-alive>
-                <component ref="page" class="col" :is="activePage"
-                ></component>
+                <component :is="activePage" ref="page" class="col"></component>
             </keep-alive>
         </div>
     </Window>
@@ -23,8 +22,7 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import Window from '../../share/Window.vue';
 import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
@@ -49,10 +47,12 @@ const tabs = [
     ['DonateHelpPage', 'Помочь проекту'],
 ];
 
-export default @Component({
+const componentOptions = {
     components: Object.assign({ Window }, pages),
-})
-class HelpPage extends Vue {
+};
+class HelpPage {
+    _options = componentOptions;
+
     selectedTab = 'CommonHelpPage';
 
     close() {
@@ -87,6 +87,8 @@ class HelpPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(HelpPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 15 - 8
client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue

@@ -1,29 +1,34 @@
 <template>
     <div class="page">
         <div style="font-size: 120%">
-            <div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
+            <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 class="text-right text-italic" style="font-size: 80%">
+                * Изменить сочетания клавиш можно в настройках
+            </div>
+            <UserHotKeys v-model="userHotKeys" readonly />
         </div>
     </div>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
 
 import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
 
-export default @Component({
+const componentOptions = {
     components: {
         UserHotKeys,
     },
-})
-class HotkeysHelpPage extends Vue {
+};
+class HotkeysHelpPage {
+    _options = componentOptions;
+
     created() {
     }
 
@@ -36,6 +41,8 @@ class HotkeysHelpPage extends Vue {
     }
 
 }
+
+export default vueComponent(HotkeysHelpPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 23 - 13
client/components/Reader/HelpPage/MouseHelpPage/MouseHelpPage.vue

@@ -3,21 +3,28 @@
         <span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
         <ul>
             <li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
-                <div class="click-map-page">
-                    <ClickMapPage ref="clickMapPage"></ClickMapPage>
-                </div>
+            <div class="click-map-page">
+                <ClickMapPage ref="clickMapPage"></ClickMapPage>
+            </div>
         
             <li><b>ПКМ</b> - показать/скрыть панель управления</li>
             <li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
             <br>
             <li>Жесты для тачскрина:</li>
             <ul>
-                <li style="list-style-type: square">от центра вверх: на весь экран</li>
-                <li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
-                <li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
-                <li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
+                <li style="list-style-type: square">
+                    от центра вверх: на весь экран
+                </li>
+                <li style="list-style-type: square">
+                    от центра вниз: плавный скроллинг
+                </li>
+                <li style="list-style-type: square">
+                    от центра вправо: увеличить скорость скроллинга
+                </li>
+                <li style="list-style-type: square">
+                    от центра влево: уменьшить скорость скроллинга
+                </li>
             </ul>
-
         </ul>
         * Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках
     </div>
@@ -25,17 +32,18 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
 
 import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue';
 
-export default @Component({
+const componentOptions = {
     components: {
         ClickMapPage,
     },
-})
-class MouseHelpPage extends Vue {
+};
+class MouseHelpPage {
+    _options = componentOptions;
+
     created() {
     }
 
@@ -44,6 +52,8 @@ class MouseHelpPage extends Vue {
         this.$refs.clickMapPage.$el.style.backgroundColor = '#478355';
     }
 }
+
+export default vueComponent(MouseHelpPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 7 - 7
client/components/Reader/HelpPage/VersionHistoryPage/VersionHistoryPage.vue

@@ -3,9 +3,9 @@
         <span class="text-h6 text-bold">История версий:</span>
         <br><br>
 
-        <span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
+        <span v-for="(item, index) in versionHeader" :key="index" class="clickable" @click="showRelease(item)">
             <p>
-            {{ item }}
+                {{ item }}
             </p>
         </span>
 
@@ -20,13 +20,11 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
+
 import {versionHistory} from '../../versionHistory';
 
-export default @Component({
-})
-class VersionHistoryPage extends Vue {
+class VersionHistoryPage {
     versionHeader = [];
     versionContent = [];
 
@@ -54,6 +52,8 @@ class VersionHistoryPage extends Vue {
         }
     }
 }
+
+export default vueComponent(VersionHistoryPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 10 - 6
client/components/Reader/LibsPage/LibsPage.vue

@@ -4,14 +4,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import Window from '../../share/Window.vue';
 import * as utils from '../../../share/utils';
 //import rstore from '../../../store/modules/reader';
+import _ from 'lodash';
 
-export default @Component({
+const componentOptions = {
     components: {
         Window
     },
@@ -20,8 +20,10 @@ export default @Component({
             this.sendLibs();
         },
     }    
-})
-class LibsPage extends Vue {
+};
+class LibsPage {
+    _options = componentOptions;
+
     created() {
         this.popupWindow = null;
         this.commit = this.$store.commit;
@@ -113,13 +115,15 @@ class LibsPage extends Vue {
     }
 
     sendLibs() {
-        this.sendMessage({type: 'libs', data: this.libs});
+        this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs)});
     }
 
     close() {
         this.$emit('libs-close');
     }
 }
+
+export default vueComponent(LibsPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 21 - 17
client/components/Reader/LoaderPage/LoaderPage.vue

@@ -1,7 +1,7 @@
 <template>
     <div ref="main" class="column no-wrap" style="min-height: 500px">
         <div v-if="mode != 'liberama.top'" class="relative-position">
-            <GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
+            <GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
         </div>
         <div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
             <span class="greeting"><b>{{ title }}</b></span>
@@ -12,13 +12,13 @@
         </div>
 
         <div class="col-auto column justify-start items-center no-wrap overflow-hidden">
-            <q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
-                <template v-slot:append>
-                    <q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
+            <q-input ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" placeholder="URL книги" @keydown="onInputKeydown">
+                <template #append>
+                    <q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
                 </template>
             </q-input>
 
-            <input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
+            <input id="file" ref="file" type="file" style="display: none;" @change="loadFile" />
 
             <div class="q-my-sm"></div>
             <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
@@ -58,20 +58,22 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
+
 import GithubCorner from './GithubCorner/GithubCorner.vue';
 
 import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
 import {versionHistory} from '../versionHistory';
 
-export default @Component({
+const componentOptions = {
     components: {
         GithubCorner,
         PasteTextPage,
     },
-})
-class LoaderPage extends Vue {
+};
+class LoaderPage {
+    _options = componentOptions;
+
     bookUrl = null;
     loadPercent = 0;
     pasteTextActive = false;
@@ -166,18 +168,18 @@ class LoaderPage extends Vue {
         window.open('http://old.omnireader.ru', '_blank');
     }
 
+    onInputKeydown(event) {
+        if (event.key == 'Enter') {
+            this.submitUrl();
+        }
+    }
+
     keyHook(event) {
         if (this.pasteTextActive) {
             return this.$refs.pasteTextPage.keyHook(event);
         }
 
-        //недостатки сторонних ui
-        const input = this.$refs.input.$refs.input;
-        if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
-            this.submitUrl();
-            return true;
-        }
-
+        const input = this.$refs.input.getNativeElement();
         if (event.type == 'keydown' && document.activeElement !== input) {
             const action = this.$root.readerActionByKeyEvent(event);
             switch (action) {
@@ -190,6 +192,8 @@ class LoaderPage extends Vue {
         return false;
     }
 }
+
+export default vueComponent(LoaderPage);
 //-----------------------------------------------------------------------------
 </script>
 <style scoped>

+ 20 - 13
client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue

@@ -1,6 +1,6 @@
 <template>
     <Window @close="close">
-        <template slot="header">
+        <template #header>
             <span style="position: relative; top: -3px">
                 Вставьте текст и нажмите
                 <span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
@@ -8,27 +8,28 @@
             </span>
         </template>
 
-        <q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
-        <hr/>
+        <q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
+        <hr />
         <textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
     </Window>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
 
 import Window from '../../../share/Window.vue';
 import _ from 'lodash';
 import * as utils from '../../../../share/utils';
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
-})
-class PasteTextPage extends Vue {
+};
+class PasteTextPage {
+    _options = componentOptions;
+
     bookTitle = '';
 
     created() {
@@ -59,15 +60,19 @@ class PasteTextPage extends Vue {
 
     calcTitle(event) {
         if (this.bookTitle == '') {
-            let text = event.clipboardData.getData('text');
-            this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}: ` + _.compact([
-                this.getNonEmptyLine3words(text, 1),
-                this.getNonEmptyLine3words(text, 2)
-            ]).join(' - ');
+            this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`;
+            if (event) {
+                let text = event.clipboardData.getData('text');
+                this.bookTitle += ': ' + _.compact([
+                    this.getNonEmptyLine3words(text, 1),
+                    this.getNonEmptyLine3words(text, 2)
+                ]).join(' - ');
+            }
         }
     }
 
     loadBuffer() {
+        this.calcTitle();
         this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
         this.close();
     }
@@ -90,6 +95,8 @@ class PasteTextPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(PasteTextPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 6 - 6
client/components/Reader/ProgressPage/ProgressPage.vue

@@ -17,7 +17,7 @@
 
             <div>
                 <span class="text-yellow">{{ text }}</span>
-                <q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
+                <q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px" />
             </div>
         </div>
     </div>
@@ -25,8 +25,8 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
+
 import * as utils from '../../../share/utils';
 
 const ruMessage = {
@@ -42,9 +42,7 @@ const ruMessage = {
     'upload': 'отправка',
 };
 
-export default @Component({
-})
-class ProgressPage extends Vue {
+class ProgressPage {
     text = '';
     totalSteps = 1;
     step = 1;
@@ -96,5 +94,7 @@ class ProgressPage extends Vue {
         return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
     }
 }
+
+export default vueComponent(ProgressPage);
 //-----------------------------------------------------------------------------
 </script>

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

@@ -1,75 +1,105 @@
 <template>
     <div class="column no-wrap">
-        <div ref="header" class="header" v-show="toolBarActive">
+        <div v-show="toolBarActive" ref="header" class="header">
             <div ref="buttons" class="row justify-between no-wrap">
                 <div>
-                    <button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
-                        <q-icon name="la la-arrow-left" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
+                    <button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
+                        <q-icon name="la la-arrow-left" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
+                            {{ rstore.readerActions['loader'] }}
+                        </q-tooltip>
                     </button>
                 </div>
 
                 <div>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['undoAction'] }}</q-tooltip>
+                    <button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
+                        <q-icon name="la la-angle-left" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['undoAction'] }}
+                        </q-tooltip>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
+                    <button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
+                        <q-icon name="la la-angle-right" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['redoAction'] }}
+                        </q-tooltip>
                     </button>
                     <div class="space"></div>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['fullScreen'] }}</q-tooltip>
+                    <button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
+                        <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%">
+                            {{ rstore.readerActions['fullScreen'] }}
+                        </q-tooltip>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['scrolling'] }}</q-tooltip>
+                    <button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
+                        <q-icon name="la la-film" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['scrolling'] }}
+                        </q-tooltip>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['setPosition'] }}</q-tooltip>
+                    <button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
+                        <q-icon name="la la-angle-double-right" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['setPosition'] }}
+                        </q-tooltip>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['search'] }}</q-tooltip>
+                    <button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
+                        <q-icon name="la la-search" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['search'] }}
+                        </q-tooltip>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
+                    <button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
+                        <q-icon name="la la-copy" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['copyText'] }}
+                        </q-tooltip>
                     </button>
-                    <button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
-                        <q-icon name="la la-magic" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
+                    <button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
+                        <q-icon name="la la-magic" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['convOptions'] }}
+                        </q-tooltip>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
+                    <button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
+                        <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['refresh'] }}
+                        </q-tooltip>
                     </button>
                     <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 v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
+                        <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>
-                        <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>
+                    <button v-show="mode == 'liberama.top' && showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
+                        <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>
                     </button>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
+                    <button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
+                        <q-icon name="la la-book-open" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['recentBooks'] }}
+                        </q-tooltip>
                     </button>
                 </div>
 
                 <div>
-                    <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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
+                    <button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
+                        <q-icon name="la la-unlink" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                            {{ rstore.readerActions['offlineMode'] }}
+                        </q-tooltip>
                     </button>
-                    <button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
-                        <q-icon name="la la-cog" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
+                    <button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
+                        <q-icon name="la la-cog" size="32px" />
+                        <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">
+                            {{ rstore.readerActions['settings'] }}
+                        </q-tooltip>
                     </button>
                 </div>
             </div>
@@ -77,7 +107,10 @@
 
         <div class="main col row relative-position">
             <keep-alive>
-                <component ref="page" class="col" :is="activePage"
+                <component 
+                    :is="activePage"
+                    ref="page"
+                    class="col"
                     @load-book="loadBook"
                     @load-file="loadFile"
                     @book-pos-changed="bookPosChanged"
@@ -86,21 +119,23 @@
             </keep-alive>
 
             <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" 
                 @do-action="doAction"
                 @book-pos-changed="bookPosChanged"
                 @start-text-search="startTextSearch"
-                @stop-text-search="stopTextSearch">
-            </SearchPage>
+                @stop-text-search="stopTextSearch"
+            ></SearchPage>
             <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
             <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>
             <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>
-            <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
             <ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
 
+            <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
             <ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
         </div>
     </div>
@@ -108,8 +143,8 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
+
 import _ from 'lodash';
 import {Buffer} from 'safe-buffer';
 
@@ -125,9 +160,9 @@ import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
 import SettingsPage from './SettingsPage/SettingsPage.vue';
 import HelpPage from './HelpPage/HelpPage.vue';
 import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
-import ServerStorage from './ServerStorage/ServerStorage.vue';
 import ContentsPage from './ContentsPage/ContentsPage.vue';
 
+import ServerStorage from './ServerStorage/ServerStorage.vue';
 import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
 
 import bookManager from './share/bookManager';
@@ -141,7 +176,7 @@ import miscApi from '../../api/misc';
 import {versionHistory} from './versionHistory';
 import * as utils from '../../share/utils';
 
-export default @Component({
+const componentOptions = {
     components: {
         LoaderPage,
         TextPage,
@@ -155,9 +190,9 @@ export default @Component({
         SettingsPage,
         HelpPage,
         ClickMapPage,
-        ServerStorage,
         ContentsPage,
 
+        ServerStorage,
         ReaderDialogs,
     },
     watch: {
@@ -202,24 +237,30 @@ export default @Component({
                 this.stopScrolling();
         },
     },
-})
-class Reader extends Vue {
+};
+
+class Reader {
+    _options = componentOptions;
+
     rstore = {};
+
     loaderActive = false;
-    offlineModeActive = false;
-    progressActive = false;
     fullScreenActive = false;
-
-    scrollingActive = false;
     setPositionActive = false;
     searchActive = false;
     copyTextActive = false;
+    convOptionsActive = false;
+    refreshActive = false;
+    contentsActive = false;    
     libsActive = false;
     recentBooksActive = false;
+    offlineModeActive = false;
     settingsActive = false;
-    helpActive = false;
+
     clickMapActive = false;
-    contentsActive = false;
+    helpActive = false;
+    scrollingActive = false;
+    progressActive = false;
 
     bookPos = null;
     allowUrlParamBookPos = false;
@@ -243,10 +284,19 @@ class Reader extends Vue {
         this.reader = this.$store.state.reader;
         this.config = this.$store.state.config;
 
-        this.$root.addKeyHook(this.keyHook);
+        this.$root.addEventHook('key', this.keyHook);
 
         this.lastActivePage = false;
 
+        this.$watch(
+            () => this.$route.path,
+            (newValue) => {
+                if (newValue == '/reader') {
+                    this.updateRoute();
+                }
+            }
+        );
+
         this.debouncedSetRecentBook = _.debounce(async(newValue) => {
             const recent = this.mostRecentBook();
             if (recent && (recent.bookPos != newValue || recent.bookPosSeen !== this.bookPosSeen)) {
@@ -281,7 +331,7 @@ class Reader extends Vue {
             await bookManager.init(this.settings);
             bookManager.addEventListener(this.bookManagerEvent);
 
-            if (this.$root.rootRoute() == '/reader') {
+            if (this.$root.getRootRoute() == '/reader') {
                 if (this.routeParamUrl) {
                     await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
                 } else {
@@ -566,7 +616,7 @@ class Reader extends Vue {
 
     toolBarToggle() {
         this.commit('reader/setToolBarActive', !this.toolBarActive);
-        this.$root.$emit('resize');
+        this.$root.eventHook('resize');
     }
 
     fullScreenToggle() {
@@ -876,7 +926,7 @@ class Reader extends Vue {
     }
 
     get activePage() {
-        let result = '';
+        let result = undefined;
 
         if (this.progressActive)
             result = 'ProgressPage';
@@ -891,7 +941,7 @@ class Reader extends Vue {
         }
 
         if (result != 'TextPage') {
-            this.$root.$emit('set-app-title');
+            this.$root.setAppTitle();
         }
 
         // на LoaderPage всегда показываем toolBar
@@ -906,7 +956,7 @@ class Reader extends Vue {
                 const isParsed = await bookManager.hasBookParsed(last);
 
                 if (!isParsed) {
-                    this.$root.$emit('set-app-title');
+                    this.$root.setAppTitle();
                     return;
                 }
 
@@ -1232,7 +1282,7 @@ class Reader extends Vue {
 
     keyHook(event) {
         let result = false;
-        if (this.$root.rootRoute() == '/reader') {
+        if (this.$root.getRootRoute() == '/reader') {
             if (this.$root.stdDialog.active)
                 return result;
 
@@ -1278,6 +1328,8 @@ class Reader extends Vue {
         return result;
     }
 }
+
+export default vueComponent(Reader);
 //-----------------------------------------------------------------------------
 </script>
 

+ 16 - 9
client/components/Reader/ReaderDialogs/ReaderDialogs.vue

@@ -1,7 +1,7 @@
 <template>
     <div>
         <Dialog ref="dialog1" v-model="whatsNewVisible">
-            <template slot="header">
+            <template #header>
                 Что нового:
             </template>
 
@@ -14,7 +14,7 @@
         </Dialog>
 
         <Dialog ref="dialog2" v-model="donationVisible">
-            <template slot="header">
+            <template #header>
                 Здравствуйте, уважаемые читатели!
             </template>
 
@@ -35,7 +35,9 @@
                 Автор также обращается с просьбой о помощи в распространении 
                 <a href="https://omnireader.ru" target="_blank">ссылки</a>
                 <q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
-                    <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                    <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
+                        Скопировать
+                    </q-tooltip>                    
                 </q-icon>
                 на читалку через тематические форумы, соцсети, мессенджеры и пр.
                 Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
@@ -47,7 +49,9 @@
 
                 <br><br>
                 <div class="row justify-center">
-                    <q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
+                    <q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
+                        Помочь проекту
+                    </q-btn>
                 </div>
             </div>
 
@@ -62,14 +66,13 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import Dialog from '../../share/Dialog.vue';
 import * as utils from '../../../share/utils';
 import {versionHistory} from '../versionHistory';
 
-export default @Component({
+const componentOptions = {
     components: {
         Dialog
     },
@@ -78,8 +81,10 @@ export default @Component({
             this.loadSettings();
         },
     },
-})
-class ReaderDialogs extends Vue {
+};
+class ReaderDialogs {
+    _options = componentOptions;
+
     whatsNewVisible = false;
     whatsNewContent = '';
     donationVisible = false;
@@ -181,6 +186,8 @@ class ReaderDialogs extends Vue {
         return false;
     }
 }
+
+export default vueComponent(ReaderDialogs);
 //-----------------------------------------------------------------------------
 </script>
 

+ 42 - 30
client/components/Reader/RecentBooksPage/RecentBooksPage.vue

@@ -1,35 +1,41 @@
 <template>
-    <Window width="600px" ref="window" @close="close">
-        <template slot="header">
+    <Window ref="window" width="600px" @close="close">
+        <template #header>
             <span v-show="!loading">{{ header }}</span>
-            <span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
+            <span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7" />
+                Список загружается
+            </span>
         </template>
 
-        <a ref="download" style='display: none;' target="_blank"></a>
+        <a ref="download" style="display: none;" target="_blank"></a>
 
         <q-table
             class="recent-books-table col"
-            :data="tableData"
-            :columns="columns"
+            :rows="tableData"
             row-key="key"
-            :pagination.sync="pagination"
+            :columns="columns"
+            :pagination="pagination"
             separator="cell"
             hide-bottom
             virtual-scroll
             dense
         > 
-            <template v-slot:header="props">
+            <template #header="props">
                 <q-tr :props="props">
-                    <q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
-                    <q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
-                    <q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
-                        <q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
-                            placeholder="Найти"
-                            v-model="search"
+                    <q-th key="num" class="td-mp" style="width: 25px" :props="props">
+                        <span v-html="props.cols[0].label"></span>
+                    </q-th>
+                    <q-th key="date" class="td-mp break-word" style="width: 77px" :props="props">
+                        <span v-html="props.cols[1].label"></span>
+                    </q-th>
+                    <q-th key="desc" class="td-mp" style="width: 332px" :props="props" colspan="4">
+                        <q-input ref="input" v-model="search"
+                            outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
+                            placeholder="Найти"                            
                             @click.stop
                         >
-                            <template v-slot:append>
-                                <q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch"/>
+                            <template #append>
+                                <q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
                             </template>
                         </q-input>
                         <span v-html="props.cols[2].label"></span>
@@ -37,7 +43,7 @@
                 </q-tr>
             </template>
 
-            <template v-slot:body="props">
+            <template #body="props">
                 <q-tr :props="props">
                     <q-td key="num" :props="props" class="td-mp" auto-width>
                         <div class="break-word" style="width: 25px">
@@ -45,16 +51,18 @@
                         </div>
                     </q-td>
 
-                    <q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
+                    <q-td key="date" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
                         <div class="break-word" style="width: 68px">
                             {{ props.row.touchDate }}<br>
                             {{ props.row.touchTime }}
                         </div>
                     </q-td>
 
-                    <q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
+                    <q-td key="desc" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
                         <div class="break-word" style="width: 332px; font-size: 90%">
-                            <div style="color: green">{{ props.row.desc.author }}</div>
+                            <div style="color: green">
+                                {{ props.row.desc.author }}
+                            </div>
                             <div>{{ props.row.desc.title }}</div>
                             <div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
                         </div>
@@ -72,8 +80,9 @@
                             <q-btn
                                 dense
                                 style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
-                                @click="handleDel(props.row.key)">
-                                <q-icon class="la la-times" size="14px" style="top: -6px"/>
+                                @click="handleDel(props.row.key)"
+                            >
+                                <q-icon class="la la-times" size="14px" />
                             </q-btn>
                         </div>
                     </q-td>
@@ -82,15 +91,14 @@
                 </q-tr>
             </template>
         </q-table>
-
     </Window>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
-import path from 'path';
+import vueComponent from '../../vueComponent.js';
+
+import path from 'path-browserify';
 //import _ from 'lodash';
 
 import * as utils from '../../../share/utils';
@@ -98,7 +106,7 @@ import Window from '../../share/Window.vue';
 import bookManager from '../share/bookManager';
 import readerApi from '../../../api/reader';
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
@@ -107,8 +115,10 @@ export default @Component({
             this.updateTableData();
         }
     },
-})
-class RecentBooksPage extends Vue {
+};
+class RecentBooksPage {
+    _options = componentOptions;
+
     loading = false;
     search = '';
     tableData = [];
@@ -171,13 +181,13 @@ class RecentBooksPage extends Vue {
                 return;
             this.initing = true;
 
-
             if (this.firstInit) {//для отзывчивости
                 await this.updateTableData(20);
                 this.firstInit = false;
             }
             await utils.sleep(50);
             await this.updateTableData();
+
             this.initing = false;
         })();
     }
@@ -324,6 +334,8 @@ class RecentBooksPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(RecentBooksPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 21 - 11
client/components/Reader/SearchPage/SearchPage.vue

@@ -1,6 +1,6 @@
 <template>
     <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
-        <template slot="header">
+        <template #header>
             {{ header }}
         </template>
 
@@ -11,15 +11,22 @@
                 <!--input ref="input"
                     placeholder="что ищем"
                     :value="needle" @input="needle = $event.target.value"/-->
-                <q-input ref="input" class="col" outlined dense
+                <q-input ref="input" v-model="needle"
+                    class="col" outlined dense
                     placeholder="что ищем"
-                    v-model="needle" @keydown="inputKeyDown"
+                    @keydown="inputKeyDown"         
                 />
-                <div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
+                <div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
+                    {{ foundText }}
+                </div>
             </div>
             <q-btn-group v-show="!initStep" class="button-group row no-wrap">
-                <q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
-                <q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
+                <q-btn class="button" dense stretch @click="showNext">
+                    <q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
+                </q-btn>
+                <q-btn class="button" dense stretch @click="showPrev">
+                    <q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
+                </q-btn>
             </q-btn-group>
         </div>
     </Window>
@@ -27,13 +34,12 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import Window from '../../share/Window.vue';
 import {sleep} from '../../../share/utils';
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
@@ -49,8 +55,10 @@ export default @Component({
                 el.style.paddingRight = newValue.length*12 + 'px';
         },
     },
-})
-class SearchPage extends Vue {
+};
+class SearchPage {
+    _options = componentOptions;
+
     header = null;
     initStep = null;
     initPercentage = 0;
@@ -180,6 +188,8 @@ class SearchPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(SearchPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 10 - 6
client/components/Reader/ServerStorage/ServerStorage.vue

@@ -4,8 +4,8 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
+
 import _ from 'lodash';
 
 import bookManager from '../share/bookManager';
@@ -18,7 +18,7 @@ const ssCacheStore = localForage.createInstance({
     name: 'ssCacheStore'
 });
 
-export default @Component({
+const componentOptions = {
     watch: {
         serverSyncEnabled: function() {
             this.serverSyncEnabledChanged();
@@ -39,14 +39,16 @@ export default @Component({
             this.debouncedSaveLibs();
         },
     },
-})
-class ServerStorage extends Vue {
+};
+class ServerStorage {
+    _options = componentOptions;
+
     created() {
         this.inited = false;
         this.keyInited = false;
         this.commit = this.$store.commit;
         this.prevServerStorageKey = null;
-        this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
+        this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
 
         this.debouncedSaveSettings = _.debounce(() => {
             this.saveSettings();
@@ -734,5 +736,7 @@ class ServerStorage extends Vue {
         return result;
     }
 }
+
+export default vueComponent(ServerStorage);
 //-----------------------------------------------------------------------------
 </script>

+ 12 - 8
client/components/Reader/SetPositionPage/SetPositionPage.vue

@@ -1,16 +1,17 @@
 <template>
     <Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
-        <template slot="header">
+        <template #header>
             Установить позицию
         </template>
 
         <div id="set-position-slider" class="slider q-px-md">
             <q-slider
-                thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
                 v-model="sliderValue"
+                thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
+                
                 :max="sliderMax"
                 label
-                :label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
+                :label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
                 color="primary"
             />
         </div>
@@ -19,12 +20,11 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
 
 import Window from '../../share/Window.vue';
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
     },
@@ -34,8 +34,10 @@ export default @Component({
                 this.$emit('book-pos-changed', {bookPos: newValue});
         },
     },
-})
-class SetPositionPage extends Vue {
+};
+class SetPositionPage {
+    _options = componentOptions;
+
     sliderValue = null;
     sliderMax = null;
 
@@ -67,6 +69,8 @@ class SetPositionPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(SetPositionPage);
 //-----------------------------------------------------------------------------
 </script>
 

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

@@ -3,8 +3,7 @@
 <div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
     <div class="label-3"></div>
     <div class="col row">
-        <q-checkbox size="xs" @input="changeShowToolButton(item.name)"
-            :value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
+        <q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
         />
     </div>
 </div>

+ 0 - 0
client/components/Reader/SettingsPage/include/ConvertTab.inc → client/components/Reader/SettingsPage/ConvertTab.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/KeysTab.inc → client/components/Reader/SettingsPage/KeysTab.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/OthersTab.inc → client/components/Reader/SettingsPage/OthersTab.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/PageMoveTab.inc → client/components/Reader/SettingsPage/PageMoveTab.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/ProfilesTab.inc → client/components/Reader/SettingsPage/ProfilesTab.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/ResetTab.inc → client/components/Reader/SettingsPage/ResetTab.inc


+ 84 - 41
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -1,6 +1,6 @@
-<template lang="includer">
+<template>
     <Window ref="window" height="95%" width="600px" @close="close">
-        <template slot="header">
+        <template #header>
             Настройки
         </template>
 
@@ -8,8 +8,9 @@
             <div class="full-height">
                 <q-tabs
                     ref="tabs"
-                    class="bg-grey-3 text-black"
                     v-model="selectedTab"
+                    class="bg-grey-3 text-black"
+                    
                     left-icon="la la-caret-up"
                     right-icon="la la-caret-down"
                     active-color="white"
@@ -20,7 +21,7 @@
                     stretch
                     inline-label
                 >
-                    <div v-show="tabsScrollable" class="q-pt-lg"/>
+                    <div v-show="tabsScrollable" class="q-pt-lg" />
                     <q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
                     <q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
                     <q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
@@ -29,53 +30,91 @@
                     <q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
                     <q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
                     <q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
-                    <div v-show="tabsScrollable" class="q-pt-lg"/>
+                    <div v-show="tabsScrollable" class="q-pt-lg" />
                 </q-tabs>
             </div>
 
             <div class="col fit">
                 <!-- Профили --------------------------------------------------------------------->
                 <div v-if="selectedTab == 'profiles'" class="fit tab-panel">
-                    @@include('./include/ProfilesTab.inc');
+                    @@include('./ProfilesTab.inc');
                 </div>
                 <!-- Вид ------------------------------------------------------------------------->                    
                 <div v-if="selectedTab == 'view'" class="fit column">
-                    @@include('./include/ViewTab.inc');
+                    <q-tabs
+                        v-model="selectedViewTab"
+                        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="mode" label="Режим" />
+                        <q-tab name="color" label="Цвет" />
+                        <q-tab name="font" label="Шрифт" />
+                        <q-tab name="text" label="Текст" />
+                        <q-tab name="status" label="Строка статуса" />
+                    </q-tabs>
+
+                    <div class="q-mb-sm" />
+
+                    <div class="col tab-panel">
+                        <div v-if="selectedViewTab == 'mode'">
+                            @@include('./ViewTab/Mode.inc');
+                        </div>
+
+                        <div v-if="selectedViewTab == 'color'">
+                            @@include('./ViewTab/Color.inc');
+                        </div>
+
+                        <div v-if="selectedViewTab == 'font'">
+                            @@include('./ViewTab/Font.inc');
+                        </div>
+
+                        <div v-if="selectedViewTab == 'text'">
+                            @@include('./ViewTab/Text.inc');
+                        </div>
+
+                        <div v-if="selectedViewTab == 'status'">
+                            @@include('./ViewTab/Status.inc');
+                        </div>
+                    </div>
                 </div>
                 <!-- Кнопки ---------------------------------------------------------------------->
                 <div v-if="selectedTab == 'buttons'" class="fit tab-panel">
-                    @@include('./include/ButtonsTab.inc');
+                    @@include('./ButtonsTab.inc');
                 </div>
                 <!-- Управление ------------------------------------------------------------------>
                 <div v-if="selectedTab == 'keys'" class="fit column">
-                    @@include('./include/KeysTab.inc');
+                    @@include('./KeysTab.inc');
                 </div>
                 <!-- Листание -------------------------------------------------------------------->
                 <div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
-                    @@include('./include/PageMoveTab.inc');
+                    @@include('./PageMoveTab.inc');
                 </div>
                 <!-- Конвертирование ------------------------------------------------------------->
                 <div v-if="selectedTab == 'convert'" class="fit tab-panel">
-                    @@include('./include/ConvertTab.inc');
+                    @@include('./ConvertTab.inc');
                 </div>
                 <!-- Прочее ---------------------------------------------------------------------->
                 <div v-if="selectedTab == 'others'" class="fit tab-panel">
-                    @@include('./include/OthersTab.inc');
+                    @@include('./OthersTab.inc');
                 </div>
                 <!-- Сброс ----------------------------------------------------------------------->
                 <div v-if="selectedTab == 'reset'" class="fit tab-panel">
-                    @@include('./include/ResetTab.inc');
+                    @@include('./ResetTab.inc');
                 </div>
             </div>
-
         </div>
     </Window>
 </template>
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import { ref, watch } from 'vue';
+import vueComponent from '../../vueComponent.js';
+
 import _ from 'lodash';
 
 import * as utils from '../../../share/utils';
@@ -90,7 +129,7 @@ import defPalette from './defPalette';
 
 const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
 
-export default @Component({
+const componentOptions = {
     components: {
         Window,
         NumInput,
@@ -104,8 +143,9 @@ export default @Component({
             this.settingsChanged();
         },
         form: function(newValue) {
-            if (this.inited)
-                this.commit('reader/setSettings', newValue);
+            if (this.inited) {
+                this.commit('reader/setSettings', _.cloneDeep(newValue));
+            }
         },
         fontBold: function(newValue) {
             this.fontWeight = (newValue ? 'bold' : '');
@@ -165,12 +205,13 @@ export default @Component({
                 this.statusBarColor = newValue;
         },
     },
-})
-class SettingsPage extends Vue {
+};
+class SettingsPage {
+    _options = componentOptions;
+
     selectedTab = 'profiles';
     selectedViewTab = 'mode';
     selectedKeysTab = 'mouse';
-    form = {};
     fontBold = false;
     fontItalic = false;
     vertShift = 0;
@@ -186,6 +227,19 @@ class SettingsPage extends Vue {
     toolButtons = [];
     rstore = {};
 
+    setup() {
+        const settingsProps = { form: ref({}) };
+
+        for (let prop in rstore.settingDefaults) {
+            settingsProps[prop] = ref(_.cloneDeep(rstore.settingDefaults[prop]));
+            watch(settingsProps[prop], (newValue) => {
+                settingsProps.form.value = Object.assign({}, settingsProps.form.value, {[prop]: newValue});
+            }, {deep: true});
+        }
+
+        return settingsProps;
+    }
+
     created() {
         this.commit = this.$store.commit;
         this.reader = this.$store.state.reader;
@@ -200,7 +254,7 @@ class SettingsPage extends Vue {
         this.$watch(
             '$refs.tabs.scrollable',
             (newValue) => {
-                this.tabsScrollable = newValue && !this.$isMobileDevice;
+                this.tabsScrollable = newValue && !this.$root.isMobileDevice;
             }
         );
     }
@@ -215,18 +269,8 @@ class SettingsPage extends Vue {
             return;
 
         this.form = Object.assign({}, this.settings);
-        if (!this.unwatch)
-            this.unwatch = {};
-
-        for (let prop in rstore.settingDefaults) {
-            if (this.unwatch && this.unwatch[prop])
-                this.unwatch[prop]();
-
-            this[prop] = this.form[prop];
-
-            this.unwatch[prop] = this.$watch(prop, (newValue) => {
-                this.form = Object.assign({}, this.form, {[prop]: newValue});
-            });
+        for (const prop in rstore.settingDefaults) {
+            this[prop] = _.cloneDeep(this.form[prop]);
         }
 
         this.fontBold = (this.fontWeight == 'bold');
@@ -421,10 +465,6 @@ class SettingsPage extends Vue {
         }
     }
 
-    changeShowToolButton(buttonName) {
-        this.showToolButton = Object.assign({}, this.showToolButton, {[buttonName]: !this.showToolButton[buttonName]});
-    }
-
     async addProfile() {
         try {
             if (Object.keys(this.profiles).length >= 100) {
@@ -457,7 +497,7 @@ class SettingsPage extends Vue {
             return;
 
         try {
-            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` +
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
                     `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
                     `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
@@ -550,7 +590,8 @@ class SettingsPage extends Vue {
             });
 
             if (result && result.value && result.value.toLowerCase() == 'да') {
-                this.$root.$emit('generateNewServerStorageKey');
+                if (this.$root.generateNewServerStorageKey)
+                    this.$root.generateNewServerStorageKey();
             }
         } catch (e) {
             //
@@ -630,6 +671,8 @@ class SettingsPage extends Vue {
         return true;
     }
 }
+
+export default vueComponent(SettingsPage);
 //-----------------------------------------------------------------------------
 </script>
 

+ 47 - 38
client/components/Reader/SettingsPage/UserHotKeys/UserHotKeys.vue

@@ -2,14 +2,20 @@
     <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="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"
+                <div style="width: 80px">
+                    Сочетание клавиш
+                </div>
+                <q-input
+                    ref="input"
+                    v-model="search"
+                    class="q-ml-sm col"
                     outlined dense rounded
                     bg-color="grey-4"
-                    placeholder="Найти"
-                    v-model="search"
+                    placeholder="Найти"                    
                     @click.stop
                 />
                 <div v-show="!readonly" class="q-ml-sm column justify-center">
@@ -23,35 +29,38 @@
         </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 v-for="(action, index) in tableData" :key="index" class="table-row row">
+            <div class="desc q-pa-sm">
+                {{ rstore.readerActions[action] }}
+            </div>
             <div class="hotKeys col q-pa-sm">
                 <q-chip
+                    v-for="(code, index2) in modelValue[action]" :key="index2"
                     :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)"
+                    text-color="white" @remove="removeCode(action, code)"
                     @click="collisionWarning(code)"
-                    >
+                >
                     {{ code }}
                 </q-chip>
             </div>
             <div v-show="!readonly" class="column q-pa-xs">
                 <q-icon
+                    v-ripple
+                    :disabled="(modelValue[action].length >= maxCodesLength) || null"
                     name="la la-plus-circle"
                     class="button bg-green-8 text-white"
-                    @click="addHotKey(action)"
-                    v-ripple
-                    :disabled="value[action].length >= maxCodesLength"
+                    @click="addHotKey(action)"                    
                 >
                     <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
                         Добавить сочетание клавиш
                     </q-tooltip>
                 </q-icon>
                 <q-icon
+                    v-ripple
                     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%">
                         По умолчанию
@@ -64,31 +73,29 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../../vueComponent.js';
 
 import rstore from '../../../../store/modules/reader';
 //import * as utils from '../../share/utils';
 
-const UserHotKeysProps = Vue.extend({
-    props: {
-        value: Object,
-        readonly: Boolean,
-    }
-});
-
-export default @Component({
+const componentOptions = {
     watch: {
         search: function() {
             this.updateTableData();
         },
-        value: function() {
+        modelValue: function() {
             this.checkCollisions();
             this.updateTableData();
         }
     },
-})
-class UserHotKeys extends UserHotKeysProps {
+};
+class UserHotKeys {
+    _options = componentOptions;
+    _props = {
+        modelValue: Object,
+        readonly: Boolean,
+    };
+
     search = '';
     rstore = {};
     tableData = [];
@@ -113,7 +120,7 @@ class UserHotKeys extends UserHotKeysProps {
 
         const search = this.search.toLowerCase();
         const codesIncludeSearch = (action) => {
-            for (const code of this.value[action]) {
+            for (const code of this.modelValue[action]) {
                 if (code.toLowerCase().includes(search))
                     return true;
             }
@@ -131,7 +138,7 @@ class UserHotKeys extends UserHotKeysProps {
 
     checkCollisions() {
         const cols = {};
-        for (const [action, codes] of Object.entries(this.value)) {
+        for (const [action, codes] of Object.entries(this.modelValue)) {
             codes.forEach(code => {
                 if (!cols[code])
                     cols[code] = [];
@@ -158,26 +165,26 @@ class UserHotKeys extends UserHotKeysProps {
     }
 
     removeCode(action, code) {
-        let codes = Array.from(this.value[action]);
+        let codes = Array.from(this.modelValue[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);
+            const newValue = Object.assign({}, this.modelValue, {[action]: codes});
+            this.$emit('update:modelValue', newValue);
         }
     }
 
     async addHotKey(action) {
-        if (this.value[action].length >= this.maxCodesLength)
+        if (this.modelValue[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]);
+                let codes = Array.from(this.modelValue[action]);
                 if (codes.indexOf(result) < 0) {
                     codes.push(result);
-                    const newValue = Object.assign({}, this.value, {[action]: codes});
-                    this.$emit('input', newValue);
+                    const newValue = Object.assign({}, this.modelValue, {[action]: codes});
+                    this.$emit('update:modelValue', newValue);
                     this.$nextTick(() => {
                         this.collisionWarning(result);
                     });
@@ -192,8 +199,8 @@ class UserHotKeys extends UserHotKeysProps {
         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);
+                const newValue = Object.assign({}, this.modelValue, {[action]: codes});
+                this.$emit('update:modelValue', newValue);
             }
         } catch (e) {
             //
@@ -204,13 +211,15 @@ class UserHotKeys extends UserHotKeysProps {
         try {
             if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
                 const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
-                this.$emit('input', newValue);
+                this.$emit('update:modelValue', newValue);
             }
         } catch (e) {
             //
         }
     }
 }
+
+export default vueComponent(UserHotKeys);
 //-----------------------------------------------------------------------------
 </script>
 

+ 116 - 0
client/components/Reader/SettingsPage/ViewTab/Color.inc

@@ -0,0 +1,116 @@
+                            <!---------------------------------------------->
+                            <div class="hidden part-header">
+                                Цвет
+                            </div>
+
+                            <div class="item row">
+                                <div class="label-2">
+                                    Текст
+                                </div>
+                                <div class="col row">
+                                    <q-input
+                                        v-model="textColorFiltered"
+                                        class="col-left no-mp"
+                                        outlined dense
+                                        
+                                        :rules="['hexColor']"
+                                        style="max-width: 150px"
+                                    >
+                                        <template #prepend>
+                                            <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
+                                                <q-popup-proxy anchor="bottom middle" self="top middle">
+                                                    <div>
+                                                        <q-color
+                                                            v-model="textColor"
+                                                            no-header default-view="palette" :palette="predefineTextColors"
+                                                        />
+                                                    </div>
+                                                </q-popup-proxy>
+                                            </q-icon>
+                                        </template>
+                                    </q-input>
+                                </div>
+                            </div>
+
+                            <div class="q-mt-md" />
+                            <div class="item row">
+                                <div class="label-2">
+                                    Фон
+                                </div>
+                                <div class="col row">
+                                    <q-input 
+                                        v-model="bgColorFiltered"
+                                        class="col-left no-mp"
+                                        outlined dense
+                                        
+                                        :rules="['hexColor']"
+                                        style="max-width: 150px"
+                                    >
+                                        <template #prepend>
+                                            <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
+                                                <q-popup-proxy anchor="bottom middle" self="top middle">
+                                                    <div>
+                                                        <q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors" />
+                                                    </div>
+                                                </q-popup-proxy>
+                                            </q-icon>
+                                        </template>
+                                    </q-input>
+                                </div>
+                            </div>
+
+                            <div class="q-mt-md" />
+                            <div class="item row">
+                                <div class="label-2">
+                                    Обои
+                                </div>
+                                <div class="col row items-center">
+                                    <q-select 
+                                        v-model="wallpaper"
+                                        class="col-left no-mp"
+                                        :options="wallpaperOptions"
+                                        dropdown-icon="la la-angle-down la-sm"
+                                        outlined dense emit-value map-options
+                                    >
+                                        <template #selected-item="scope">
+                                            <div>
+                                                {{ scope.opt.label }}
+                                            </div>
+                                            <div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
+                                        </template>
+
+                                        <template #option="scope">
+                                            <q-item
+                                                v-bind="scope.itemProps"
+                                            >
+                                                <q-item-section style="min-width: 50px;">
+                                                    <q-item-label v-html="scope.opt.label" />
+                                                </q-item-section>
+                                                <q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
+                                            </q-item>
+                                        </template>
+                                    </q-select>
+
+                                    <div class="q-px-xs" />
+                                    <q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
+                                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                            Добавить файл обоев
+                                        </q-tooltip>
+                                    </q-btn>
+                                    <q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
+                                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                                            Удалить выбранные обои
+                                        </q-tooltip>
+                                    </q-btn>
+                                </div>
+                            </div>
+
+                            <div class="q-mt-sm" />
+                            <div class="item row">
+                                <div class="label-2"></div>
+                                <div class="col row items-center">
+                                    <q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
+                                </div>
+                            </div>
+
+                            <input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />

+ 0 - 0
client/components/Reader/SettingsPage/include/ViewTab/Font.inc → client/components/Reader/SettingsPage/ViewTab/Font.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/ViewTab/Mode.inc → client/components/Reader/SettingsPage/ViewTab/Mode.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/ViewTab/Status.inc → client/components/Reader/SettingsPage/ViewTab/Status.inc


+ 0 - 0
client/components/Reader/SettingsPage/include/ViewTab/Text.inc → client/components/Reader/SettingsPage/ViewTab/Text.inc


+ 0 - 39
client/components/Reader/SettingsPage/include/ViewTab.inc

@@ -1,39 +0,0 @@
-<q-tabs
-    v-model="selectedViewTab"
-    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="mode" label="Режим" />
-    <q-tab name="color" label="Цвет" />
-    <q-tab name="font" label="Шрифт" />
-    <q-tab name="text" label="Текст" />
-    <q-tab name="status" label="Строка статуса" />
-</q-tabs>
-
-<div class="q-mb-sm"/>
-
-<div class="col tab-panel">
-    <div v-if="selectedViewTab == 'mode'">
-        @@include('./ViewTab/Mode.inc');
-    </div>
-
-    <div v-if="selectedViewTab == 'color'">
-        @@include('./ViewTab/Color.inc');
-    </div>
-
-    <div v-if="selectedViewTab == 'font'">
-        @@include('./ViewTab/Font.inc');
-    </div>
-
-    <div v-if="selectedViewTab == 'text'">
-        @@include('./ViewTab/Text.inc');
-    </div>
-
-    <div v-if="selectedViewTab == 'status'">
-        @@include('./ViewTab/Status.inc');
-    </div>
-</div>

+ 0 - 95
client/components/Reader/SettingsPage/include/ViewTab/Color.inc

@@ -1,95 +0,0 @@
-<!---------------------------------------------->
-<div class="hidden part-header">Цвет</div>
-
-<div class="item row">
-    <div class="label-2">Текст</div>
-    <div class="col row">
-        <q-input class="col-left no-mp"
-            outlined dense
-            v-model="textColorFiltered"
-            :rules="['hexColor']"
-            style="max-width: 150px"
-        >
-            <template v-slot:prepend>
-                <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
-                    <q-popup-proxy anchor="bottom middle" self="top middle">
-                        <div>
-                            <q-color v-model="textColor"
-                                no-header default-view="palette" :palette="predefineTextColors"
-                            />
-                        </div>
-                    </q-popup-proxy>
-                </q-icon>
-            </template>
-        </q-input>
-    </div>
-</div>
-
-<div class="q-mt-md"/>
-<div class="item row">
-    <div class="label-2">Фон</div>
-    <div class="col row">
-        <q-input class="col-left no-mp"
-            outlined dense
-            v-model="bgColorFiltered"
-            :rules="['hexColor']"
-            style="max-width: 150px"
-        >
-            <template v-slot:prepend>
-                <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
-                    <q-popup-proxy anchor="bottom middle" self="top middle">
-                        <div>
-                            <q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
-                        </div>
-                    </q-popup-proxy>
-                </q-icon>
-            </template>
-        </q-input>
-    </div>
-</div>
-
-<div class="q-mt-md"/>
-<div class="item row">
-    <div class="label-2">Обои</div>
-    <div class="col row items-center">
-        <q-select class="col-left no-mp" v-model="wallpaper" :options="wallpaperOptions"
-            dropdown-icon="la la-angle-down la-sm"
-            outlined dense emit-value map-options
-        >
-            <template v-slot:selected-item="scope">
-                <div >{{ scope.opt.label }}</div>
-                <div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
-            </template>
-
-            <template v-slot:option="scope">
-                <q-item
-                    v-bind="scope.itemProps"
-                    v-on="scope.itemEvents"
-                >
-                    <q-item-section style="min-width: 50px;">
-                        <q-item-label v-html="scope.opt.label" />
-                    </q-item-section>
-                    <q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;"/>
-                </q-item>
-            </template>
-        </q-select>
-
-        <div class="q-px-xs"/>
-        <q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
-            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить файл обоев</q-tooltip>
-        </q-btn>
-        <q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
-            <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить выбранные обои</q-tooltip>
-        </q-btn>
-    </div>
-</div>
-
-<div class="q-mt-sm"/>
-<div class="item row">
-    <div class="label-2"></div>
-    <div class="col row items-center">
-        <q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
-    </div>
-</div>
-
-<input type="file" ref="file" @change="loadWallpaperFile" style='display: none;'/>

+ 31 - 20
client/components/Reader/TextPage/TextPage.vue

@@ -17,15 +17,22 @@
         <div v-show="showStatusBar" ref="statusBar" class="layout">
             <div v-html="statusBar"></div>
         </div>
-        <div v-show="clickControl" ref="layoutEvents" class="layout events" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
+        <div v-show="clickControl" ref="layoutEvents" class="layout events" 
+            oncontextmenu="return false;"
+            @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
             @wheel.prevent.stop="onMouseWheel"
-            @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
-            oncontextmenu="return false;">
-            <div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
-                @click.prevent.stop="onStatusBarClick"></div>
+            @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"            
+        >
+            <div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
+                @click.prevent.stop="onStatusBarClick"
+                v-html="statusBarClickable"
+            ></div>
         </div>
-        <div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
-            @click.prevent.stop="onStatusBarClick">
+        <div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" 
+            @mousedown.prevent.stop @touchstart.stop
+            @click.prevent.stop="onStatusBarClick"
+            v-html="statusBarClickable"
+        >
         </div>
         <!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
         <canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
@@ -35,8 +42,8 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../../vueComponent.js';
+
 import {loadCSS} from 'fg-loadcss';
 import _ from 'lodash';
 
@@ -51,7 +58,7 @@ import {clickMap} from '../share/clickMap';
 
 const minLayoutWidth = 100;
 
-export default @Component({
+const componentOptions = {
     watch: {
         bookPos: function() {
             this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -70,8 +77,10 @@ export default @Component({
             this.updateLayout();
         },
     },
-})
-class TextPage extends Vue {
+};
+class TextPage {
+    _options = componentOptions;
+
     toggleLayout = false;
     showStatusBar = false;
     clickControl = true;
@@ -140,7 +149,7 @@ class TextPage extends Vue {
             await this.doPageAnimation();
         }, 10);
 
-        this.$root.$on('resize', async() => {
+        this.$root.addEventHook('resize', async() => {
             this.$nextTick(this.onResize);
             await utils.sleep(500);
             this.$nextTick(this.onResize);
@@ -436,7 +445,7 @@ class TextPage extends Vue {
 
                     this.title = bt.fullTitle;
 
-                    this.$root.$emit('set-app-title', this.title);
+                    this.$root.setAppTitle(this.title);
 
                     this.parsed = this.book.parsed;
 
@@ -1036,7 +1045,7 @@ class TextPage extends Vue {
     }
 
     onTouchStart(event) {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         this.endClickRepeat();
 
@@ -1064,7 +1073,7 @@ class TextPage extends Vue {
     }
 
     onTouchEnd(event) {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         this.endClickRepeat();
 
@@ -1100,13 +1109,13 @@ class TextPage extends Vue {
     }
 
     onTouchCancel() {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         this.endClickRepeat();
     }
 
     onMouseDown(event) {
-        if (this.$isMobileDevice)
+        if (this.$root.isMobileDevice)
             return;
         this.endClickRepeat();
         if (event.button == 0) {
@@ -1123,13 +1132,13 @@ class TextPage extends Vue {
     }
 
     onMouseUp() {
-        if (this.$isMobileDevice)
+        if (this.$root.isMobileDevice)
             return;
         this.endClickRepeat();
     }
 
     onMouseWheel(event) {
-        if (this.$isMobileDevice)
+        if (this.$root.isMobileDevice)
             return;
         if (event.deltaY > 0) {
             this.doDown();
@@ -1195,6 +1204,8 @@ class TextPage extends Vue {
    }
 
 }
+
+export default vueComponent(TextPage);
 //-----------------------------------------------------------------------------
 </script>
 <style scoped>

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

@@ -1,4 +1,15 @@
 export const versionHistory = [
+{
+    showUntil: '2021-11-17',
+    header: '0.11.0 (2021-11-18)',
+    content:
+`
+<ul>
+    <li>переход на Vue 3</li>
+</ul>
+`
+},
+
 {
     showUntil: '2021-10-23',
     header: '0.10.3 (2021-10-24)',

+ 4 - 5
client/components/Settings/Settings.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-})
-class Settings extends Vue {
+class Settings {
     created() {
     }
 
 }
+
+export default vueComponent(Settings);
 //-----------------------------------------------------------------------------
 </script>

+ 4 - 5
client/components/Sources/Sources.vue

@@ -6,15 +6,14 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-})
-class Sources extends Vue {
+class Sources {
     created() {
     }
 
 }
+
+export default vueComponent(Sources);
 //-----------------------------------------------------------------------------
 </script>

+ 30 - 14
client/components/share/Dialog.vue

@@ -1,12 +1,12 @@
 <template>
-    <q-dialog v-model="active" no-route-dismiss>
+    <q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
         <div class="column bg-white no-wrap">
             <div class="header row">
                 <div class="caption col row items-center q-ml-md">
                     <slot name="header"></slot>
                 </div>
                 <div class="close-icon column justify-center items-center">
-                    <q-btn flat round dense v-close-popup>
+                    <q-btn v-close-popup flat round dense>
                         <q-icon name="la la-times" size="18px"></q-icon>
                     </q-btn>
                 </div>
@@ -25,26 +25,42 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
+import * as utils from '../../share/utils';
 
-const DialogProps = Vue.extend({
-    props: {
-        value: Boolean,
-    }
-});
+class Dialog {
+    _props = {
+        modelValue: Boolean,
+    };
+
+    shown = false;
 
-export default @Component({
-})
-class Dialog extends DialogProps {
     get active() {
-        return this.value;
+        return this.modelValue;
     }
 
     set active(value) {
-        this.$emit('input', value);
+        this.$emit('update:modelValue', value);
+    }
+
+    onShow() {
+        this.shown = true;
+    }
+
+    onHide() {
+        this.shown = false;
+    }
+
+    async waitShown() {
+        let i = 100;
+        while (!this.shown && i > 0) {
+            await utils.sleep(10);
+            i--;
+        }
     }
 }
+
+export default vueComponent(Dialog);
 //-----------------------------------------------------------------------------
 </script>
 

+ 4 - 5
client/components/share/Notify.vue

@@ -4,12 +4,9 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-})
-class Notify extends Vue {
+class Notify {
     notify(opts) {
         let {
             caption = null,
@@ -55,5 +52,7 @@ class Notify extends Vue {
         this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
     }
 }
+
+export default vueComponent(Notify);
 //-----------------------------------------------------------------------------
 </script>

+ 33 - 30
client/components/share/NumInput.vue

@@ -1,17 +1,19 @@
 <template>
-    <q-input outlined dense
+    <q-input
         v-model="filteredValue"
+        outlined dense
         input-style="text-align: center"
         class="no-mp"
         :class="(error ? 'error' : '')"
         :disable="disable"
     >
         <slot></slot>
-        <template v-slot:prepend>
-            <q-icon :class="(validate(value - step) ? '' : 'disable')" 
+        <template #prepend>
+            <q-icon
+                v-ripple="validate(modelValue - step)" 
+                :class="(validate(modelValue - step) ? '' : 'disable')" 
                 name="la la-minus-circle" 
                 class="button" 
-                v-ripple="validate(value - step)" 
                 @click="minus"
                 @mousedown.prevent.stop="onMouseDown($event, 'minus')"
                 @mouseup.prevent.stop="onMouseUp"
@@ -21,11 +23,12 @@
                 @touchcancel.prevent.stop="onTouchEnd"
             />
         </template>
-        <template v-slot:append>
-            <q-icon :class="(validate(value + step) ? '' : 'disable')"
+        <template #append>
+            <q-icon
+                v-ripple="validate(modelValue + step)"
+                :class="(validate(modelValue + step) ? '' : 'disable')"
                 name="la la-plus-circle"
                 class="button"
-                v-ripple="validate(value + step)"
                 @click="plus"
                 @mousedown.prevent.stop="onMouseDown($event, 'plus')"
                 @mouseup.prevent.stop="onMouseUp"
@@ -40,43 +43,41 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
 import * as utils from '../../share/utils';
 
-const NumInputProps = Vue.extend({
-    props: {
-        value: Number,
-        min: { type: Number, default: -Number.MAX_VALUE },
-        max: { type: Number, default: Number.MAX_VALUE },
-        step: { type: Number, default: 1 },
-        digits: { type: Number, default: 0 },
-        disable: Boolean
-    }
-});
-
-export default @Component({
+const componentOptions = {
     watch: {
         filteredValue: function(newValue) {
             if (this.validate(newValue)) {
                 this.error = false;
-                this.$emit('input', this.string2number(newValue));
+                this.$emit('update:modelValue', this.string2number(newValue));
             } else {
                 this.error = true;
             }
         },
-        value: function(newValue) {
+        modelValue: function(newValue) {
             this.filteredValue = newValue;
         },
     }
-})
-class NumInput extends NumInputProps {
+};
+class NumInput {
+    _options = componentOptions;
+    _props = {
+        modelValue: Number,
+        min: { type: Number, default: -Number.MAX_VALUE },
+        max: { type: Number, default: Number.MAX_VALUE },
+        step: { type: Number, default: 1 },
+        digits: { type: Number, default: 0 },
+        disable: Boolean
+    };
+
     filteredValue = 0;
     error = false;
 
     created() {
-        this.filteredValue = this.value;
+        this.filteredValue = this.modelValue;
     }
 
     string2number(value) {
@@ -95,13 +96,13 @@ class NumInput extends NumInputProps {
     }
 
     plus() {
-        const newValue = this.value + this.step;
+        const newValue = this.modelValue + this.step;
         if (this.validate(newValue))
             this.filteredValue = newValue;
     }
 
     minus() {
-        const newValue = this.value - this.step;
+        const newValue = this.modelValue - this.step;
         if (this.validate(newValue))
             this.filteredValue = newValue;
     }
@@ -136,7 +137,7 @@ class NumInput extends NumInputProps {
     }
 
     onTouchStart(event, way) {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         if (event.touches.length == 1) {
             this.inTouch = true;
@@ -145,12 +146,14 @@ class NumInput extends NumInputProps {
     }
 
     onTouchEnd() {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         this.inTouch = false;
         this.onMouseUp();
     }
 }
+
+export default vueComponent(NumInput);
 //-----------------------------------------------------------------------------
 </script>
 

+ 42 - 23
client/components/share/StdDialog.vue

@@ -1,5 +1,5 @@
 <template>
-    <q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
+    <q-dialog ref="dialog" v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
         <slot></slot>
 
         <!--------------------------------------------------->
@@ -10,7 +10,7 @@
                     <div v-html="caption"></div>
                 </div>
                 <div class="close-icon column justify-center items-center">
-                    <q-btn flat round dense v-close-popup>
+                    <q-btn v-close-popup flat round dense>
                         <q-icon name="la la-times" size="18px"></q-icon>
                     </q-btn>
                 </div>
@@ -21,7 +21,9 @@
             </div>
 
             <div class="buttons row justify-end q-pa-md">
-                <q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
+                <q-btn class="q-px-md" dense no-caps @click="okClick">
+                    OK
+                </q-btn>
             </div>
         </div>
 
@@ -33,7 +35,7 @@
                     <div v-html="caption"></div>
                 </div>
                 <div class="close-icon column justify-center items-center">
-                    <q-btn flat round dense v-close-popup>
+                    <q-btn v-close-popup flat round dense>
                         <q-icon name="la la-times" size="18px"></q-icon>
                     </q-btn>
                 </div>
@@ -44,8 +46,12 @@
             </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">OK</q-btn>
+                <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                    Отмена
+                </q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
+                    OK
+                </q-btn>
             </div>
         </div>
 
@@ -57,7 +63,7 @@
                     <div v-html="caption"></div>
                 </div>
                 <div class="close-icon column justify-center items-center">
-                    <q-btn flat round dense v-close-popup>
+                    <q-btn v-close-popup flat round dense>
                         <q-icon name="la la-times" size="18px"></q-icon>
                     </q-btn>
                 </div>
@@ -65,13 +71,19 @@
 
             <div class="q-mx-md">
                 <div v-html="message"></div>
-                <q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
-                <div class="error"><span v-show="error != ''">{{ error }}</span></div>
+                <q-input ref="input" v-model="inputValue" class="q-mt-xs" outlined dense />
+                <div class="error">
+                    <span v-show="error != ''">{{ error }}</span>
+                </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">OK</q-btn>
+                <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                    Отмена
+                </q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
+                    OK
+                </q-btn>
             </div>
         </div>
 
@@ -83,7 +95,7 @@
                     <div v-html="caption"></div>
                 </div>
                 <div class="close-icon column justify-center items-center">
-                    <q-btn flat round dense v-close-popup>
+                    <q-btn v-close-popup flat round dense>
                         <q-icon name="la la-times" size="18px"></q-icon>
                     </q-btn>
                 </div>
@@ -92,14 +104,20 @@
             <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 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>
+                <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
+                    Отмена
+                </q-btn>
+                <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps :disabled="hotKeyCode == ''" @click="okClick">
+                    OK
+                </q-btn>
             </div>
         </div>
     </q-dialog>
@@ -107,19 +125,18 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
-
+import vueComponent from '../vueComponent.js';
 import * as utils from '../../share/utils';
 
-export default @Component({
+const componentOptions = {
     watch: {
         inputValue: function(newValue) {
             this.validate(newValue);
         },
     }
-})
-class StdDialog extends Vue {
+};
+class StdDialog {
+    _options = componentOptions;
     caption = '';
     message = '';
     active = false;
@@ -131,8 +148,8 @@ class StdDialog extends Vue {
     hotKeyCode = '';
 
     created() {
-        if (this.$root.addKeyHook) {
-            this.$root.addKeyHook(this.keyHook);
+        if (this.$root.addEventHook) {
+            this.$root.addEventHook('key', this.keyHook);
         }
     }
 
@@ -313,6 +330,8 @@ class StdDialog extends Vue {
         }
     }
 }
+
+export default vueComponent(StdDialog);
 //-----------------------------------------------------------------------------
 </script>
 

+ 20 - 14
client/components/share/Window.vue

@@ -2,11 +2,17 @@
     <div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
         <div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
             <div ref="window" class="window flexfit column no-wrap">
-                <div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
-                    @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
+                <div 
+                    ref="header"
+                    class="header row justify-end"
+                    @mousedown.prevent.stop="onMouseDown"
+                    @touchstart.stop="onTouchStart"
+                    @touchend.stop="onTouchEnd"
+                    @touchmove.stop="onTouchMove"
+                >
                     <span class="header-text col"><slot name="header"></slot></span>
                     <slot name="buttons"></slot>
-                    <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
+                    <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
                 </div>
 
                 <slot></slot>
@@ -17,19 +23,17 @@
 
 <script>
 //-----------------------------------------------------------------------------
-import Vue from 'vue';
-import Component from 'vue-class-component';
+import vueComponent from '../vueComponent.js';
 
-export default @Component({
-    props: {
+class Window {
+    _props = {
         height: { type: String, default: '100%' },
         width: { type: String, default: '100%' },
         maxWidth: { type: String, default: '' },
         topShift: { type: Number, default: 0 },
         margin: '',
-    }
-})
-class Window extends Vue {
+    };
+
     init() {
         this.$nextTick(() => {
             this.$refs.main.style.top = 0;
@@ -51,7 +55,7 @@ class Window extends Vue {
     }
 
     onMouseDown(event) {
-        if (this.$isMobileDevice)
+        if (this.$root.isMobileDevice)
             return;
         if (event.button == 0) {
             this.$refs.header.style.cursor = 'move';
@@ -81,7 +85,7 @@ class Window extends Vue {
     }
 
     onTouchStart(event) {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         if (event.touches.length == 1) {
             const touch = event.touches[0];
@@ -93,7 +97,7 @@ class Window extends Vue {
     }
 
     onTouchMove(event) {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         if (event.touches.length == 1 && this.moving) {
             const touch = event.touches[0];
@@ -108,7 +112,7 @@ class Window extends Vue {
     }
 
     onTouchEnd() {
-        if (!this.$isMobileDevice)
+        if (!this.$root.isMobileDevice)
             return;
         this.$refs.header.style.cursor = 'default';
         this.moving = false;
@@ -120,6 +124,8 @@ class Window extends Vue {
             this.$emit('close');
     }
 }
+
+export default vueComponent(Window);
 //-----------------------------------------------------------------------------
 </script>
 

+ 52 - 0
client/components/vueComponent.js

@@ -0,0 +1,52 @@
+import { defineComponent } from 'vue';
+import _ from 'lodash';
+
+export default function(componentClass) {    
+    const comp = {};
+    const obj = new componentClass();
+    
+    //data, options, props
+    const data = {};
+    for (const prop of Object.getOwnPropertyNames(obj)) {
+        if (['_options', '_props'].includes(prop)) {//meta props
+            if (prop === '_options') {
+                const options = obj[prop];
+                for (const optName of ['components', 'watch', 'emits']) {
+                    if (options[optName]) {
+                        comp[optName] = options[optName];
+                    }
+                }
+            } else if (prop === '_props') {
+                comp['props'] = obj[prop];
+            }
+        } else {//usual prop
+            data[prop] = obj[prop];
+        }
+    }
+    comp.data = () => _.cloneDeep(data);
+    
+    //methods
+    const classProto = Object.getPrototypeOf(obj);
+    const classMethods = Object.getOwnPropertyNames(classProto);
+    const methods = {};
+    const computed = {};
+    for (const method of classMethods) {
+        const desc = Object.getOwnPropertyDescriptor(classProto, method);
+        if (desc.get) {//has getter, computed
+            computed[method] = {get: desc.get};
+            if (desc.set)
+                computed[method].set = desc.set;
+        } else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
+                    'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
+                    'setup'].includes(method) ) {
+            comp[method] = obj[method];
+        } else if (method !== 'constructor') {//usual
+            methods[method] = obj[method];
+        }
+    }
+    comp.methods = methods;
+    comp.computed = computed;
+
+    //console.log(comp);
+    return defineComponent(comp);
+}

+ 10 - 12
client/main.js

@@ -1,18 +1,16 @@
-import Vue from 'vue';
+import { createApp } from 'vue';
 
 import router from './router';
 import store from './store';
-import './quasar';
-
-import vueSanitize from 'vue-sanitize';
-Vue.use(vueSanitize);
+import q from './quasar';
 
 import App from './components/App.vue';
-//Vue.config.productionTip = false;
-Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
 
-new Vue({
-    router,
-    store,
-    render: h => h(App),
-}).$mount('#app');
+const app = createApp(App);
+
+app.use(router);
+app.use(store);
+app.use(q.quasar, q.options);
+q.init();
+
+app.mount('#app');

+ 12 - 11
client/quasar.js

@@ -1,8 +1,7 @@
-import Vue from 'vue';
-
 import 'quasar/dist/quasar.css';
-import Quasar from 'quasar/src/vue-plugin.js'
+//import Quasar from 'quasar/dist/quasar.umd.prod.js';
 
+import Quasar from 'quasar/src/vue-plugin.js';
 //config
 const config = {};
 
@@ -80,16 +79,18 @@ const plugins = {
     Notify,
 };
 
-//use
-Vue.use(Quasar, { config, components, directives, plugins });
-
 //icons
-//import '@quasar/extras/material-icons/material-icons.css';
-//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
 //import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
+//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
 
 import '@quasar/extras/line-awesome/line-awesome.css';
-
-//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
 import lineAwesome from 'quasar/icon-set/line-awesome.js'
-Quasar.iconSet.set(lineAwesome);
+
+//const q: {Quasar, QuasarOptions: { config, components, directives, plugins }};
+export default {
+    quasar: Quasar,
+    options: { config, components, directives, plugins }, 
+    init: () => {
+        Quasar.iconSet.set(lineAwesome);
+}
+};

+ 4 - 6
client/router.js

@@ -1,5 +1,4 @@
-import Vue from 'vue';
-import VueRouter from 'vue-router';
+import { createRouter, createWebHashHistory } from 'vue-router';
 import _ from 'lodash';
 
 const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
@@ -36,7 +35,7 @@ const myRoutes = [
     ['/settings', Settings],
     ['/help', Help],
     ['/404', NotFound404],
-    ['*', null, null, '/cardindex'],
+    ['/:pathMatch(.*)*', null, null, '/cardindex'],
 ];
 
 let routes = {};
@@ -63,8 +62,7 @@ for (let route of myRoutes) {
 }
 routes = routes.children;
 
-Vue.use(VueRouter);
-
-export default new VueRouter({
+export default createRouter({
+    history: createWebHashHistory(),
     routes
 });

+ 2 - 5
client/store/index.js

@@ -1,5 +1,4 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
+import { createStore } from 'vuex';
 import createPersistedState from 'vuex-persistedstate';
 
 import root from './root.js';
@@ -7,11 +6,9 @@ import uistate from './modules/uistate';
 import config from './modules/config';
 import reader from './modules/reader';
 
-Vue.use(Vuex);
-
 const debug = process.env.NODE_ENV !== 'production';
 
-export default new Vuex.Store(Object.assign({}, root, {
+export default createStore(Object.assign({}, root, {
     modules: {
         uistate,
         config,

+ 43 - 0
docs/beta/beta.omnireader_http

@@ -0,0 +1,43 @@
+server {
+  listen 80;
+  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 {
+      types { } default_type "application/xml; charset=utf-8";
+      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;
+}

+ 3 - 3
docs/omnireader.ru/README.md

@@ -44,15 +44,15 @@ sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graph
 Сначала настроим для HTTP:
 ```
 sudo apt install nginx
-sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
-sudo ln -s /etc/nginx/sites-available/omnireader_http /etc/nginx/sites-enabled/omnireader
+sudo cp docs/omnireader.ru/omnireader_http /etc/nginx/sites-available/omnireader
+sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
 sudo rm /etc/nginx/sites-enabled/default
 sudo service nginx reload
 sudo chown -R www-data.www-data /var/www
 ```
 
 ### certbot
-#### Следовать инструкции установки certbot https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx
+#### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20
 После установки сертификата, можно использовать конфиг для nginx c ssl:
 ```
 sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader

Разница между файлами не показана из-за своего большого размера
+ 12454 - 3917
package-lock.json


+ 43 - 49
package.json

@@ -1,6 +1,6 @@
 {
   "name": "Liberama",
-  "version": "0.10.3",
+  "version": "0.11.0",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "repository": "bookpauk/liberama",
@@ -21,73 +21,67 @@
     "scripts": "server/config/*.js"
   },
   "devDependencies": {
-    "babel-core": "^6.22.1",
-    "babel-eslint": "^10.1.0",
-    "babel-loader": "^7.1.1",
-    "babel-plugin-component": "^1.1.1",
-    "babel-plugin-syntax-dynamic-import": "^6.18.0",
-    "babel-plugin-transform-class-properties": "^6.24.1",
-    "babel-plugin-transform-decorators-legacy": "^1.3.5",
-    "babel-preset-env": "^1.3.2",
-    "clean-webpack-plugin": "^1.0.1",
-    "copy-webpack-plugin": "^5.1.2",
-    "css-loader": "^1.0.0",
-    "eslint": "^5.16.0",
-    "eslint-plugin-html": "^5.0.5",
-    "eslint-plugin-node": "^8.0.0",
-    "eslint-plugin-vue": "^5.2.3",
-    "file-loader": "^3.0.1",
-    "html-webpack-plugin": "^3.2.0",
-    "mini-css-extract-plugin": "^0.5.0",
-    "optimize-css-assets-webpack-plugin": "^5.0.8",
-    "terser-webpack-plugin": "^1.4.5",
-    "url-loader": "^1.1.2",
-    "vue-class-component": "^6.3.2",
-    "vue-loader": "^15.9.8",
+    "@babel/core": "^7.16.0",
+    "@babel/eslint-parser": "^7.16.3",
+    "@babel/eslint-plugin": "^7.14.5",
+    "@babel/plugin-proposal-decorators": "^7.16.0",
+    "@babel/preset-env": "^7.16.0",
+    "@vue/compiler-sfc": "^3.2.22",
+    "babel-loader": "^8.2.3",
+    "copy-webpack-plugin": "^9.1.0",
+    "css-loader": "^6.5.1",
+    "css-minimizer-webpack-plugin": "^3.1.3",
+    "eslint": "^8.2.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "html-webpack-plugin": "^5.5.0",
+    "mini-css-extract-plugin": "^2.4.4",
+    "terser-webpack-plugin": "^5.2.5",
+    "vue-eslint-parser": "^8.0.1",
+    "vue-loader": "^16.8.3",
     "vue-style-loader": "^4.1.3",
-    "vue-template-compiler": "^2.6.14",
-    "webpack": "^4.46.0",
-    "webpack-cli": "^3.3.12",
-    "webpack-dev-middleware": "^3.7.3",
+    "webpack": "^5.64.1",
+    "webpack-cli": "^4.9.1",
+    "webpack-dev-middleware": "^5.2.1",
     "webpack-hot-middleware": "^2.25.1",
-    "webpack-merge": "^4.2.2",
-    "workbox-webpack-plugin": "^5.1.4"
+    "webpack-merge": "^5.8.0",
+    "workbox-webpack-plugin": "^6.4.1"
   },
   "dependencies": {
-    "@quasar/extras": "^1.11.2",
-    "axios": "^0.18.1",
-    "base-x": "^3.0.8",
-    "chardet": "^0.7.0",
+    "@quasar/extras": "^1.12.0",
+    "@vue/compat": "^3.2.21",
+    "axios": "^0.24.0",
+    "base-x": "^3.0.9",
+    "chardet": "^1.4.0",
     "compression": "^1.7.4",
     "express": "^4.17.1",
-    "fg-loadcss": "^2.1.0",
-    "fs-extra": "^7.0.1",
-    "got": "^9.6.0",
+    "fg-loadcss": "^3.1.0",
+    "fs-extra": "^9.0.1",
+    "got": "^11.8.2",
     "he": "^1.2.0",
-    "iconv-lite": "^0.4.24",
+    "iconv-lite": "^0.6.3",
     "localforage": "^1.10.0",
     "lodash": "^4.17.21",
     "minimist": "^1.2.5",
     "multer": "^1.4.3",
-    "pako": "^1.0.11",
+    "pako": "^2.0.4",
     "path-browserify": "^1.0.1",
-    "pidusage": "^2.0.21",
+    "pidusage": "^3.0.0",
     "pkg": "^4.4.9",
-    "quasar": "^1.16.0",
+    "quasar": "^2.3.2",
     "safe-buffer": "^5.2.1",
+    "sanitize-html": "^2.5.3",
     "sjcl": "^1.0.8",
     "sql-template-strings": "^2.2.2",
     "sqlite": "^4.0.23",
     "sqlite3": "^5.0.2",
     "tar-fs": "^2.1.1",
     "unbzip2-stream": "^1.4.3",
-    "vue": "^2.6.14",
-    "vue-router": "^3.5.2",
-    "vue-sanitize": "^0.2.1",
-    "vuex": "^3.6.2",
-    "vuex-persistedstate": "^2.7.1",
-    "webdav": "^2.10.2",
-    "ws": "^7.5.5",
-    "zip-stream": "^2.1.3"
+    "vue": "^3.2.22",
+    "vue-router": "^4.0.12",
+    "vuex": "^4.0.2",
+    "vuex-persistedstate": "^4.1.0",
+    "webdav": "^4.7.0",
+    "ws": "^8.2.3",
+    "zip-stream": "^4.1.0"
   }
 }

+ 1 - 1
server/controllers/WebSocketController.js

@@ -23,7 +23,7 @@ class WebSocketController {
 
         wss.on('connection', (ws) => {
             ws.on('message', (message) => {
-                this.onMessage(ws, message);
+                this.onMessage(ws, message.toString());
             });
         });
 

+ 2 - 2
server/core/FileDownloader.js

@@ -8,10 +8,10 @@ class FileDownloader {
     async load(url, callback, abort) {
         let errMes = '';
         const options = {
-            encoding: null,
             headers: {
                 'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'
-            }
+            },
+            responseType: 'buffer',
         };
 
         const response = await got(url, Object.assign({}, options, {method: 'HEAD'}));

+ 1 - 1
server/core/Reader/BookConverter/textUtils.js

@@ -4,7 +4,7 @@ function getEncoding(buf) {
     let selected = getEncodingLite(buf);
 
     if (selected == 'ISO-8859-5') {
-        const charsetAll = chardet.detectAll(buf.slice(0, 20000));
+        const charsetAll = chardet.analyse(buf.slice(0, 20000));
         for (const charset of charsetAll) {
             if (charset.name.indexOf('ISO-8859') < 0) {
                 selected = charset.name;

+ 2 - 1
server/core/WebSocketConnection.js

@@ -9,7 +9,8 @@ const cleanPeriod = 5*1000;//5 секунд
 class WebSocketConnection {
     //messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
     constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
-        this.WebSocket = (isBrowser ? WebSocket : require('ws'));
+        //const ws = 'ws';//for nodejs
+        this.WebSocket = (isBrowser ? WebSocket : null/*for nodejs require(ws)*/);
         this.url = url;
         this.ws = null;
         this.listeners = [];

+ 4 - 2
server/core/Zip/ZipStreamer.js

@@ -1,7 +1,7 @@
-const fs = require('fs-extra');
+/*const fs = require('fs-extra');
 const path = require('path');
 
-const zipStream = require('zip-stream');
+const zipStream = require('zip-stream');*/
 const unzipStream = require('./node_stream_zip');
 
 class ZipStreamer {
@@ -10,6 +10,7 @@ class ZipStreamer {
 
     //TODO: сделать рекурсивный обход директорий, пока только файлы
     //files = ['filename', 'dirname/']
+    /* zip-stream 2.1.3 => 4.1.0 Актуализировать!
     pack(zipFile, files, options, entryCallback) {
         return new Promise((resolve, reject) => { (async() => {
             entryCallback = (entryCallback ? entryCallback : () => {});
@@ -51,6 +52,7 @@ class ZipStreamer {
             zip.finish();
         })().catch(reject); });
     }
+    */
 
     unpack(zipFile, outputDir, options, entryCallback) {
         return new Promise((resolve, reject) => {

Некоторые файлы не были показаны из-за большого количества измененных файлов