Selaa lähdekoodia

Merge branch 'release/0.9.0'

Book Pauk 5 vuotta sitten
vanhempi
commit
22e5d38ef5
64 muutettua tiedostoa jossa 2255 lisäystä ja 1391 poistoa
  1. 31 0
      build/includer.js
  2. 5 0
      build/webpack.base.config.js
  3. 4 1
      client/api/misc.js
  4. 15 4
      client/api/reader.js
  5. 1 5
      client/api/webSocketConnection.js
  6. 67 77
      client/components/App.vue
  7. 2 2
      client/components/CardIndex/Book/Book.vue
  8. 2 2
      client/components/CardIndex/Card/Card.vue
  9. 7 13
      client/components/CardIndex/CardIndex.vue
  10. 2 2
      client/components/CardIndex/History/History.vue
  11. 2 2
      client/components/CardIndex/Search/Search.vue
  12. 2 2
      client/components/Help/Help.vue
  13. 2 2
      client/components/Income/Income.vue
  14. 2 2
      client/components/NotFound404/NotFound404.vue
  15. 12 16
      client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue
  16. 21 46
      client/components/Reader/HelpPage/DonateHelpPage/DonateHelpPage.vue
  17. 51 40
      client/components/Reader/HelpPage/HelpPage.vue
  18. 1 6
      client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue
  19. 1 6
      client/components/Reader/HelpPage/MouseHelpPage/MouseHelpPage.vue
  20. 4 7
      client/components/Reader/HelpPage/VersionHistoryPage/VersionHistoryPage.vue
  21. 13 7
      client/components/Reader/LoaderPage/GithubCorner/GithubCorner.vue
  22. 24 60
      client/components/Reader/LoaderPage/LoaderPage.vue
  23. 2 4
      client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue
  24. 35 41
      client/components/Reader/ProgressPage/ProgressPage.vue
  25. 129 127
      client/components/Reader/Reader.vue
  26. 191 126
      client/components/Reader/RecentBooksPage/RecentBooksPage.vue
  27. 22 17
      client/components/Reader/SearchPage/SearchPage.vue
  28. 3 3
      client/components/Reader/ServerStorage/ServerStorage.vue
  29. 16 12
      client/components/Reader/SetPositionPage/SetPositionPage.vue
  30. 249 611
      client/components/Reader/SettingsPage/SettingsPage.vue
  31. 17 0
      client/components/Reader/SettingsPage/defPalette.js
  32. 8 0
      client/components/Reader/SettingsPage/include/ButtonsTab.inc
  33. 8 0
      client/components/Reader/SettingsPage/include/KeysTab.inc
  34. 91 0
      client/components/Reader/SettingsPage/include/OthersTab.inc
  35. 28 0
      client/components/Reader/SettingsPage/include/PageMoveTab.inc
  36. 101 0
      client/components/Reader/SettingsPage/include/ProfilesTab.inc
  37. 3 0
      client/components/Reader/SettingsPage/include/ResetTab.inc
  38. 34 0
      client/components/Reader/SettingsPage/include/ViewTab.inc
  39. 58 0
      client/components/Reader/SettingsPage/include/ViewTab/Color.inc
  40. 56 0
      client/components/Reader/SettingsPage/include/ViewTab/Font.inc
  41. 24 0
      client/components/Reader/SettingsPage/include/ViewTab/Status.inc
  42. 160 0
      client/components/Reader/SettingsPage/include/ViewTab/Text.inc
  43. 34 44
      client/components/Reader/TextPage/TextPage.vue
  44. 2 0
      client/components/Reader/share/BookParser.js
  45. 12 0
      client/components/Reader/versionHistory.js
  46. 2 2
      client/components/Settings/Settings.vue
  47. 2 2
      client/components/Sources/Sources.vue
  48. 64 0
      client/components/share/Dialog.vue
  49. 58 0
      client/components/share/Notify.vue
  50. 185 0
      client/components/share/NumInput.vue
  51. 255 0
      client/components/share/StdDialog.vue
  52. 19 22
      client/components/share/Window.vue
  53. 0 69
      client/element.js
  54. 1 1
      client/main.js
  55. 88 0
      client/quasar.js
  56. 1 1
      client/store/modules/reader.js
  57. BIN
      client/theme/fonts/element-icons.ttf
  58. BIN
      client/theme/fonts/element-icons.woff
  59. 0 0
      client/theme/index.css
  60. 11 1
      package-lock.json
  61. 3 1
      package.json
  62. 2 1
      server/config/base.js
  63. 5 2
      server/controllers/MiscController.js
  64. 5 2
      server/controllers/WebSocketController.js

+ 31 - 0
build/includer.js

@@ -0,0 +1,31 @@
+const path = require('path');
+const fs = require('fs');
+
+//пример в коде:
+//  @@include('./test/testFile.inc');
+
+function includeRecursive(self, parentFile, source, depth) {
+    depth = (depth ? depth : 0);
+    if (depth > 50)
+        throw new Error('includer: stack too big');
+    const lines = source.split('\n');
+    let result = [];
+    for (const line of lines) {
+        const trimmed = line.trim();
+        const m = trimmed.match(/^@@[\s]*?include[\s]*?\(['"](.*)['"]\)/);
+        if (m) {
+            const includedFile = path.resolve(path.dirname(parentFile), m[1]);
+            self.addDependency(includedFile);
+
+            const fileContent = fs.readFileSync(includedFile, 'utf8');
+            result = result.concat(includeRecursive(self, includedFile, fileContent, depth + 1));
+        } else {
+            result.push(line);
+        }
+    }
+    return result;
+}
+
+exports.default = function includer(source) {
+    return includeRecursive(this, this.resourcePath, source).join('\n');
+}

+ 5 - 0
build/webpack.base.config.js

@@ -16,6 +16,11 @@ module.exports = {
                 test: /\.vue$/,
                 test: /\.vue$/,
                 loader: "vue-loader"
                 loader: "vue-loader"
             },
             },
+            {
+                test: /\.includer$/,
+                resourceQuery: /^\?vue/,
+                use: path.resolve('build/includer.js')
+            },
             {
             {
                 test: /\.js$/,
                 test: /\.js$/,
                 loader: 'babel-loader',
                 loader: 'babel-loader',

+ 4 - 1
client/api/misc.js

@@ -14,7 +14,10 @@ class Misc {
 
 
         try {
         try {
             await wsc.open();
             await wsc.open();
-            return await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
+            const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
+            if (config.error)
+                throw new Error(config.error);
+            return config;
         } catch (e) {
         } catch (e) {
             console.error(e);
             console.error(e);
         }
         }

+ 15 - 4
client/api/reader.js

@@ -18,17 +18,22 @@ class Reader {
         if (!callback) callback = () => {};
         if (!callback) callback = () => {};
 
 
         let response = {};
         let response = {};
-
         try {
         try {
             await wsc.open();
             await wsc.open();
             const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
             const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
 
 
+            let prevResponse = false;
             while (1) {// eslint-disable-line no-constant-condition
             while (1) {// eslint-disable-line no-constant-condition
                 response = await wsc.message(requestId);
                 response = await wsc.message(requestId);
-                callback(response);
 
 
-                if (!response.state)
-                    throw new Error('Неверный ответ api');
+                if (!response.state && prevResponse !== false) {//экономия траффика
+                    callback(prevResponse);
+                } else {//были изменения worker state
+                    if (!response.state)
+                        throw new Error('Неверный ответ api');
+                    callback(response);
+                    prevResponse = response;
+                }
 
 
                 if (response.state == 'finish' || response.state == 'error') {
                 if (response.state == 'finish' || response.state == 'error') {
                     break;
                     break;
@@ -127,6 +132,9 @@ class Reader {
                 response = await api.post('/restore-cached-file', {path: url});
                 response = await api.post('/restore-cached-file', {path: url});
                 response = response.data;
                 response = response.data;
             }
             }
+            if (response.state == 'error') {
+                throw new Error(response.error);
+            }
 
 
             const workerId = response.workerId;
             const workerId = response.workerId;
             if (!workerId)
             if (!workerId)
@@ -215,6 +223,9 @@ class Reader {
         const state = response.state;
         const state = response.state;
         if (!state)
         if (!state)
             throw new Error('Неверный ответ api');
             throw new Error('Неверный ответ api');
+        if (response.state == 'error') {
+            throw new Error(response.error);
+        }
 
 
         return response;
         return response;
     }
     }

+ 1 - 5
client/api/webSocketConnection.js

@@ -111,11 +111,7 @@ class WebSocketConnection {
                 requestId,
                 requestId,
                 timeout,
                 timeout,
                 onMessage: (mes) => {
                 onMessage: (mes) => {
-                    if (mes.error) {
-                        reject(mes.error);
-                    } else {
-                        resolve(mes);
-                    }
+                    resolve(mes);
                 },
                 },
                 onError: (e) => {
                 onError: (e) => {
                     reject(e);
                     reject(e);

+ 67 - 77
client/components/App.vue

@@ -1,9 +1,19 @@
 <template>
 <template>
-    <el-container>
-        <el-aside v-if="showAsideBar" :width="asideWidth">
+    <!--q-layout view="lhr lpr lfr">
+        <q-drawer v-model="showAsideBar" :width="asideWidth">
             <div class="app-name"><span v-html="appName"></span></div>
             <div class="app-name"><span v-html="appName"></span></div>
-            <el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
-            <el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
+            <q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
+
+            <q-list>
+                <q-item clickable v-ripple>
+                    <q-item-section avatar>
+                        <q-icon name="inbox" />
+                    </q-item-section>
+
+                    <q-item-section>Inbox</q-item-section>
+                </q-item>
+            </q-list-->
+            <!--el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
               <el-menu-item index="/cardindex">
               <el-menu-item index="/cardindex">
                 <i class="el-icon-search"></i>
                 <i class="el-icon-search"></i>
                 <span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
                 <span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
@@ -32,24 +42,37 @@
                 <i class="el-icon-question"></i>
                 <i class="el-icon-question"></i>
                 <span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
                 <span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
               </el-menu-item>
               </el-menu-item>
-            </el-menu>
-        </el-aside>
+            </el-menu-->
+        <!--/q-drawer>
 
 
-        <el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
+        <q-page-container>
             <keep-alive>
             <keep-alive>
                 <router-view></router-view>
                 <router-view></router-view>
             </keep-alive>
             </keep-alive>
-        </el-main>
-    </el-container>
+        </q-page-container>
+    </q-layout-->
+    <div class="fit row">
+        <Notify ref="notify"/>
+        <StdDialog ref="stdDialog"/>
+        <keep-alive>
+            <router-view class="col"></router-view>
+        </keep-alive>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 import Vue from 'vue';
 import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
+import Notify from './share/Notify.vue';
+import StdDialog from './share/StdDialog.vue';
 import * as utils from '../share/utils';
 import * as utils from '../share/utils';
 
 
 export default @Component({
 export default @Component({
+    components: {
+        Notify,
+        StdDialog,
+    },
     watch: {
     watch: {
         mode: function() {
         mode: function() {
             this.setAppTitle();
             this.setAppTitle();
@@ -75,6 +98,18 @@ class App extends Vue {
         this.uistate = this.$store.state.uistate;
         this.uistate = this.$store.state.uistate;
         this.config = this.$store.state.config;
         this.config = this.$store.state.config;
 
 
+        //root route
+        let cachedRoute = '';
+        let cachedPath = '';
+        this.$root.rootRoute = () => {
+            if (this.$route.path != cachedPath) {
+                cachedPath = this.$route.path;
+                const m = cachedPath.match(/^(\/[^/]*).*$/i);
+                cachedRoute = (m ? m[1] : this.$route.path);
+            }
+            return cachedRoute;
+        }
+
         // set-app-title
         // set-app-title
         this.$root.$on('set-app-title', this.setAppTitle);
         this.$root.$on('set-app-title', this.setAppTitle);
 
 
@@ -108,17 +143,16 @@ class App extends Vue {
     }
     }
 
 
     mounted() {
     mounted() {
+        this.$root.notify = this.$refs.notify;
+        this.$root.stdDialog = this.$refs.stdDialog;
+
         this.dispatch('config/loadConfig');
         this.dispatch('config/loadConfig');
         this.$watch('apiError', function(newError) {
         this.$watch('apiError', function(newError) {
             if (newError) {
             if (newError) {
                 let mes = newError.message;
                 let mes = newError.message;
                 if (newError.response && newError.response.config)
                 if (newError.response && newError.response.config)
                     mes = newError.response.config.url + '<br>' + newError.response.statusText;
                     mes = newError.response.config.url + '<br>' + newError.response.statusText;
-                this.$notify.error({
-                    title: 'Ошибка API',
-                    dangerouslyUseHTMLString: true,
-                    message: mes
-                });
+                this.$root.notify.error(mes, 'Ошибка API');
             }
             }
         });
         });
 
 
@@ -137,9 +171,9 @@ class App extends Vue {
 
 
     get asideWidth() {
     get asideWidth() {
         if (this.uistate.asideBarCollapse) {
         if (this.uistate.asideBarCollapse) {
-            return '64px';
+            return 64;
         } else {
         } else {
-            return '170px';
+            return 170;
         }
         }
     }
     }
 
 
@@ -163,10 +197,7 @@ class App extends Vue {
     }
     }
 
 
     get rootRoute() {
     get rootRoute() {
-        const m = this.$route.path.match(/^(\/[^/]*).*$/i);
-        this.$root.rootRoute = (m ? m[1] : this.$route.path);
-
-        return this.$root.rootRoute;
+        return this.$root.rootRoute();
     }
     }
 
 
     setAppTitle(title) {
     setAppTitle(title) {
@@ -193,12 +224,11 @@ class App extends Vue {
         return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
         return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
     }
     }
 
 
-    get isReaderActive() {
-        return this.rootRoute == '/reader';
+    set showAsideBar(value) {
     }
     }
 
 
-    get showMain() {
-        return (this.showAsideBar || this.isReaderActive);
+    get isReaderActive() {
+        return this.rootRoute == '/reader';
     }
     }
 
 
     redirectIfNeeded() {
     redirectIfNeeded() {
@@ -228,68 +258,28 @@ class App extends Vue {
     line-height: 140%;
     line-height: 140%;
     font-weight: bold;
     font-weight: bold;
 }
 }
-
-.bold-font {
-    font-weight: bold;
-}
-
-.el-container {
-    height: 100%;
-}
-
-.el-aside {
-    line-height: 1;
-    background-color: #ccc;
-    color: #000;
-}
-
-.el-main {
-    padding: 0;
-    background-color: #E6EDF4;
-    color: #000;
-}
-
-.el-menu-vertical:not(.el-menu--collapse) {
-    background-color: inherit;
-    color: inherit;
-    text-align: left;
-    width: 100%;
-    border: 0;
-}
-
-.el-menu--collapse {
-    background-color: inherit;
-    color: inherit;
-    border: 0;
-}
-
-.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
-    background-color: inherit;
-    color: inherit;
-    margin-top: 5px;
-    width: 100%;
-    height: 64px;
-    border: 0;
-}
-.el-menu-item {
-    font-size: 85%;
-}
 </style>
 </style>
 
 
 <style>
 <style>
-body, html, #app {
+body, html, #app {    
     margin: 0;
     margin: 0;
     padding: 0;
     padding: 0;
+    width: 100%;
     height: 100%;
     height: 100%;
     font: normal 12pt ReaderDefault;
     font: normal 12pt ReaderDefault;
 }
 }
 
 
-.el-tabs__content {
-    flex: 1;
-    padding: 0 !important;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
+.dborder {
+    border: 2px solid yellow !important;
+}
+
+.icon-rotate {
+    vertical-align: middle;
+    animation: rotating 2s linear infinite;
+}
+
+.notify-button-icon {
+    font-size: 16px !important;
 }
 }
 
 
 @font-face {
 @font-face {

+ 2 - 2
client/components/CardIndex/Book/Book.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Book в разработке
         Раздел Book в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 2 - 2
client/components/CardIndex/Card/Card.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Card в разработке
         Раздел Card в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 7 - 13
client/components/CardIndex/CardIndex.vue

@@ -1,15 +1,9 @@
 <template>
 <template>
-    <el-container direction="vertical">
-        <el-tabs type="border-card" style="height: 100%;" v-model="selectedTab">
-            <el-tab-pane label="Поиск"></el-tab-pane>
-            <el-tab-pane label="Автор"></el-tab-pane>
-            <el-tab-pane label="Книга"></el-tab-pane>
-            <el-tab-pane label="История"></el-tab-pane>
-            <keep-alive>
-                <router-view></router-view>
-            </keep-alive>
-        </el-tabs>
-    </el-container>
+    <div>
+        <keep-alive>
+            <router-view></router-view>
+        </keep-alive>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>
@@ -18,7 +12,7 @@ import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
-const rootRoute = '/cardindex';
+const selfRoute = '/cardindex';
 const tab2Route = [
 const tab2Route = [
     '/cardindex/search',
     '/cardindex/search',
     '/cardindex/card',
     '/cardindex/card',
@@ -51,7 +45,7 @@ class CardIndex extends Vue {
             if (t !== this.selectedTab)
             if (t !== this.selectedTab)
                 this.selectedTab = t.toString();
                 this.selectedTab = t.toString();
         } else {
         } else {
-            if (route == rootRoute && lastActiveTab !== null)
+            if (route == selfRoute && lastActiveTab !== null)
                 this.setRouteByTab(lastActiveTab);
                 this.setRouteByTab(lastActiveTab);
         }
         }
     }
     }

+ 2 - 2
client/components/CardIndex/History/History.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел History в разработке
         Раздел History в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 2 - 2
client/components/CardIndex/Search/Search.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Search в разработке
         Раздел Search в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 2 - 2
client/components/Help/Help.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Help в разработке
         Раздел Help в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 2 - 2
client/components/Income/Income.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Income в разработке
         Раздел Income в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 2 - 2
client/components/NotFound404/NotFound404.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Страница не найдена
         Страница не найдена
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 12 - 16
client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue

@@ -1,12 +1,12 @@
 <template>
 <template>
     <div class="page">
     <div class="page">
-        <h4>Возможности читалки:</h4>
+        <span class="text-h6 text-bold">Возможности читалки:</span>
         <ul>
         <ul>
             <li>загрузка любой страницы интернета</li>
             <li>загрузка любой страницы интернета</li>
+            <li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
             <li>работа в автономном режиме (без связи)</li>
             <li>работа в автономном режиме (без связи)</li>
             <li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
             <li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
             <li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
             <li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
-            <li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
             <li>кэширование файлов книг на клиенте и на сервере</li>
             <li>кэширование файлов книг на клиенте и на сервере</li>
             <li>открытие книг с локального диска</li>
             <li>открытие книг с локального диска</li>
             <li>плавный скроллинг текста</li>
             <li>плавный скроллинг текста</li>
@@ -25,10 +25,10 @@
         <div v-show="mode == 'omnireader'">
         <div v-show="mode == 'omnireader'">
             <p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
             <p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
                 <br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
                 <br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
-                &nbsp;
-                <span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
-                    (скопировать)
-                </span>
+                <q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
+                    <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                </q-icon>
+
                 <br>или перетащив на панель закладок следующую ссылку:
                 <br>или перетащив на панель закладок следующую ссылку:
                 <br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
                 <br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
                 <br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
                 <br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
@@ -60,9 +60,9 @@ class CommonHelpPage extends Vue {
         const result = await copyTextToClipboard(text);
         const result = await copyTextToClipboard(text);
         const msg = (result ? mes : 'Копирование не удалось');
         const msg = (result ? mes : 'Копирование не удалось');
         if (result)
         if (result)
-            this.$notify.success({message: msg});
+            this.$root.notify.success(msg);
         else
         else
-            this.$notify.error({message: msg});
+            this.$root.notify.error(msg);
     }
     }
 }
 }
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -70,20 +70,16 @@ class CommonHelpPage extends Vue {
 
 
 <style scoped>
 <style scoped>
 .page {
 .page {
-    flex: 1;
     padding: 15px;
     padding: 15px;
     overflow-y: auto;
     overflow-y: auto;
     font-size: 120%;
     font-size: 120%;
     line-height: 130%;
     line-height: 130%;
 }
 }
 
 
-h4 {
-    margin: 0;
-}
-
-.clickable {
-    color: blue;
-    text-decoration: underline;
+.copy-icon {
+    margin-left: 10px;
     cursor: pointer;
     cursor: pointer;
+    font-size: 120%;
+    color: blue;
 }
 }
 </style>
 </style>

+ 21 - 46
client/components/Reader/HelpPage/DonateHelpPage/DonateHelpPage.vue

@@ -4,62 +4,47 @@
             <p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
             <p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
             <div class="address">
             <div class="address">
                 <img class="logo" src="./assets/yandex.png">
                 <img class="logo" src="./assets/yandex.png">
-                <el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
+                <q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
                 <div class="para">{{ yandexAddress }}
                 <div class="para">{{ yandexAddress }}
-                    <el-tooltip :open-delay="500" effect="light">
-                        <template slot="content">
-                            Скопировать
-                        </template>
-                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(yandexAddress, 'Яндекс кошелек')"></i>
-                    </el-tooltip>
+                    <q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                    </q-icon>
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <div class="address">
+            <div class="address">                
                 <img class="logo" src="./assets/paypal.png">
                 <img class="logo" src="./assets/paypal.png">
                 <div class="para">{{ paypalAddress }}
                 <div class="para">{{ paypalAddress }}
-                    <el-tooltip :open-delay="500" effect="light">
-                        <template slot="content">
-                            Скопировать
-                        </template>
-                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(paypalAddress, 'Paypal-адрес')"></i>
-                    </el-tooltip>
+                    <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-icon>
                 </div>
                 </div>
-            </div>            
+            </div>
 
 
             <div class="address">                
             <div class="address">                
                 <img class="logo" src="./assets/bitcoin.png">
                 <img class="logo" src="./assets/bitcoin.png">
                 <div class="para">{{ bitcoinAddress }}
                 <div class="para">{{ bitcoinAddress }}
-                    <el-tooltip :open-delay="500" effect="light">
-                        <template slot="content">
-                            Скопировать
-                        </template>
-                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')"></i>
-                    </el-tooltip>
+                    <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-icon>
                 </div>
                 </div>
             </div>
             </div>
 
 
             <div class="address">                
             <div class="address">                
                 <img class="logo" src="./assets/litecoin.png">
                 <img class="logo" src="./assets/litecoin.png">
                 <div class="para">{{ litecoinAddress }}
                 <div class="para">{{ litecoinAddress }}
-                    <el-tooltip :open-delay="500" effect="light">
-                        <template slot="content">
-                            Скопировать
-                        </template>
-                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')"></i>
-                    </el-tooltip>
+                    <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-icon>
                 </div>
                 </div>
             </div>
             </div>
 
 
             <div class="address">                
             <div class="address">                
                 <img class="logo" src="./assets/monero.png">
                 <img class="logo" src="./assets/monero.png">
                 <div class="para">{{ moneroAddress }}
                 <div class="para">{{ moneroAddress }}
-                    <el-tooltip :open-delay="500" effect="light">
-                        <template slot="content">
-                            Скопировать
-                        </template>
-                        <i class="el-icon-copy-document copy-icon" @click="copyAddress(moneroAddress, 'Monero-адрес')"></i>
-                    </el-tooltip>
+                    <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-icon>
                 </div>
                 </div>
             </div>
             </div>
         </div>
         </div>
@@ -91,9 +76,9 @@ class DonateHelpPage extends Vue {
     async copyAddress(address, prefix) {
     async copyAddress(address, prefix) {
         const result = await copyTextToClipboard(address);
         const result = await copyTextToClipboard(address);
         if (result)
         if (result)
-            this.$notify.success({message: `${prefix} ${address} успешно скопирован в буфер обмена`});
+            this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
         else
         else
-            this.$notify.error({message: 'Копирование не удалось'});
+            this.$root.notify.error('Копирование не удалось');
     }
     }
 }
 }
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -101,12 +86,10 @@ class DonateHelpPage extends Vue {
 
 
 <style scoped>
 <style scoped>
 .page {
 .page {
-    flex: 1;
     padding: 15px;
     padding: 15px;
     overflow-y: auto;
     overflow-y: auto;
     font-size: 120%;
     font-size: 120%;
     line-height: 130%;
     line-height: 130%;
-    display: flex;
 }
 }
 
 
 .p {
 .p {
@@ -116,15 +99,10 @@ class DonateHelpPage extends Vue {
 }
 }
 
 
 .box {
 .box {
-    flex: 1;
     max-width: 550px;
     max-width: 550px;
     overflow-wrap: break-word;
     overflow-wrap: break-word;
 }
 }
 
 
-h5 {
-    margin: 0;
-}
-
 .address {
 .address {
     padding-top: 10px;
     padding-top: 10px;
     margin-top: 20px;
     margin-top: 20px;
@@ -134,10 +112,6 @@ h5 {
     margin: 10px 10px 10px 40px;
     margin: 10px 10px 10px 40px;
 }
 }
 
 
-.button {
-    margin-left: 10px;
-}
-
 .logo {
 .logo {
     width: 130px;
     width: 130px;
     position: relative;
     position: relative;
@@ -148,5 +122,6 @@ h5 {
     margin-left: 10px;
     margin-left: 10px;
     cursor: pointer;
     cursor: pointer;
     font-size: 120%;
     font-size: 120%;
+    color: blue;
 }
 }
 </style>
 </style>

+ 51 - 40
client/components/Reader/HelpPage/HelpPage.vue

@@ -4,23 +4,20 @@
             Справка
             Справка
         </template>
         </template>
 
 
-        <el-tabs type="border-card" v-model="selectedTab">
-            <el-tab-pane class="tab" label="Общее">
-                <CommonHelpPage></CommonHelpPage>
-            </el-tab-pane>
-            <el-tab-pane label="Клавиатура">
-                <HotkeysHelpPage></HotkeysHelpPage>
-            </el-tab-pane>
-            <el-tab-pane label="Мышь/тачскрин">
-                <MouseHelpPage></MouseHelpPage>
-            </el-tab-pane>
-            <el-tab-pane label="История версий" name="releases">
-                <VersionHistoryPage></VersionHistoryPage>
-            </el-tab-pane>
-            <el-tab-pane label="Помочь проекту" name="donate">
-                <DonateHelpPage></DonateHelpPage>
-            </el-tab-pane>
-        </el-tabs>
+        <div class="col column" style="min-width: 600px">
+            <q-btn-toggle
+                v-model="selectedTab"
+                toggle-color="primary"
+                no-caps unelevated
+                :options="buttons"
+            />
+            <div class="separator"></div>
+
+            <keep-alive>
+                <component ref="page" class="col" :is="activePage"
+                ></component>
+            </keep-alive>
+        </div>
     </Window>
     </Window>
 </template>
 </template>
 
 
@@ -33,32 +30,54 @@ import Window from '../../share/Window.vue';
 import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
 import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
 import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
 import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
 import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
 import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
-import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
 import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
 import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
+import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
+
+const pages = {
+    'CommonHelpPage': CommonHelpPage,
+    'HotkeysHelpPage': HotkeysHelpPage,
+    'MouseHelpPage': MouseHelpPage,
+    'VersionHistoryPage': VersionHistoryPage,
+    'DonateHelpPage': DonateHelpPage,
+};
+
+const tabs = [
+    ['CommonHelpPage', 'Общее'],
+    ['HotkeysHelpPage', 'Клавиатура'],
+    ['MouseHelpPage', 'Мышь/тачскрин'],
+    ['VersionHistoryPage', 'История версий'],
+    ['DonateHelpPage', 'Помочь проекту'],
+];
 
 
 export default @Component({
 export default @Component({
-    components: {
-        Window,
-        CommonHelpPage,
-        HotkeysHelpPage,
-        MouseHelpPage,
-        DonateHelpPage,
-        VersionHistoryPage,
-    },
+    components: Object.assign({ Window }, pages),
 })
 })
 class HelpPage extends Vue {
 class HelpPage extends Vue {
-    selectedTab = null;
+    selectedTab = 'CommonHelpPage';
 
 
     close() {
     close() {
         this.$emit('help-toggle');
         this.$emit('help-toggle');
     }
     }
 
 
+    get activePage() {
+        if (pages[this.selectedTab])
+            return pages[this.selectedTab];
+        return null;
+    }
+
+    get buttons() {
+        let result = [];
+        for (const tab of tabs)
+            result.push({label: tab[1], value: tab[0]});
+        return result;
+    }
+
     activateDonateHelpPage() {
     activateDonateHelpPage() {
-        this.selectedTab = 'donate';
+        this.selectedTab = 'DonateHelpPage';
     }
     }
 
 
     activateVersionHistoryHelpPage() {
     activateVersionHistoryHelpPage() {
-        this.selectedTab = 'releases';
+        this.selectedTab = 'VersionHistoryPage';
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {
@@ -72,16 +91,8 @@ class HelpPage extends Vue {
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.el-tabs {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-}
-
-.el-tab-pane {
-    flex: 1;
-    display: flex;
-    overflow: hidden;
+.separator {
+    height: 1px;
+    background-color: #E0E0E0;
 }
 }
 </style>
 </style>

+ 1 - 6
client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
     <div class="page">
     <div class="page">
-        <h4>Управление с помощью горячих клавиш:</h4>
+        <span class="text-h6 text-bold">Управление с помощью горячих клавиш:</span>
         <ul>
         <ul>
             <li><b>F1, H</b> - открыть справку</li>
             <li><b>F1, H</b> - открыть справку</li>
             <li><b>Escape</b> - показать/скрыть страницу загрузки</li>
             <li><b>Escape</b> - показать/скрыть страницу загрузки</li>
@@ -42,14 +42,9 @@ class HotkeysHelpPage extends Vue {
 
 
 <style scoped>
 <style scoped>
 .page {
 .page {
-    flex: 1;
     padding: 15px;
     padding: 15px;
     overflow-y: auto;
     overflow-y: auto;
     font-size: 120%;
     font-size: 120%;
     line-height: 130%;
     line-height: 130%;
 }
 }
-
-h4 {
-    margin: 0;
-}
 </style>
 </style>

+ 1 - 6
client/components/Reader/HelpPage/MouseHelpPage/MouseHelpPage.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
     <div class="page">
     <div class="page">
-        <h4>Управление с помощью мыши/тачскрина:</h4>
+        <span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
         <ul>
         <ul>
             <li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
             <li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
                 <div class="click-map-page">
                 <div class="click-map-page">
@@ -49,17 +49,12 @@ class MouseHelpPage extends Vue {
 
 
 <style scoped>
 <style scoped>
 .page {
 .page {
-    flex: 1;
     padding: 15px;
     padding: 15px;
     overflow-y: auto;
     overflow-y: auto;
     font-size: 120%;
     font-size: 120%;
     line-height: 130%;
     line-height: 130%;
 }
 }
 
 
-h4 {
-    margin: 0;
-}
-
 .click-map-page {
 .click-map-page {
     position: relative;
     position: relative;
     width: 400px;
     width: 400px;

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

@@ -1,13 +1,14 @@
 <template>
 <template>
     <div id="versionHistoryPage" class="page">
     <div id="versionHistoryPage" class="page">
+        <span class="text-h6 text-bold">История версий:</span>
+        <br><br>
+
         <span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
         <span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
             <p>
             <p>
             {{ item }}
             {{ item }}
             </p>
             </p>
         </span>
         </span>
 
 
-        <br>
-        <h4>История версий:</h4>
         <br>
         <br>
 
 
         <div v-for="item in versionContent" :id="item.key" :key="item.key">
         <div v-for="item in versionContent" :id="item.key" :key="item.key">
@@ -58,15 +59,11 @@ class VersionHistoryPage extends Vue {
 
 
 <style scoped>
 <style scoped>
 .page {
 .page {
-    flex: 1;
     padding: 15px;
     padding: 15px;
     overflow-y: auto;
     overflow-y: auto;
     font-size: 120%;
     font-size: 120%;
     line-height: 130%;
     line-height: 130%;
-}
-
-h4 {
-    margin: 0;
+    position: relative;
 }
 }
 
 
 p {
 p {

+ 13 - 7
client/components/Reader/LoaderPage/GithubCorner/GithubCorner.vue

@@ -47,7 +47,7 @@
         default: false
         default: false
       }
       }
     },
     },
-    data () {
+    data() {
       return {
       return {
         svgStyle: {
         svgStyle: {
           fill: this.cornerColor,
           fill: this.cornerColor,
@@ -60,8 +60,13 @@
         },
         },
         flipped: false,
         flipped: false,
         svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
         svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
-        svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
-        svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
+        svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 ' + 
+          '123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
+        svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 ' + 
+          'C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 ' + 
+          '176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 ' + 
+          '216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 ' + 
+          'C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
       }
       }
     },
     },
     methods: {
     methods: {
@@ -99,7 +104,7 @@
   }
   }
 </script>
 </script>
 
 
-<style>
+<style scoped>
   #github-corner .octo-arm {
   #github-corner .octo-arm {
     transform-origin: 130px 106px
     transform-origin: 130px 106px
   }
   }
@@ -122,7 +127,8 @@
     top: 0;
     top: 0;
     border: 0;
     border: 0;
   }
   }
-    #github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
-      transition: fill 1s ease;
-    }
+
+  #github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
+    transition: fill 1s ease;
+  }
 </style>
 </style>

+ 24 - 60
client/components/Reader/LoaderPage/LoaderPage.vue

@@ -1,31 +1,36 @@
 <template>
 <template>
-    <div ref="main" class="main">
-        <GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F"></GithubCorner>
-        <div class="part top">
-            <span class="greeting bold-font">{{ title }}</span>
-            <div class="space"></div>
+    <div ref="main" class="column no-wrap" style="min-height: 500px">
+        <div class="relative-position">
+            <GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#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>
+            <div class="q-my-sm"></div>
             <span class="greeting">Добро пожаловать!</span>
             <span class="greeting">Добро пожаловать!</span>
             <span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
             <span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
             <span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
             <span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
         </div>
         </div>
 
 
-        <div class="part center">
-            <el-input ref="input" placeholder="URL книги" v-model="bookUrl">
-                <el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
-            </el-input>
-            <div class="space"></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"/>
+                </template>
+            </q-input>
+
             <input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
             <input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
 
 
-            <el-button size="mini" @click="loadFileClick">
+            <div class="q-my-sm"></div>
+            <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
                 Загрузить файл с диска
                 Загрузить файл с диска
-            </el-button>
-            <div class="space"></div>
-            <el-button size="mini" @click="loadBufferClick">
+            </q-btn>
+            
+            <div class="q-my-sm"></div>
+            <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
                 Из буфера обмена
                 Из буфера обмена
-            </el-button>
+            </q-btn>
 
 
-            <div class="space"></div>
-            <div class="space"></div>
+            <div class="q-my-md"></div>
             <div v-if="mode == 'omnireader'">
             <div v-if="mode == 'omnireader'">
                 <div ref="yaShare2" class="ya-share2" 
                 <div ref="yaShare2" class="ya-share2" 
                     data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
                     data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
@@ -34,12 +39,12 @@
                     data-url="https://omnireader.ru">
                     data-url="https://omnireader.ru">
                 </div>
                 </div>
             </div>
             </div>
-            <div class="space"></div>
+            <div class="q-my-sm"></div>
             <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
             <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
             <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
             <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
         </div>
         </div>
 
 
-        <div class="part bottom">
+        <div class="col column justify-end items-center no-wrap overflow-hidden">
             <span class="bottom-span clickable" @click="openHelp">Справка</span>
             <span class="bottom-span clickable" @click="openHelp">Справка</span>
             <span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
             <span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
 
 
@@ -188,60 +193,19 @@ class LoaderPage extends Vue {
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 </script>
 </script>
 <style scoped>
 <style scoped>
-.main {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    min-height: 480px;
-}
-
-.part {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-}
-
 .greeting {
 .greeting {
     font-size: 120%;
     font-size: 120%;
     line-height: 160%;
     line-height: 160%;
 }
 }
 
 
-.bold-font {
-    font-weight: bold;
-}
-
 .clickable {
 .clickable {
     color: blue;
     color: blue;
     text-decoration: underline;
     text-decoration: underline;
     cursor: pointer;
     cursor: pointer;
 }
 }
 
 
-.top {
-    min-height: 120px;
-}
-
-.center {
-    justify-content: flex-start;
-    padding: 0 10px 0 10px;
-    min-height: 250px;
-}
-
-.bottom {
-    justify-content: flex-end;
-}
-
 .bottom-span {
 .bottom-span {
     font-size: 70%;
     font-size: 70%;
     margin-bottom: 10px;
     margin-bottom: 10px;
 }
 }
-
-.el-input {
-    max-width: 700px;
-}
-
-.space {
-    height: 20px;
-}
 </style>
 </style>

+ 2 - 4
client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue

@@ -3,14 +3,12 @@
         <template slot="header">
         <template slot="header">
             <span style="position: relative; top: -3px">
             <span style="position: relative; top: -3px">
                 Вставьте текст и нажмите
                 Вставьте текст и нажмите
-                <span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
+                <span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
                 или F2
                 или F2
             </span>
             </span>
         </template>
         </template>
 
 
-        <div>
-            <el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
-        </div>
+        <q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
         <hr/>
         <hr/>
         <textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
         <textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
     </Window>
     </Window>

+ 35 - 41
client/components/Reader/ProgressPage/ProgressPage.vue

@@ -1,8 +1,24 @@
 <template>
 <template>
-    <div v-show="visible" class="main">
-        <div class="center">
-            <el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
-            <p class="text">{{ text }}</p>
+    <div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
+        <div class="column justify-start items-center" style="height: 250px">
+            <q-circular-progress
+                show-value
+                instant-feedback
+                font-size="13px"
+                :value="percentage"
+                size="100px"
+                :thickness="0.11"
+                color="green-7"
+                track-color="grey-4"
+                class="q-ma-md"
+            >
+                <span class="text-yellow">{{ percentage }}%</span>
+            </q-circular-progress>
+
+            <div>
+                <span class="text-yellow">{{ text }}</span>
+                <q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
+            </div>
         </div>
         </div>
     </div>
     </div>
 </template>
 </template>
@@ -11,6 +27,7 @@
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 import Vue from 'vue';
 import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
+import * as utils from '../../../share/utils';
 
 
 const ruMessage = {
 const ruMessage = {
     'start': ' ',
     'start': ' ',
@@ -33,14 +50,15 @@ class ProgressPage extends Vue {
     step = 1;
     step = 1;
     progress = 0;
     progress = 0;
     visible = false;
     visible = false;
+    iconStyle = '';
 
 
     show() {
     show() {
-        this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
-        this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
         this.text = '';
         this.text = '';
         this.totalSteps = 1;
         this.totalSteps = 1;
         this.step = 1;
         this.step = 1;
         this.progress = 0;
         this.progress = 0;
+        this.iconAngle = 0;
+        this.ani = false;
 
 
         this.visible = true;
         this.visible = true;
     }
     }
@@ -48,6 +66,7 @@ class ProgressPage extends Vue {
     hide() {
     hide() {
         this.visible = false;
         this.visible = false;
         this.text = '';
         this.text = '';
+        this.iconAngle = 0;
     }
     }
 
 
     setState(state) {
     setState(state) {
@@ -61,46 +80,21 @@ class ProgressPage extends Vue {
         this.step = (state.step ? state.step : this.step);
         this.step = (state.step ? state.step : this.step);
         this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
         this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
         this.progress = state.progress || 0;
         this.progress = state.progress || 0;
+
+        if (!this.ani) {
+            (async() => {
+                this.ani = true;
+                this.iconAngle += 30;
+                this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
+                await utils.sleep(150);
+                this.ani = false;
+            })();
+        }
     }
     }
 
 
     get percentage() {
     get percentage() {
-        let circle = document.querySelector('path[class="el-progress-circle__path"]');
-        if (circle)
-            circle.style.transition = '';
         return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
         return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
     }
     }
 }
 }
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 </script>
 </script>
-<style scoped>
-.main {
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-
-    z-index: 100;
-    background-color: rgba(0, 0, 0, 0.8);
-
-    position: absolute;
-}
-.center {
-    display: flex;
-    flex-direction: column;
-    justify-content: flex-start;
-    align-items: center;
-
-    color: white;
-    height: 300px;
-}
-
-.text {
-    color: yellow;
-}
-
-</style>
-<style>
-.el-progress__text {
-    color: lightgreen !important;
-}
-</style>

+ 129 - 127
client/components/Reader/Reader.vue

@@ -1,57 +1,67 @@
 <template>
 <template>
-    <el-container>
-        <el-header v-show="toolBarActive" height='50px'>
-            <div ref="header" class="header">
-                <el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
-                    <el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
-                </el-tooltip>
+    <div class="column no-wrap">
+        <div ref="header" class="header" v-show="toolBarActive">
+            <div ref="buttons" class="row justify-between no-wrap">
+                <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%">Загрузить книгу</q-tooltip>
+                </button>
 
 
                 <div>
                 <div>
-                    <el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
-                        <el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
-                        <el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
-                    </el-tooltip>
+                    <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%">Действие назад</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%">Действие вперед</q-tooltip>
+                    </button>
                     <div class="space"></div>
                     <div class="space"></div>
-                    <el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
-                        <el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
-                        <el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
-                        <el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
-                        <el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
-                        <el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
-                        <el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
-                            <i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
-                        </el-button>
-                    </el-tooltip>
+                    <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%">На весь экран</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%">Плавный скроллинг</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%">На страницу</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%">Найти в тексте</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%">Скопировать текст со страницы</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"/>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Принудительно обновить книгу в обход кэша</q-tooltip>
+                    </button>
                     <div class="space"></div>
                     <div class="space"></div>
-                    <el-tooltip v-show="showToolButton['offlineMode']" content="Автономный режим (без интернета)" :open-delay="1000" effect="light">
-                        <el-button ref="offlineMode" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"><i class="el-icon-connection"></i></el-button>
-                    </el-tooltip>
-                    <el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
-                        <el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
-                    </el-tooltip>
+                    <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%">Автономный режим (без интернета)</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%">Открыть недавние</q-tooltip>
+                    </button>
                 </div>
                 </div>
 
 
-                <el-tooltip content="Настроить" :open-delay="1000" effect="light">
-                    <el-button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')"><i class="el-icon-setting"></i></el-button>            
-                </el-tooltip>
+                <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%">Настроить</q-tooltip>
+                </button>
             </div>
             </div>
-        </el-header>
+        </div>
 
 
-        <el-main>
+        <div class="main col row relative-position">
             <keep-alive>
             <keep-alive>
-                <component ref="page" :is="activePage"
+                <component ref="page" class="col" :is="activePage"
                     @load-book="loadBook"
                     @load-book="loadBook"
                     @load-file="loadFile"
                     @load-file="loadFile"
                     @book-pos-changed="bookPosChanged"
                     @book-pos-changed="bookPosChanged"
@@ -72,28 +82,30 @@
                 @stop-text-search="stopTextSearch">
                 @stop-text-search="stopTextSearch">
             </SearchPage>
             </SearchPage>
             <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
             <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
-            <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
-            <SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
+            <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
+            <SettingsPage v-show="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
             <HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
             <HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
             <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
             <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
             <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
             <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
 
 
-            <el-dialog
-                title="Что нового:"
-                :visible.sync="whatsNewVisible"
-                width="80%">
+            <Dialog ref="dialog1" v-model="whatsNewVisible">
+                <template slot="header">
+                    Что нового:
+                </template>
+
                 <div style="line-height: 20px" v-html="whatsNewContent"></div>
                 <div style="line-height: 20px" v-html="whatsNewContent"></div>
 
 
                 <span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
                 <span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
-                <span slot="footer" class="dialog-footer">
-                    <el-button @click="whatsNewDisable">Больше не показывать</el-button>
+                <span slot="footer">
+                    <q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
                 </span>
                 </span>
-            </el-dialog>
+            </Dialog>
+
+            <Dialog ref="dialog2" v-model="donationVisible">
+                <template slot="header">
+                    Здравствуйте, уважаемые читатели!
+                </template>
 
 
-            <el-dialog
-                title="Здравствуйте, уважаемые читатели!"
-                :visible.sync="donationVisible"
-                width="90%">
                 <div style="word-break: normal">
                 <div style="word-break: normal">
                     Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
                     Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
 
 
@@ -110,12 +122,9 @@
 
 
                     Автор также обращается с просьбой о помощи в распространении 
                     Автор также обращается с просьбой о помощи в распространении 
                     <a href="https://omnireader.ru" target="_blank">ссылки</a>
                     <a href="https://omnireader.ru" target="_blank">ссылки</a>
-                    <el-tooltip :open-delay="500" effect="light">
-                        <template slot="content">
-                            Скопировать
-                        </template>
-                        <i class="el-icon-copy-document" style="cursor: pointer; font-size: 100%" @click="copyLink('https://omnireader.ru')"></i>
-                    </el-tooltip>
+                    <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-icon>
                     на читалку через тематические форумы, соцсети, мессенджеры и пр.
                     на читалку через тематические форумы, соцсети, мессенджеры и пр.
                     Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
                     Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
 
 
@@ -125,20 +134,20 @@
                     P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
                     P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
 
 
                     <br><br>
                     <br><br>
-                    <el-row type="flex" justify="center">
-                        <el-button type="success" round @click="openDonate">Помочь проекту</el-button>
-                    </el-row>
+                    <div class="row justify-center">
+                        <q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
+                    </div>
                 </div>
                 </div>
 
 
-                <span slot="footer" class="dialog-footer">
-                    <span class="clickable" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>                        
-                    <br><br>
-                    <el-button @click="donationDialogRemind">Напомнить позже</el-button>
+                <span slot="footer">
+                    <span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>                        
+                    <br>
+                    <q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
                 </span>
                 </span>
-            </el-dialog>
+            </Dialog>
 
 
-        </el-main>
-    </el-container>
+        </div>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>
@@ -165,6 +174,7 @@ import bookManager from './share/bookManager';
 import readerApi from '../../api/reader';
 import readerApi from '../../api/reader';
 import * as utils from '../../share/utils';
 import * as utils from '../../share/utils';
 import {versionHistory} from './versionHistory';
 import {versionHistory} from './versionHistory';
+import Dialog from '../share/Dialog.vue';
 
 
 export default @Component({
 export default @Component({
     components: {
     components: {
@@ -180,6 +190,7 @@ export default @Component({
         HelpPage,
         HelpPage,
         ClickMapPage,
         ClickMapPage,
         ServerStorage,
         ServerStorage,
+        Dialog,
     },
     },
     watch: {
     watch: {
         bookPos: function(newValue) {
         bookPos: function(newValue) {
@@ -292,8 +303,8 @@ class Reader extends Vue {
         (async() => {
         (async() => {
             await bookManager.init(this.settings);
             await bookManager.init(this.settings);
             bookManager.addEventListener(this.bookManagerEvent);
             bookManager.addEventListener(this.bookManagerEvent);
-            
-            if (this.$root.rootRoute == '/reader') {
+
+            if (this.$root.rootRoute() == '/reader') {
                 if (this.routeParamUrl) {
                 if (this.routeParamUrl) {
                     await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
                     await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
                 } else {
                 } else {
@@ -330,8 +341,13 @@ class Reader extends Vue {
 
 
     updateHeaderMinWidth() {
     updateHeaderMinWidth() {
         const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
         const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
-        if (this.$refs.header)
-            this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
+        if (this.$refs.buttons)
+            this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
+        (async() => {
+            await utils.sleep(1000);
+            if (this.$refs.header)
+                this.$refs.header.style.overflowX = 'auto';
+        })();
     }
     }
 
 
     checkSetStorageAccessKey() {
     checkSetStorageAccessKey() {
@@ -417,9 +433,9 @@ class Reader extends Vue {
     async copyLink(link) {
     async copyLink(link) {
         const result = await utils.copyTextToClipboard(link);
         const result = await utils.copyTextToClipboard(link);
         if (result)
         if (result)
-            this.$notify.success({message: `Ссылка ${link} успешно скопирована в буфер обмена`});
+            this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
         else
         else
-            this.$notify.error({message: 'Копирование не удалось'});
+            this.$root.notify.error('Копирование не удалось');
     }
     }
 
 
     openVersionHistory() {
     openVersionHistory() {
@@ -562,22 +578,9 @@ class Reader extends Vue {
     fullScreenToggle() {
     fullScreenToggle() {
         this.fullScreenActive = !this.fullScreenActive;
         this.fullScreenActive = !this.fullScreenActive;
         if (this.fullScreenActive) {
         if (this.fullScreenActive) {
-            const element = document.documentElement;
-            if (element.requestFullscreen) {
-                element.requestFullscreen();
-            } else if (element.webkitrequestFullscreen) {
-                element.webkitRequestFullscreen();
-            } else if (element.mozRequestFullscreen) {
-                element.mozRequestFullScreen();
-            }
+            this.$q.fullscreen.request();
         } else {
         } else {
-            if (document.cancelFullScreen) {
-                document.cancelFullScreen();
-            } else if (document.mozCancelFullScreen) {
-                document.mozCancelFullScreen();
-            } else if (document.webkitCancelFullScreen) {
-                document.webkitCancelFullScreen();
-            }
+            this.$q.fullscreen.exit();
         }
         }
     }
     }
 
 
@@ -600,7 +603,8 @@ class Reader extends Vue {
 
 
     setPositionToggle() {
     setPositionToggle() {
         this.setPositionActive = !this.setPositionActive;
         this.setPositionActive = !this.setPositionActive;
-        if (this.setPositionActive && this.activePage == 'TextPage' && this.mostRecentBook()) {
+        const page = this.$refs.page;
+        if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
             this.closeAllTextPages();
             this.closeAllTextPages();
             this.setPositionActive = true;
             this.setPositionActive = true;
 
 
@@ -680,6 +684,10 @@ class Reader extends Vue {
         }
         }
     }
     }
 
 
+    recentBooksClose() {
+        this.recentBooksActive = false;
+    }
+
     recentBooksToggle() {
     recentBooksToggle() {
         this.recentBooksActive = !this.recentBooksActive;
         this.recentBooksActive = !this.recentBooksActive;
         if (this.recentBooksActive) {
         if (this.recentBooksActive) {
@@ -745,7 +753,7 @@ class Reader extends Vue {
     buttonClick(button) {
     buttonClick(button) {
         const activeClass = this.buttonActiveClass(button);
         const activeClass = this.buttonActiveClass(button);
 
 
-        this.$refs[button].$el.blur();
+        this.$refs[button].blur();
 
 
         if (activeClass['tool-button-disabled'])
         if (activeClass['tool-button-disabled'])
             return;
             return;
@@ -914,6 +922,8 @@ class Reader extends Vue {
             return;
             return;
         }
         }
 
 
+        this.closeAllTextPages();
+
         let url = encodeURI(decodeURI(opts.url));
         let url = encodeURI(decodeURI(opts.url));
 
 
         if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
         if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
@@ -1020,7 +1030,7 @@ class Reader extends Vue {
         } catch (e) {
         } catch (e) {
             progress.hide(); this.progressActive = false;
             progress.hide(); this.progressActive = false;
             this.loaderActive = true;
             this.loaderActive = true;
-            this.$alert(e.message, 'Ошибка', {type: 'error'});
+            this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
         }
         }
     }
     }
 
 
@@ -1044,7 +1054,7 @@ class Reader extends Vue {
         } catch (e) {
         } catch (e) {
             progress.hide(); this.progressActive = false;
             progress.hide(); this.progressActive = false;
             this.loaderActive = true;
             this.loaderActive = true;
-            this.$alert(e.message, 'Ошибка', {type: 'error'});
+            this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
         }
         }
     }
     }
 
 
@@ -1077,7 +1087,10 @@ class Reader extends Vue {
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {
-        if (this.$root.rootRoute == '/reader') {
+        if (this.$root.rootRoute() == '/reader') {
+            if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
+                return;
+
             let handled = false;
             let handled = false;
             if (!handled && this.helpActive)
             if (!handled && this.helpActive)
                 handled = this.$refs.helpPage.keyHook(event);
                 handled = this.$refs.helpPage.keyHook(event);
@@ -1156,37 +1169,22 @@ class Reader extends Vue {
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.el-container {
-    padding: 0;
-    margin: 0;
-    height: 100%;
-}
-
-.el-header {
+.header {
     padding-left: 5px;
     padding-left: 5px;
     padding-right: 5px;
     padding-right: 5px;
     background-color: #1B695F;
     background-color: #1B695F;
     color: #000;
     color: #000;
-    overflow-x: auto;
-    overflow-y: hidden;
-}
-
-.header {
-    display: flex;
-    justify-content: space-between;
+    overflow: hidden;
+    height: 50px;
 }
 }
 
 
-.el-main {
-    position: relative;
-    display: flex;
-    padding: 0;
-    margin: 0;
+.main {
     background-color: #EBE2C9;
     background-color: #EBE2C9;
     color: #000;
     color: #000;
 }
 }
 
 
 .tool-button {
 .tool-button {
-    margin: 0 2px 0 2px;
+    margin: 0px 2px 0 2px;
     padding: 0;
     padding: 0;
     color: #3E843E;
     color: #3E843E;
     background-color: #E6EDF4;
     background-color: #E6EDF4;
@@ -1194,15 +1192,14 @@ class Reader extends Vue {
     height: 38px;
     height: 38px;
     width: 38px;
     width: 38px;
     border: 0;
     border: 0;
+    border-radius: 6px;
     box-shadow: 3px 3px 5px black;
     box-shadow: 3px 3px 5px black;
-}
-
-.tool-button + .tool-button {
-    margin: 0 2px 0 2px;
+    outline: 0;
 }
 }
 
 
 .tool-button:hover {
 .tool-button:hover {
     background-color: white;
     background-color: white;
+    cursor: pointer;
 }
 }
 
 
 .tool-button-active {
 .tool-button-active {
@@ -1217,20 +1214,19 @@ class Reader extends Vue {
 .tool-button-active:hover {
 .tool-button-active:hover {
     color: white;
     color: white;
     background-color: #81C581;
     background-color: #81C581;
+    cursor: pointer;
 }
 }
 
 
 .tool-button-disabled {
 .tool-button-disabled {
     color: lightgray;
     color: lightgray;
     background-color: gray;
     background-color: gray;
+    cursor: default;
 }
 }
 
 
 .tool-button-disabled:hover {
 .tool-button-disabled:hover {
     color: lightgray;
     color: lightgray;
     background-color: gray;
     background-color: gray;
-}
-
-i {
-    font-size: 200%;
+    cursor: default;
 }
 }
 
 
 .space {
 .space {
@@ -1247,4 +1243,10 @@ i {
     text-decoration: underline;
     text-decoration: underline;
     cursor: pointer;
     cursor: pointer;
 }
 }
+
+.copy-icon {
+    cursor: pointer;
+    font-size: 120%;
+    color: blue;
+}
 </style>
 </style>

+ 191 - 126
client/components/Reader/RecentBooksPage/RecentBooksPage.vue

@@ -1,97 +1,84 @@
 <template>
 <template>
     <Window width="600px" ref="window" @close="close">
     <Window width="600px" ref="window" @close="close">
         <template slot="header">
         <template slot="header">
-            <span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
-            <span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
+            <span v-show="!loading">{{ header }}</span>
+            <span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
         </template>
         </template>
 
 
         <a ref="download" style='display: none;'></a>
         <a ref="download" style='display: none;'></a>
-        <el-table
+
+        <q-table
+            class="recent-books-table col"
             :data="tableData"
             :data="tableData"
-            style="width: 570px"
-            size="mini"
-            height="1px"
-            stripe
-            border
-            :default-sort = "{prop: 'touchDateTime', order: 'descending'}"
-            :header-cell-style = "headerCellStyle"
-            :row-key = "rowKey"
-            >
-
-            <el-table-column
-                type="index"
-                width="35px"
-                >
-            </el-table-column>
-            <el-table-column
-                prop="touchDateTime"
-                min-width="85px"
-                sortable
-                >
-                <template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
-                    <span style="font-size: 90%">Время<br>просм.</span>
-                </template>
-                <template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
-                    <div class="desc" @click="loadBook(scope.row.url)">
-                        {{ scope.row.touchDate }}<br>
-                        {{ scope.row.touchTime }}
-                    </div>
-                </template>
-            </el-table-column>
-
-            <el-table-column
-                >
-                <template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
-                    <!--el-input ref="input"
-                        :value="search" @input="search = $event"
-                        size="mini"
-                        style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
-                        placeholder="Найти"/-->
-                        <div class="el-input el-input--mini">
-                            <input class="el-input__inner"
-                                ref="input"
-                                placeholder="Найти"
-                                style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
-                                :value="search" @input="search = $event.target.value"
-                            />
+            :columns="columns"
+            row-key="key"
+            :pagination.sync="pagination"
+            separator="cell"
+            hide-bottom
+            virtual-scroll
+            dense
+        > 
+            <template v-slot: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"
+                            @click.stop
+                        />
+
+                        <span v-html="props.cols[2].label"></span>
+                    </q-th>
+                </q-tr>
+            </template>
+
+            <template v-slot:body="props">
+                <q-tr :props="props">
+                    <q-td key="num" :props="props" class="td-mp" auto-width>
+                        <div class="break-word" style="width: 25px">
+                            {{ props.row.num }}
                         </div>
                         </div>
-                </template>
-
-                <el-table-column
-                    min-width="280px"
-                    >
-                    <template slot-scope="scope">
-                        <div class="desc" @click="loadBook(scope.row.url)">
-                            <span style="color: green">{{ scope.row.desc.author }}</span><br>
-                            <span>{{ scope.row.desc.title }}</span>
+                    </q-td>
+
+                    <q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
+                        <div class="break-word" style="width: 68px">
+                            {{ props.row.touchDate }}<br>
+                            {{ props.row.touchTime }}
                         </div>
                         </div>
-                    </template>
-                </el-table-column>
-
-                <el-table-column
-                    min-width="90px"
-                    >
-                    <template slot-scope="scope">
-                        <a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
-                        <a :href="scope.row.path" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
-                    </template>
-                </el-table-column>
-
-                <el-table-column
-                    width="60px"
-                    >
-                    <template slot-scope="scope">
-                        <el-button
-                            size="mini"
-                            style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
-                            @click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
-                        </el-button>
-                    </template>
-                </el-table-column>
-
-            </el-table-column>
-
-        </el-table>
+                    </q-td>
+
+                    <q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
+                        <div class="break-word" style="width: 332px; font-size: 90%">
+                            <div style="color: green">{{ props.row.desc.author }}</div>
+                            <div>{{ props.row.desc.title }}</div>
+                        </div>
+                    </q-td>
+
+                    <q-td key="links" :props="props" class="td-mp" auto-width>
+                        <div class="break-word" style="width: 75px; font-size: 90%">
+                            <a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
+                            <a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
+                        </div>
+                    </q-td>
+
+                    <q-td key="close" :props="props" class="td-mp" auto-width>
+                        <div style="width: 38px">
+                            <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"/>
+                            </q-btn>
+                        </div>
+                    </q-td>
+                    <q-td key="last" :props="props" class="no-mp">
+                    </q-td>
+                </q-tr>
+            </template>
+        </q-table>
+
     </Window>
     </Window>
 </template>
 </template>
 
 
@@ -121,52 +108,90 @@ class RecentBooksPage extends Vue {
     loading = false;
     loading = false;
     search = null;
     search = null;
     tableData = [];
     tableData = [];
+    columns = [];
+    pagination = {};
 
 
     created() {
     created() {
+        this.pagination = {rowsPerPage: 0};
+
+        this.columns = [
+            {
+                name: 'num',
+                label: '#',
+                align: 'center',
+                sortable: true,
+                field: 'num',
+            },
+            {
+                name: 'date',
+                label: 'Время<br>просм.',
+                align: 'left',
+                field: 'touchDateTime',
+                sortable: true,
+                sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
+            },
+            {
+                name: 'desc',
+                label: 'Название',
+                align: 'left',
+                field: 'descString',
+                sortable: true,
+            },
+            {
+                name: 'links',
+                label: '',
+                align: 'left',
+            },
+            {
+                name: 'close',
+                label: '',
+                align: 'left',
+            },
+            {
+                name: 'last',
+                label: '',
+                align: 'left',
+            },
+        ];
     }
     }
 
 
     init() {
     init() {
         this.$refs.window.init();
         this.$refs.window.init();
 
 
         this.$nextTick(() => {
         this.$nextTick(() => {
-            //this.$refs.input.focus();
+            //this.$refs.input.focus();//плохо на планшетах
         });
         });
-        (async() => {//отбражение подгрузки списка, иначе тормозит
+        (async() => {//подгрузка списка
             if (this.initing)
             if (this.initing)
                 return;
                 return;
             this.initing = true;
             this.initing = true;
 
 
-            await this.updateTableData(3);
-            await utils.sleep(200);
 
 
-            if (bookManager.loaded) {
-                const t = Date.now();
+            if (!bookManager.loaded) {
                 await this.updateTableData(10);
                 await this.updateTableData(10);
-                if (bookManager.getSortedRecent().length > 10)
-                    await utils.sleep(10*(Date.now() - t));
-            } else {
+                //для отзывчивости
+                await utils.sleep(100);
                 let i = 0;
                 let i = 0;
                 let j = 5;
                 let j = 5;
                 while (i < 500 && !bookManager.loaded) {
                 while (i < 500 && !bookManager.loaded) {
                     if (i % j == 0) {
                     if (i % j == 0) {
                         bookManager.sortedRecentCached = null;
                         bookManager.sortedRecentCached = null;
-                        await this.updateTableData(100);
+                        await this.updateTableData(20);
                         j *= 2;
                         j *= 2;
                     }
                     }
 
 
                     await utils.sleep(100);
                     await utils.sleep(100);
                     i++;
                     i++;
                 }
                 }
+            } else {
+                //для отзывчивости
+                await utils.sleep(100);
             }
             }
             await this.updateTableData();
             await this.updateTableData();
             this.initing = false;
             this.initing = false;
         })();
         })();
     }
     }
 
 
-    rowKey(row) {
-        return row.key;
-    }
-
     async updateTableData(limit) {
     async updateTableData(limit) {
         while (this.updating) await utils.sleep(100);
         while (this.updating) await utils.sleep(100);
         this.updating = true;
         this.updating = true;
@@ -175,11 +200,13 @@ class RecentBooksPage extends Vue {
         this.loading = !!limit;
         this.loading = !!limit;
         const sorted = bookManager.getSortedRecent();
         const sorted = bookManager.getSortedRecent();
 
 
+        let num = 0;
         for (let i = 0; i < sorted.length; i++) {
         for (let i = 0; i < sorted.length; i++) {
             const book = sorted[i];
             const book = sorted[i];
             if (book.deleted)
             if (book.deleted)
                 continue;
                 continue;
 
 
+            num++;
             if (limit && result.length >= limit)
             if (limit && result.length >= limit)
                 break;
                 break;
 
 
@@ -221,19 +248,19 @@ class RecentBooksPage extends Vue {
             author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
             author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
 
 
             result.push({
             result.push({
+                num,
                 touchDateTime: book.touchTime,
                 touchDateTime: book.touchTime,
                 touchDate: t[0],
                 touchDate: t[0],
                 touchTime: t[1],
                 touchTime: t[1],
                 desc: {
                 desc: {
-                    title: `${title}${perc}${textLen}`,
                     author,
                     author,
+                    title: `${title}${perc}${textLen}`,
                 },
                 },
+                descString: `${author}${title}${perc}${textLen}`,
                 url: book.url,
                 url: book.url,
                 path: book.path,
                 path: book.path,
                 key: book.key,
                 key: book.key,
             });
             });
-            if (result.length >= 100)
-                break;
         }
         }
 
 
         const search = this.search;
         const search = this.search;
@@ -245,29 +272,23 @@ class RecentBooksPage extends Vue {
                 item.desc.author.toLowerCase().includes(search.toLowerCase())
                 item.desc.author.toLowerCase().includes(search.toLowerCase())
         });
         });
 
 
-        /*for (let i = 0; i < result.length; i++) {
-            if (!_.isEqual(this.tableData[i], result[i])) {
-                this.$set(this.tableData, i, result[i]);
-                await utils.sleep(10);
-            }
-        }
-        if (this.tableData.length > result.length)
-            this.tableData.splice(result.length);*/
-
         this.tableData = result;
         this.tableData = result;
         this.updating = false;
         this.updating = false;
     }
     }
 
 
-    headerCellStyle(cell) {
-        let result = {margin: 0, padding: 0};
-        if (cell.columnIndex > 0) {
-            result['border-bottom'] = 0;
+    wordEnding(num) {
+        const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
+        const deci = num % 100;
+        if (deci > 10 && deci < 20) {
+            return '';
+        } else {
+            return endings[num % 10];
         }
         }
-        if (cell.rowIndex > 0) {
-            result.height = '0px';
-            result['border-right'] = 0;
-        }
-        return result;
+    }
+
+    get header() {
+        const len = (this.tableData ? this.tableData.length : 0);
+        return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
     }
     }
 
 
     async downloadBook(fb2path) {
     async downloadBook(fb2path) {
@@ -282,7 +303,7 @@ class RecentBooksPage extends Vue {
             let errMes = e.message;
             let errMes = e.message;
             if (errMes.indexOf('404') >= 0)
             if (errMes.indexOf('404') >= 0)
                 errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
                 errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
-            this.$alert(errMes, 'Ошибка', {type: 'error'});
+            this.$root.stdDialog.alert(errMes, 'Ошибка', {type: 'negative'});
         }
         }
     }
     }
 
 
@@ -296,7 +317,7 @@ class RecentBooksPage extends Vue {
 
 
     async handleDel(key) {
     async handleDel(key) {
         await bookManager.delRecentBook({key});
         await bookManager.delRecentBook({key});
-        this.updateTableData();
+        //this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
 
 
         if (!bookManager.mostRecentBook())
         if (!bookManager.mostRecentBook())
             this.close();
             this.close();
@@ -315,11 +336,11 @@ class RecentBooksPage extends Vue {
     }
     }
 
 
     close() {
     close() {
-        this.$emit('recent-books-toggle');
+        this.$emit('recent-books-close');
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {
-        if (event.type == 'keydown' && event.code == 'Escape') {
+        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
             this.close();
             this.close();
         }
         }
         return true;
         return true;
@@ -329,7 +350,51 @@ class RecentBooksPage extends Vue {
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.desc {
+.recent-books-table {
+    width: 600px;
+    overflow-y: auto;
+    overflow-x: hidden;
+}
+
+.clickable {
     cursor: pointer;
     cursor: pointer;
 }
 }
-</style>
+
+.td-mp {
+    margin: 0 !important;
+    padding: 4px 4px 4px 4px !important;
+    border-bottom: 1px solid #ddd;
+}
+
+.no-mp {
+    margin: 0 !important;
+    padding: 0 !important;
+    border: 0;
+    border-left: 1px solid #ddd !important;
+}
+
+.break-word {
+    line-height: 180%;
+    overflow-wrap: break-word;
+    word-wrap: break-word;
+    white-space: normal;
+}
+
+</style>
+
+<style>
+.recent-books-table .q-table__middle {
+    height: 100%;
+    overflow-x: hidden;
+}
+
+.recent-books-table thead tr:first-child th {
+    position: sticky;
+    z-index: 1;
+    top: 0;
+    background-color: #c1f4cd;
+}
+.recent-books-table tr:nth-child(even) {
+    background-color: #f8f8f8;
+}
+</style>

+ 22 - 17
client/components/Reader/SearchPage/SearchPage.vue

@@ -8,15 +8,19 @@
             <span v-show="initStep">{{ initPercentage }}%</span>
             <span v-show="initStep">{{ initPercentage }}%</span>
 
 
             <div v-show="!initStep" class="input">
             <div v-show="!initStep" class="input">
-                <input ref="input" class="el-input__inner"
+                <!--input ref="input"
                     placeholder="что ищем"
                     placeholder="что ищем"
-                    :value="needle" @input="needle = $event.target.value"/>
+                    :value="needle" @input="needle = $event.target.value"/-->
+                <q-input ref="input" class="col" outlined dense
+                    placeholder="что ищем"
+                    v-model="needle" @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>
             </div>
-            <el-button-group v-show="!initStep" class="button-group">
-                <el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
-                <el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
-            </el-button-group>
+            <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-group>
         </div>
         </div>
     </Window>
     </Window>
 </template>
 </template>
@@ -39,7 +43,10 @@ export default @Component({
 
 
         },
         },
         foundText: function(newValue) {
         foundText: function(newValue) {
-            this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
+            //недостатки сторонних ui
+            const el = this.$refs.input.$el.querySelector('label div div div input');
+            if (el)
+                el.style.paddingRight = newValue.length*12 + 'px';
         },
         },
     },
     },
 })
 })
@@ -160,12 +167,13 @@ class SearchPage extends Vue {
         this.$emit('search-toggle');
         this.$emit('search-toggle');
     }
     }
 
 
-    keyHook(event) {
-        //недостатки сторонних ui
-        if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
+    inputKeyDown(event) {
+        if (event.key == 'Enter') {
             this.showNext();
             this.showNext();
         }
         }
+    }
 
 
+    keyHook(event) {
         if (event.type == 'keydown' && (event.code == 'Escape')) {
         if (event.type == 'keydown' && (event.code == 'Escape')) {
             this.close();
             this.close();
         }
         }
@@ -194,17 +202,14 @@ class SearchPage extends Vue {
 }
 }
 
 
 .button-group {
 .button-group {
-    width: 150px;
+    width: 100px;
     margin: 0;
     margin: 0;
     padding: 0;
     padding: 0;
+    height: 37px;
 }
 }
 
 
-.el-button {
+.button {
     padding: 9px 17px 9px 17px;
     padding: 9px 17px 9px 17px;
-    width: 55px;
-}
-
-i {
-    font-size: 20px;
+    width: 50px;
 }
 }
 </style>
 </style>

+ 3 - 3
client/components/Reader/ServerStorage/ServerStorage.vue

@@ -177,17 +177,17 @@ class ServerStorage extends Vue {
 
 
     success(message) {
     success(message) {
         if (this.showServerStorageMessages)
         if (this.showServerStorageMessages)
-            this.$notify.success({message});
+            this.$root.notify.success(message);
     }
     }
 
 
     warning(message) {
     warning(message) {
         if (this.showServerStorageMessages && !this.offlineModeActive)
         if (this.showServerStorageMessages && !this.offlineModeActive)
-            this.$notify.warning({message});
+            this.$root.notify.warning(message);
     }
     }
 
 
     error(message) {
     error(message) {
         if (this.showServerStorageMessages && !this.offlineModeActive)
         if (this.showServerStorageMessages && !this.offlineModeActive)
-            this.$notify.error({message});
+            this.$root.notify.error(message);
     }
     }
 
 
     async loadSettings(force = false, doNotifySuccess = true) {
     async loadSettings(force = false, doNotifySuccess = true) {

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

@@ -4,8 +4,15 @@
             Установить позицию
             Установить позицию
         </template>
         </template>
 
 
-        <div class="slider">
-            <el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
+        <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"
+                :max="sliderMax"
+                label
+                :label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
+                color="primary"
+            />
         </div>
         </div>
     </Window>
     </Window>
 </template>
 </template>
@@ -46,13 +53,6 @@ class SetPositionPage extends Vue {
         this.initialized = true;
         this.initialized = true;
     }
     }
 
 
-    formatTooltip(val) {
-        if (this.sliderMax)
-            return (val/this.sliderMax*100).toFixed(2) + '%';
-        else
-            return 0;
-    }
-
     close() {
     close() {
         this.$emit('set-position-toggle');
         this.$emit('set-position-toggle');
     }
     }
@@ -73,9 +73,13 @@ class SetPositionPage extends Vue {
     background-color: #efefef;
     background-color: #efefef;
     border-radius: 15px;
     border-radius: 15px;
 }
 }
+</style>
 
 
-.el-slider {
-    margin-right: 20px;
-    margin-left: 20px;
+<style>
+#set-position-slider .q-slider__thumb path {
+    fill: white !important;
+    stroke: blue !important;
+    stroke-width: 2 !important;
 }
 }
+
 </style>
 </style>

+ 249 - 611
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -1,530 +1,69 @@
-<template>
+<template lang="includer">
     <Window ref="window" height="95%" width="600px" @close="close">
     <Window ref="window" height="95%" width="600px" @close="close">
         <template slot="header">
         <template slot="header">
             Настройки
             Настройки
         </template>
         </template>
 
 
-        <el-tabs type="border-card" tab-position="left" v-model="selectedTab">
-            <!-- Профили ------------------------------------------------------------------------->
-            <el-tab-pane label="Профили">
-                <el-form :model="form" size="small" label-width="80px" @submit.native.prevent>
-                    <div class="partHeader">Управление синхронизацией данных</div>
-                    <el-form-item label="">
-                        <el-checkbox v-model="serverSyncEnabled">Включить синхронизацию с сервером</el-checkbox>
-                    </el-form-item>
-                </el-form>
-
-                <div v-show="serverSyncEnabled">
-                <el-form :model="form" size="small" label-width="80px" @submit.native.prevent>
-                    <div class="partHeader">Профили устройств</div>
-
-                    <el-form-item label="">
-                        <div class="text">
-                            Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
-                            <br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
-                        </div>
-                    </el-form-item>
-
-                    <el-form-item label="Устройство">
-                        <el-select v-model="currentProfile" placeholder="">
-                            <el-option label="Нет" value=""></el-option>
-                            <el-option v-for="item in profilesArray"
-                                :key="item"
-                                :label="item"
-                                :value="item">
-                            </el-option>
-                        </el-select>
-                    </el-form-item>
-
-                    <el-form-item label="">
-                            <el-button @click="addProfile">Добавить</el-button>
-                            <el-button @click="delProfile">Удалить</el-button>
-                            <el-button @click="delAllProfiles">Удалить все</el-button>
-                    </el-form-item>
-                </el-form>
-
-                <el-form :model="form" size="small" label-width="80px" @submit.native.prevent>
-                    <div class="partHeader">Ключ доступа</div>
-
-                    <el-form-item label="">
-                        <div class="text">
-                            Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
-                            Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
-                        </div>
-                    </el-form-item>
-
-                    <el-form-item label="">
-                            <el-button style="width: 250px" @click="showServerStorageKey">
-                                <span v-show="serverStorageKeyVisible">Скрыть</span>
-                                <span v-show="!serverStorageKeyVisible">Показать</span>
-                                ключ доступа
-                         </el-button>
-                    </el-form-item>
-
-                    <el-form-item label="">
-                        <div v-if="!serverStorageKeyVisible">
-                            <hr/>
-                            <b>{{ partialStorageKey }}</b> (часть вашего ключа)
-                            <hr/>
-                        </div>
-                        <div v-else style="line-height: 100%">
-                            <hr/>
-                            <div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;"><b>{{ serverStorageKey }}</b></div>
-                            <br><div class="center">
-                                <el-button size="mini" class="copy-button" @click="copyToClip(serverStorageKey, 'Ключ')">Скопировать ключ</el-button>
-                            </div>
-                            <div v-if="mode == 'omnireader'">
-                                <br>Переход по ссылке позволит автоматически ввести ключ доступа:
-                                <br><div class="center" style="margin-top: 5px">
-                                    <a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
-                                </div>
-                                <br><div class="center">
-                                    <el-button size="mini" class="copy-button" @click="copyToClip(setStorageKeyLink, 'Ссылка')">Скопировать ссылку</el-button>
-                                </div>
-                            </div>
-                            <hr/>
-                        </div>
-                    </el-form-item>
-
-                    <el-form-item label="">
-                            <el-button style="width: 250px" @click="enterServerStorageKey">Ввести ключ доступа</el-button>
-                    </el-form-item>
-                    <el-form-item label="">
-                            <el-button style="width: 250px" @click="generateServerStorageKey">Сгенерировать новый ключ</el-button>
-                    </el-form-item>
-
-                    <el-form-item label="">
-                        <div class="text">
-                            Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
-                            например, после переустановки ОС или чистки/смены браузера.<br>
-                            <b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
-                            и шифруются ключом доступа перед отправкой на сервер.
-                        </div>
-                    </el-form-item>
-                </el-form>
+        <div class="col row">
+            <div class="full-height">
+                <q-tabs
+                    ref="tabs"
+                    class="bg-grey-3 text-black"
+                    v-model="selectedTab"
+                    left-icon="la la-caret-up"
+                    right-icon="la la-caret-down"
+                    active-color="white"
+                    active-bg-color="primary"
+                    indicator-color="black"
+                    vertical
+                    no-caps
+                    stretch
+                    inline-label
+                >
+                    <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="Кнопки" />
+                    <q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
+                    <q-tab class="tab" name="pagemove" icon="la la-school" 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"/>
+                </q-tabs>
+            </div>
+
+            <div class="col fit">
+                <!-- Профили --------------------------------------------------------------------->
+                <div v-if="selectedTab == 'profiles'" class="fit tab-panel">
+                    @@include('./include/ProfilesTab.inc');
                 </div>
                 </div>
-            </el-tab-pane>
-
-            <!-- Вид ------------------------------------------------------------------------->                    
-            <el-tab-pane label="Вид">
-
-                <el-form :model="form" size="small" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Цвет</div>
-
-                    <el-form-item label="Текст">
-                        <el-col :span="12">
-                            <el-color-picker v-model="textColor" color-format="hex" :predefine="predefineTextColors"></el-color-picker>
-                            <span class="color-picked"><b>{{ textColor }}</b></span>
-                        </el-col>
-                        <el-col :span="5">
-                            <span style="position: relative; top: 20px;">Обои:</span>
-                        </el-col>
-                    </el-form-item>
-
-                    <el-form-item label="Фон">
-                        <el-col :span="12">
-                            <el-color-picker v-model="backgroundColor" color-format="hex" :predefine="predefineBackgroundColors" :disabled="wallpaper != ''"></el-color-picker>
-                            <span v-show="wallpaper == ''" class="color-picked"><b>{{ backgroundColor }}</b></span>
-                        </el-col>
-
-                        <el-col :span="11">
-                            <el-select v-model="wallpaper">
-                                <el-option label="Нет" value=""></el-option>
-                                <el-option label="1" value="paper1"></el-option>
-                                <el-option label="2" value="paper2"></el-option>
-                                <el-option label="3" value="paper3"></el-option>
-                                <el-option label="4" value="paper4"></el-option>
-                                <el-option label="5" value="paper5"></el-option>
-                                <el-option label="6" value="paper6"></el-option>
-                                <el-option label="7" value="paper7"></el-option>
-                                <el-option label="8" value="paper8"></el-option>
-                                <el-option label="9" value="paper9"></el-option>
-                            </el-select>
-                        </el-col>
-                    </el-form-item>
-                </el-form>
-
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Шрифт</div>
-
-                    <el-form-item label="Локальный/веб">
-                        <el-col :span="11">
-                            <el-select v-model="fontName" placeholder="Шрифт" :disabled="webFontName != ''">
-                                <el-option v-for="item in fonts"
-                                    :key="item.name"
-                                    :label="item.label"
-                                    :value="item.name">
-                                </el-option>
-                            </el-select>
-                        </el-col>
-                        <el-col :span="1">
-                            &nbsp;
-                        </el-col>
-                        <el-col :span="11">
-                            <el-tooltip :open-delay="500" effect="light" placement="top">
-                                <template slot="content">
-                                    Веб шрифты дают большое разнообразие,<br>
-                                    однако есть шанс, что шрифт будет загружаться<br>
-                                    очень медленно или вовсе не загрузится
-                                </template>
-                                <el-select v-model="webFontName">
-                                    <el-option label="Нет" value=""></el-option>
-                                    <el-option v-for="item in webFonts"
-                                        :key="item.name"
-                                        :value="item.name">
-                                    </el-option>
-                                </el-select>
-                            </el-tooltip>
-                        </el-col>
-                    </el-form-item>
-                    <el-form-item label="Размер">
-                        <el-col :span="17">
-                            <el-input-number v-model="fontSize" :min="5" :max="200"></el-input-number>
-                        </el-col>
-                        <el-col :span="1">
-                            <a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
-                        </el-col>
-                    </el-form-item>
-                    <el-form-item label="Сдвиг">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Сдвиг шрифта по вертикали в процентах от размера.<br>
-                                Отрицательное значение сдвигает вверх, положительное -<br>
-                                вниз. Значение зависит от метрики шрифта.
-                            </template>
-                            <el-input-number v-model="vertShift" :min="-100" :max="100"></el-input-number>
-                        </el-tooltip>
-                    </el-form-item>
-
-                    <el-form-item label="Стиль">
-                        <el-col :span="8">
-                            <el-checkbox v-model="fontBold">Жирный</el-checkbox>
-                        </el-col>
-                        <el-col :span="8">
-                            <el-checkbox v-model="fontItalic">Курсив</el-checkbox>
-                        </el-col>
-                    </el-form-item>
-                </el-form>
-
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Текст</div>
-
-                    <el-form-item label="Интервал">
-                        <el-input-number v-model="lineInterval" :min="0" :max="200"></el-input-number>
-                    </el-form-item>
-                    <el-form-item label="Параграф">
-                        <el-input-number v-model="p" :min="0" :max="2000"></el-input-number>
-                    </el-form-item>
-                    <el-form-item label="Отступ">
-                        <el-col :span="11">
-                            <el-tooltip :open-delay="500" effect="light">
-                                <template slot="content">
-                                    Слева/справа
-                                </template>
-                                <el-input-number v-model="indentLR" :min="0" :max="2000"></el-input-number>
-                            </el-tooltip>
-                        </el-col>
-                        <el-col :span="1">
-                            &nbsp;
-                        </el-col>
-                        <el-col :span="11">
-                            <el-tooltip :open-delay="500" effect="light">
-                                <template slot="content">
-                                    Сверху/снизу
-                                </template>
-                                <el-input-number v-model="indentTB" :min="0" :max="2000"></el-input-number>
-                            </el-tooltip>
-                        </el-col>
-                    </el-form-item>
-                    <el-form-item label="Сдвиг">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Сдвиг текста по вертикали в процентах от размера шрифта.<br>
-                                Отрицательное значение сдвигает вверх, положительное -<br>
-                                вниз.
-                            </template>
-                            <el-input-number v-model="textVertShift" :min="-100" :max="100"></el-input-number>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Скроллинг">
-                        <el-col :span="11">
-                            <el-tooltip :open-delay="500" effect="light">
-                                <template slot="content">
-                                    Замедление скроллинга в миллисекундах.<br>
-                                    Определяет время, за которое текст<br>
-                                    прокручивается на одну строку.
-                                </template>
-                                <el-input-number v-model="scrollingDelay" :min="1" :max="10000"></el-input-number>
-                            </el-tooltip>
-                        </el-col>
-                        <el-col :span="1">
-                            &nbsp;
-                        </el-col>
-                        <el-col :span="11">
-                            <el-tooltip :open-delay="500" effect="light" placement="top">
-                                <template slot="content">
-                                    Вид скроллинга: линейный,<br>
-                                    ускорение-замедление и пр.
-                                </template>
-
-                                <el-select v-model="scrollingType">
-                                    <el-option value="linear"></el-option>
-                                    <el-option value="ease"></el-option>
-                                    <el-option value="ease-in"></el-option>
-                                    <el-option value="ease-out"></el-option>
-                                    <el-option value="ease-in-out"></el-option>
-                                </el-select>
-                            </el-tooltip>
-                        </el-col>
-
-                    </el-form-item>
-                    <el-form-item label="Выравнивание">
-                        <el-checkbox v-model="textAlignJustify">По ширине</el-checkbox>
-                        <el-checkbox v-model="wordWrap">Перенос по слогам</el-checkbox>
-                    </el-form-item>
-                    <el-form-item label="">
-                        <el-col :span="12">
-                            Компактность
-                        </el-col>
-                        <el-tooltip :open-delay="500" effect="light" placement="top">
-                            <template slot="content">
-                                Степень компактности текста в процентах.<br>
-                                Чем больше компактность, тем хуже выравнивание<br>
-                                по правому краю.
-                            </template>
-                            <el-input-number v-model="compactTextPerc" :min="0" :max="100"></el-input-number>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Обработка">
-                        <el-checkbox v-model="cutEmptyParagraphs">Убирать пустые строки</el-checkbox>
-                    </el-form-item>
-                    <el-form-item label="">
-                        <el-col :span="12">
-                            Добавлять пустые
-                        </el-col>
-                        <el-input-number v-model="addEmptyParagraphs" :min="0" :max="2"></el-input-number>
-                    </el-form-item>
-                    <el-form-item label="">
-                        <el-tooltip :open-delay="500" effect="light" placement="top">
-                            <template slot="content">
-                                Html-фильтр вырезает лишние элементы со<br>
-                                страницы для определенных сайтов, таких как:<br>
-                                samlib.ru<br>
-                                www.fanfiction.net<br>
-                                archiveofourown.org<br>
-                                и других
-                            </template>
-                            <el-checkbox v-model="enableSitesFilter" @change="needTextReload">Включить html-фильтр для сайтов</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    
-                    <el-form-item label="Изображения">
-                        <el-col :span="11">
-                            <el-checkbox v-model="showImages">Показывать</el-checkbox>
-                        </el-col>
-
-                        <el-col :span="1">
-                            &nbsp;
-                        </el-col>
-                        <el-col :span="11">
-                            <el-tooltip :open-delay="500" effect="light" placement="top">
-                                <template slot="content">
-                                    Выносить все изображения в центр экрана
-                                </template>
-                                <el-checkbox v-model="showInlineImagesInCenter" @change="needReload" :disabled="!showImages">Инлайн в центр</el-checkbox>
-                            </el-tooltip>
-                        </el-col>
-                    </el-form-item>
-                    <el-form-item label="">
-                        <el-checkbox v-model="imageFitWidth" :disabled="!showImages">Ширина не более размера экрана</el-checkbox>
-                    </el-form-item>
-                    <el-form-item label="">
-                            <el-col :span="12">
-                                Высота не более
-                            </el-col>
-                            <el-tooltip :open-delay="500" effect="light" placement="top">
-                                <template slot="content">
-                                    Определяет высоту изображения количеством строк.<br>
-                                    В случае превышения высоты, изображение будет<br>
-                                    уменьшено с сохранением пропорций так, чтобы<br>
-                                    помещаться в указанное количество строк.
-                                </template>
-                                <el-input-number v-model="imageHeightLines" :min="1" :max="100" :disabled="!showImages"></el-input-number>
-                            </el-tooltip>
-                    </el-form-item>
-                </el-form>
-
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Строка статуса</div>
-
-                    <el-form-item label="Статус">
-                        <el-checkbox v-model="showStatusBar">Показывать</el-checkbox>
-                        <el-checkbox v-model="statusBarTop" :disabled="!showStatusBar">Вверху/внизу</el-checkbox>
-                    </el-form-item>
-                    <el-form-item label="Высота">
-                        <el-input-number v-model="statusBarHeight" :min="5" :max="100" :disabled="!showStatusBar"></el-input-number>
-                    </el-form-item>
-                    <el-form-item label="Прозрачность">
-                        <el-input-number v-model="statusBarColorAlpha" :min="0" :max="1" :precision="2" :step="0.1" :disabled="!showStatusBar"></el-input-number>
-                    </el-form-item>
-                </el-form>
-            </el-tab-pane>
-
-            <!-- Кнопки ------------------------------------------------------------------------->
-            <el-tab-pane label="Кнопки">
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Показывать кнопки панели</div>
-
-                    <el-form-item label="" v-for="item in toolButtons" :key="item.name">
-                        <el-checkbox @change="changeShowToolButton(item.name)" :value="showToolButton[item.name]">{{item.text}}</el-checkbox>
-                    </el-form-item>
-                </el-form>
-            </el-tab-pane>
-
-            <!-- Управление ------------------------------------------------------------------------->
-            <el-tab-pane label="Управление">
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Управление</div>
-
-                    <el-form-item label="">
-                        <el-checkbox v-model="clickControl">Включить управление кликом</el-checkbox>
-                    </el-form-item>
-                </el-form>
-            </el-tab-pane>
-
-            <!-- Листание ------------------------------------------------------------------------->
-            <el-tab-pane label="Листание">
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Анимация</div>
-
-                    <el-form-item label="Тип">
-                        <el-col :span="11">
-                            <el-select v-model="pageChangeAnimation">
-                                <el-option label="Нет" value=""></el-option>
-                                <el-option label="Вверх-вниз" value="downShift"></el-option>
-                                <el-option label="Вправо-влево" value="rightShift"></el-option>
-                                <el-option label="Протаивание" value="thaw"></el-option>
-                                <el-option label="Мерцание" value="blink"></el-option>
-                                <el-option label="Вращение" value="rotate"></el-option>
-                                <el-option v-show="wallpaper == ''" label="Листание" value="flip"></el-option>
-                            </el-select>
-                        </el-col>
-                    </el-form-item>
-
-                    <el-form-item label="Скорость">
-                        <el-input-number v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disabled="pageChangeAnimation == ''"></el-input-number>
-                    </el-form-item>
-                </el-form>
-
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Другое</div>
-
-                    <el-form-item label="Страница">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Переносить последнюю строку страницы<br>
-                                в начало следующей при листании
-                            </template>
-                            <el-checkbox v-model="keepLastToFirst">Переносить последнюю строку</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                </el-form>
-            </el-tab-pane>
-
-            <!-- Прочее ------------------------------------------------------------------------->
-            <el-tab-pane label="Прочее">
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Подсказки, уведомления</div>
-
-                    <el-form-item label="Подсказка">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Показывать или нет подсказку при каждой загрузке книги
-                            </template>
-                            <el-checkbox v-model="showClickMapPage" :disabled="!clickControl">Показывать области управления кликом</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Подсказка">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Мерцать сообщением в строке статуса и на кнопке<br>
-                                обновления при загрузке книги из кэша
-                            </template>
-                            <el-checkbox v-model="blinkCachedLoad">Предупреждать о загрузке из кэша</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Уведомление">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Показывать уведомления и ошибки от<br>
-                                синхронизатора данных с сервером
-                            </template>
-                            <el-checkbox v-model="showServerStorageMessages">Показывать сообщения синхронизации</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Уведомление">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Показывать уведомления "Что нового"<br>
-                                при каждом выходе новой версии читалки
-                            </template>
-                            <el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Уведомление">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Показывать уведомление "Оплатим хостинг вместе"
-                            </template>
-                            <el-checkbox v-model="showDonationDialog2020">Показывать "Оплатим хостинг вместе"</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                </el-form>
-
-                <el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
-                    <div class="partHeader">Другое</div>
-
-                    <el-form-item label="Парам. в URL">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Добавление параметра "__p" в строке браузера<br>
-                                позволяет передавать ссылку на книгу в читалке<br>
-                                без потери текущей позиции. Однако в этом случае<br>
-                                при листании забивается история браузера, т.к. на<br>
-                                каждое изменение позиции происходит смена URL.
-                            </template>
-                            <el-checkbox v-model="allowUrlParamBookPos">Добавлять параметр "__p"</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Парсинг">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Включение этой опции позволяет делать предварительную<br>
-                                обработку текста в ленивом режиме сразу после загрузки<br>
-                                книги. Это может повысить отзывчивость читалки, но<br>
-                                нагружает процессор каждый раз при открытии книги.
-                            </template>
-                            <el-checkbox v-model="lazyParseEnabled">Предварительная обработка текста</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                    <el-form-item label="Копирование">
-                        <el-tooltip :open-delay="500" effect="light">
-                            <template slot="content">
-                                Загружать весь текст в окно<br>
-                                копирования текста со страницы
-                            </template>
-                            <el-checkbox v-model="copyFullText">Загружать весь текст</el-checkbox>
-                        </el-tooltip>
-                    </el-form-item>
-                </el-form>
-            </el-tab-pane>
-
-            <!-- Сброс ------------------------------------------------------------------------->
-            <el-tab-pane label="Сброс">
-                <el-button @click="setDefaults">Установить по умолчанию</el-button>
-            </el-tab-pane>
-
-        </el-tabs>
+                <!-- Вид ------------------------------------------------------------------------->                    
+                <div v-if="selectedTab == 'view'" class="fit column">
+                    @@include('./include/ViewTab.inc');
+                </div>
+                <!-- Кнопки ---------------------------------------------------------------------->
+                <div v-if="selectedTab == 'buttons'" class="fit tab-panel">
+                    @@include('./include/ButtonsTab.inc');
+                </div>
+                <!-- Управление ------------------------------------------------------------------>
+                <div v-if="selectedTab == 'keys'" class="fit tab-panel">
+                    @@include('./include/KeysTab.inc');
+                </div>
+                <!-- Листание -------------------------------------------------------------------->
+                <div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
+                    @@include('./include/PageMoveTab.inc');
+                </div>
+                <!-- Прочее ---------------------------------------------------------------------->
+                <div v-if="selectedTab == 'others'" class="fit tab-panel">
+                    @@include('./include/OthersTab.inc');
+                </div>
+                <!-- Сброс ----------------------------------------------------------------------->
+                <div v-if="selectedTab == 'reset'" class="fit tab-panel">
+                    @@include('./include/ResetTab.inc');
+                </div>
+            </div>
+
+        </div>
     </Window>
     </Window>
 </template>
 </template>
 
 
@@ -536,11 +75,16 @@ import _ from 'lodash';
 
 
 import * as utils from '../../../share/utils';
 import * as utils from '../../../share/utils';
 import Window from '../../share/Window.vue';
 import Window from '../../share/Window.vue';
+import NumInput from '../../share/NumInput.vue';
 import rstore from '../../../store/modules/reader';
 import rstore from '../../../store/modules/reader';
+import defPalette from './defPalette';
+
+const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
 
 
 export default @Component({
 export default @Component({
     components: {
     components: {
         Window,
         Window,
+        NumInput,
     },
     },
     data: function() {
     data: function() {
         return Object.assign({}, rstore.settingDefaults);
         return Object.assign({}, rstore.settingDefaults);
@@ -576,14 +120,32 @@ export default @Component({
             if (newValue != '' && this.pageChangeAnimation == 'flip')
             if (newValue != '' && this.pageChangeAnimation == 'flip')
                 this.pageChangeAnimation = '';
                 this.pageChangeAnimation = '';
         },
         },
+        textColor: function(newValue) {
+            this.textColorFiltered = newValue;
+        },
+        textColorFiltered: function(newValue) {
+            if (hex.test(newValue))
+                this.textColor = newValue;
+        },
+        backgroundColor: function(newValue) {
+            this.bgColorFiltered = newValue;
+        },
+        bgColorFiltered: function(newValue) {
+            if (hex.test(newValue))
+                this.backgroundColor = newValue;
+        },
     },
     },
 })
 })
 class SettingsPage extends Vue {
 class SettingsPage extends Vue {
-    selectedTab = null;
+    selectedTab = 'profiles';
+    selectedViewTab = 'color';
     form = {};
     form = {};
     fontBold = false;
     fontBold = false;
     fontItalic = false;
     fontItalic = false;
     vertShift = 0;
     vertShift = 0;
+    tabsScrollable = false;
+    textColorFiltered = '';
+    bgColorFiltered = '';
 
 
     webFonts = [];
     webFonts = [];
     fonts = [];
     fonts = [];
@@ -600,6 +162,15 @@ class SettingsPage extends Vue {
         this.settingsChanged();
         this.settingsChanged();
     }
     }
 
 
+    mounted() {
+        this.$watch(
+            '$refs.tabs.scrollable',
+            (newValue) => {
+                this.tabsScrollable = newValue && !this.$isMobileDevice;
+            }
+        );
+    }
+
     init() {
     init() {
         this.$refs.window.init();
         this.$refs.window.init();
         this.inited = true;
         this.inited = true;
@@ -623,6 +194,8 @@ class SettingsPage extends Vue {
         this.webFonts = rstore.webFonts;
         this.webFonts = rstore.webFonts;
         const font = (this.webFontName ? this.webFontName : this.fontName);
         const font = (this.webFontName ? this.webFontName : this.fontName);
         this.vertShift = this.fontShifts[font] || 0;
         this.vertShift = this.fontShifts[font] || 0;
+        this.textColorFiltered = this.textColor;
+        this.bgColorFiltered = this.backgroundColor;
     }
     }
 
 
     get mode() {
     get mode() {
@@ -645,9 +218,52 @@ class SettingsPage extends Vue {
         return this.$store.state.reader.profiles;
         return this.$store.state.reader.profiles;
     }
     }
 
 
-    get profilesArray() {
-        const result = Object.keys(this.profiles)
-        result.sort();
+    get currentProfileOptions() {
+        const profNames = Object.keys(this.profiles)
+        profNames.sort();
+
+        let result = [{label: 'Нет', value: ''}];
+        profNames.forEach(name => {
+            result.push({label: name, value: name});
+        });
+        return result;
+    }
+
+    get wallpaperOptions() {
+        let result = [{label: 'Нет', value: ''}];
+        for (let i = 1; i < 10; i++) {
+            result.push({label: i, value: `paper${i}`});
+        }
+        return result;
+    }
+
+    get fontsOptions() {
+        let result = [];
+        this.fonts.forEach(font => {
+            result.push({label: (font.label ? font.label : font.name), value: font.name});
+        });
+        return result;
+    }
+
+    get webFontsOptions() {
+        let result = [{label: 'Нет', value: ''}];
+        this.webFonts.forEach(font => {
+            result.push({label: font.name, value: font.name});
+        });
+        return result;
+    }
+
+    get pageChangeAnimationOptions() {
+        let result = [
+            {label: 'Нет', value: ''},
+            {label: 'Вверх-вниз', value: 'downShift'},
+            {label: 'Вправо-влево', value: 'rightShift'},
+            {label: 'Протаивание', value: 'thaw'},
+            {label: 'Мерцание', value: 'blink'},
+            {label: 'Вращение', value: 'rotate'},
+        ];
+        if (this.wallpaper == '')
+            result.push({label: 'Листание', value: 'flip'});
         return result;
         return result;
     }
     }
 
 
@@ -672,18 +288,22 @@ class SettingsPage extends Vue {
     }
     }
 
 
     get predefineTextColors() {
     get predefineTextColors() {
-        return [
+        return defPalette.concat([
           '#ffffff',
           '#ffffff',
           '#000000',
           '#000000',
           '#202020',
           '#202020',
           '#323232',
           '#323232',
           '#aaaaaa',
           '#aaaaaa',
           '#00c0c0',
           '#00c0c0',
-        ];
+          '#ebe2c9',
+          '#cfdc99',
+          '#478355',
+          '#909080',
+        ]);
     }
     }
 
 
     get predefineBackgroundColors() {
     get predefineBackgroundColors() {
-        return [
+        return defPalette.concat([
           '#ffffff',
           '#ffffff',
           '#000000',
           '#000000',
           '#202020',
           '#202020',
@@ -694,15 +314,28 @@ class SettingsPage extends Vue {
           '#909080',
           '#909080',
           '#808080',
           '#808080',
           '#c8c8c8',
           '#c8c8c8',
-        ];
+        ]);
+    }
+
+    colorPanStyle(type) {
+        let result = 'width: 30px; height: 30px; border: 1px solid black; border-radius: 4px;';
+        switch (type) {
+            case 'text':
+                result += `background-color: ${this.textColor};`
+                break;
+            case 'bg':
+                result += `background-color: ${this.backgroundColor};`
+                break;
+        }
+        return result;
     }
     }
 
 
     needReload() {
     needReload() {
-        this.$notify.warning({message: 'Необходимо обновить страницу (F5), чтобы изменения возымели эффект'});
+        this.$root.notify.warning('Необходимо обновить страницу (F5), чтобы изменения возымели эффект');
     }
     }
 
 
     needTextReload() {
     needTextReload() {
-        this.$notify.warning({message: 'Необходимо обновить книгу в обход кэша, чтобы изменения возымели эффект'});
+        this.$root.notify.warning('Необходимо обновить книгу в обход кэша, чтобы изменения возымели эффект');
     }
     }
 
 
     close() {
     close() {
@@ -711,12 +344,7 @@ class SettingsPage extends Vue {
 
 
     async setDefaults() {
     async setDefaults() {
         try {
         try {
-            if (await this.$confirm('Подтвердите установку настроек по умолчанию:', '', {
-                confirmButtonText: 'OK',
-                cancelButtonText: 'Отмена',
-                customClass: 'prompt-dialog',
-                type: 'warning'
-            })) {
+            if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) {
                 this.form = Object.assign({}, rstore.settingDefaults);
                 this.form = Object.assign({}, rstore.settingDefaults);
                 for (let prop in rstore.settingDefaults) {
                 for (let prop in rstore.settingDefaults) {
                     this[prop] = this.form[prop];
                     this[prop] = this.form[prop];
@@ -734,18 +362,15 @@ class SettingsPage extends Vue {
     async addProfile() {
     async addProfile() {
         try {
         try {
             if (Object.keys(this.profiles).length >= 100) {
             if (Object.keys(this.profiles).length >= 100) {
-                this.$alert('Достигнут предел количества профилей', 'Ошибка');
+                this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
                 return;
                 return;
             }
             }
-            const result = await this.$prompt('Введите произвольное название для профиля устройства:', '', {
-                confirmButtonText: 'OK',
-                cancelButtonText: 'Отмена',
+            const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
                 inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
                 inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
-                customClass: 'prompt-dialog',
             });
             });
-            if (result.value) {
+            if (result && result.value) {
                 if (this.profiles[result.value]) {
                 if (this.profiles[result.value]) {
-                    this.$alert('Такой профиль уже существует', 'Ошибка');
+                    this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
                 } else {
                 } else {
                     const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
                     const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
                     this.commit('reader/setAllowProfilesSave', true);
                     this.commit('reader/setAllowProfilesSave', true);
@@ -766,18 +391,13 @@ class SettingsPage extends Vue {
             return;
             return;
 
 
         try {
         try {
-            const result = await this.$prompt(`<b>Предупреждение!</b> Удаление профиля '${this.currentProfile}' необратимо.` +
-                    `<br>Все настройки профиля будут потеряны,<br>однако список читаемых книг сохранится.` +
-                    `<br><br>Введите 'да' для подтверждения удаления:`, '', {
-                dangerouslyUseHTMLString: true,
-                confirmButtonText: 'OK',
-                cancelButtonText: 'Отмена',
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.currentProfile}' необратимо.` +
+                    `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
+                    `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
-                customClass: 'prompt-dialog',
-                type: 'warning',
             });
             });
 
 
-            if (result.value && result.value.toLowerCase() == 'да') {
+            if (result && result.value && result.value.toLowerCase() == 'да') {
                 if (this.profiles[this.currentProfile]) {
                 if (this.profiles[this.currentProfile]) {
                     const newProfiles = Object.assign({}, this.profiles);
                     const newProfiles = Object.assign({}, this.profiles);
                     delete newProfiles[this.currentProfile];
                     delete newProfiles[this.currentProfile];
@@ -799,17 +419,12 @@ class SettingsPage extends Vue {
             return;
             return;
 
 
         try {
         try {
-            const result = await this.$prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
-                    `<br><br>Введите 'да' для подтверждения удаления:`, '', {
-                dangerouslyUseHTMLString: true,
-                confirmButtonText: 'OK',
-                cancelButtonText: 'Отмена',
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
+                    `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
-                customClass: 'prompt-dialog',
-                type: 'warning',
             });
             });
 
 
-            if (result.value && result.value.toLowerCase() == 'да') {
+            if (result && result.value && result.value.toLowerCase() == 'да') {
                 this.commit('reader/setAllowProfilesSave', true);
                 this.commit('reader/setAllowProfilesSave', true);
                 await this.$nextTick();//ждем обработчики watch
                 await this.$nextTick();//ждем обработчики watch
                 this.commit('reader/setProfiles', {});
                 this.commit('reader/setProfiles', {});
@@ -827,9 +442,9 @@ class SettingsPage extends Vue {
         const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
         const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
         const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
         const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
         if (result)
         if (result)
-            this.$notify.success({message: msg});
+            this.$root.notify.success(msg);
         else
         else
-            this.$notify.error({message: msg});
+            this.$root.notify.error(msg);
     }
     }
 
 
     async showServerStorageKey() {
     async showServerStorageKey() {
@@ -838,18 +453,22 @@ class SettingsPage extends Vue {
 
 
     async enterServerStorageKey(key) {
     async enterServerStorageKey(key) {
         try {
         try {
-            const result = await this.$prompt(`<b>Предупреждение!</b> Изменение ключа доступа приведет к замене всех профилей и читаемых книг в читалке.` +
-                    `<br><br>Введите новый ключ доступа:`, '', {
-                dangerouslyUseHTMLString: true,
-                confirmButtonText: 'OK',
-                cancelButtonText: 'Отмена',
-                inputValidator: (str) => { if (str && utils.fromBase58(str).length == 32) return true; else return 'Неверный формат ключа'; },
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Изменение ключа доступа приведет к замене всех профилей и читаемых книг в читалке.` +
+                    `<br><br>Введите новый ключ доступа:`, ' ', {
+                inputValidator: (str) => {
+                    try {
+                        if (str && utils.fromBase58(str).length == 32) {
+                            return true;
+                        }
+                    } catch (e) {
+                        //
+                    }
+                    return 'Неверный формат ключа'; 
+                },
                 inputValue: (key && _.isString(key) ? key : null),
                 inputValue: (key && _.isString(key) ? key : null),
-                customClass: 'prompt-dialog',
-                type: 'warning',
             });
             });
 
 
-            if (result.value && utils.fromBase58(result.value).length == 32) {
+            if (result && result.value && utils.fromBase58(result.value).length == 32) {
                 this.commit('reader/setServerStorageKey', result.value);
                 this.commit('reader/setServerStorageKey', result.value);
             }
             }
         } catch (e) {
         } catch (e) {
@@ -859,17 +478,12 @@ class SettingsPage extends Vue {
 
 
     async generateServerStorageKey() {
     async generateServerStorageKey() {
         try {
         try {
-            const result = await this.$prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
-                    `<br><br>Введите 'да' для подтверждения генерации нового ключа:`, '', {
-                dangerouslyUseHTMLString: true,
-                confirmButtonText: 'OK',
-                cancelButtonText: 'Отмена',
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
+                    `<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
                 inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
-                customClass: 'prompt-dialog',
-                type: 'warning',
             });
             });
 
 
-            if (result.value && result.value.toLowerCase() == 'да') {
+            if (result && result.value && result.value.toLowerCase() == 'да') {
                 this.$root.$emit('generateNewServerStorageKey');
                 this.$root.$emit('generateNewServerStorageKey');
             }
             }
         } catch (e) {
         } catch (e) {
@@ -879,7 +493,7 @@ class SettingsPage extends Vue {
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {
-        if (event.type == 'keydown' && event.code == 'Escape') {
+        if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
             this.close();
             this.close();
         }
         }
         return true;
         return true;
@@ -889,55 +503,79 @@ class SettingsPage extends Vue {
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>
-.text {
+.tab {
+    justify-content: initial;
+}
+
+.tab-panel {
+    overflow-x: hidden;
+    overflow-y: auto;
     font-size: 90%;
     font-size: 90%;
-    line-height: 130%;
+    padding: 0 10px 15px 10px;
 }
 }
 
 
-.el-form {
+.part-header {
     border-top: 2px solid #bbbbbb;
     border-top: 2px solid #bbbbbb;
+    font-weight: bold;
+    font-size: 110%;
+    margin-top: 15px;
     margin-bottom: 5px;
     margin-bottom: 5px;
 }
 }
 
 
-.el-form-item {
-    padding: 0 !important;
-    margin: 0 !important;
-    margin-bottom: 5px !important;
+.item {
+    width: 100%;
+    margin-top: 5px;
+    margin-bottom: 5px;
 }
 }
 
 
-.color-picked {
-    margin-left: 10px;
-    position: relative;
-    top: -11px;
+.label-1 {
+    width: 75px;
 }
 }
 
 
-.partHeader {
-    font-weight: bold;
-    margin-bottom: 5px;
+.label-2, .label-3, .label-4, .label-5 {
+    width: 110px;
 }
 }
 
 
-.el-tabs {
-    flex: 1;
-    display: flex;
+.label-6 {
+    width: 100px;
 }
 }
 
 
-.el-tab-pane {
-    flex: 1;
+.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
-    width: 420px;
-    overflow-y: auto;
-    padding: 15px;
+    justify-content: center;
+    text-align: right;
+    margin-right: 10px;
+    overflow: hidden;
 }
 }
 
 
-.center {
-    text-align: center;
+.text {
+    font-size: 90%;
+    line-height: 130%;
+}
+
+.button {
+    margin: 3px 15px 3px 0;
+    padding: 0 5px 0 5px;
+}
+
+.copy-icon {
+    margin-left: 5px;
+    cursor: pointer;
+    font-size: 120%;
+    color: blue;
+}
+
+.input {
+    max-width: 150px;
+}
+
+.no-mp {
+    margin: 0;
+    padding: 0;
 }
 }
-</style>
 
 
-<style>
-.prompt-dialog {
-    width: 100% !important;
-    max-width: 450px;
+.col-left {
+    width: 150px;
 }
 }
-</style>
+</style>

+ 17 - 0
client/components/Reader/SettingsPage/defPalette.js

@@ -0,0 +1,17 @@
+const defPalette = [
+    'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
+        'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
+    'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
+        'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
+    'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
+        'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
+    'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
+    'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
+    'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
+    'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
+    'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
+    'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
+    'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
+];
+
+export default defPalette;

+ 8 - 0
client/components/Reader/SettingsPage/include/ButtonsTab.inc

@@ -0,0 +1,8 @@
+<div class="part-header">Показывать кнопки панели</div>
+
+<div class="item row" v-for="item in toolButtons" :key="item.name">
+    <div class="label-3"></div>
+    <div class="col row">
+        <q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="item.text" />
+    </div>
+</div>

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

@@ -0,0 +1,8 @@
+<div class="part-header">Управление</div>
+
+<div class="item row">
+    <div class="label-4"></div>
+    <div class="col row">
+        <q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
+    </div>
+</div>

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

@@ -0,0 +1,91 @@
+<!---------------------------------------------->
+<div class="part-header">Подсказки, уведомления</div>
+
+<div class="item row no-wrap">
+    <div class="label-6">Подсказка</div>
+    <q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Показывать или нет подсказку при каждой загрузке книги
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row">
+    <div class="label-6">Подсказка</div>
+    <q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Мерцать сообщением в строке статуса и на кнопке<br>
+            обновления при загрузке книги из кэша
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row no-wrap">
+    <div class="label-6">Уведомление</div>
+    <q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Показывать уведомления и ошибки от<br>
+            синхронизатора данных с сервером
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row">
+    <div class="label-6">Уведомление</div>
+    <q-checkbox size="xs" v-model="showWhatsNewDialog">
+        Показывать уведомление "Что нового"
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Показывать уведомления "Что нового"<br>
+            при каждом выходе новой версии читалки
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row">
+    <div class="label-6">Уведомление</div>
+    <q-checkbox size="xs" v-model="showDonationDialog2020">
+        Показывать "Оплатим хостинг вместе"
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Показывать уведомление "Оплатим хостинг вместе"
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<!---------------------------------------------->
+<div class="part-header">Другое</div>
+
+<div class="item row">
+    <div class="label-6">Парам. в URL</div>
+    <q-checkbox size="xs" v-model="allowUrlParamBookPos">
+        Добавлять параметр "__p"
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Добавление параметра "__p" в строке браузера<br>
+            позволяет передавать ссылку на книгу в читалке<br>
+            без потери текущей позиции. Однако в этом случае<br>
+            при листании забивается история браузера, т.к. на<br>
+            каждое изменение позиции происходит смена URL.
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row">
+    <div class="label-6">Парсинг</div>
+    <q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная обработка текста">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Включение этой опции позволяет делать предварительную<br>
+            обработку текста в ленивом режиме сразу после загрузки<br>
+            книги. Это может повысить отзывчивость читалки, но<br>
+            нагружает процессор каждый раз при открытии книги.
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row">
+    <div class="label-6">Копирование</div>
+    <q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Загружать весь текст в окно<br>
+            копирования текста со страницы
+        </q-tooltip>
+    </q-checkbox>
+</div>

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

@@ -0,0 +1,28 @@
+<!---------------------------------------------->
+<div class="part-header">Анимация</div>
+
+<div class="item row">
+    <div class="label-5">Тип</div>
+    <q-select class="col-left" v-model="pageChangeAnimation" :options="pageChangeAnimationOptions"
+        dropdown-icon="la la-angle-down la-sm"
+        outlined dense emit-value map-options
+    />
+</div>
+
+<div class="item row">
+    <div class="label-5">Скорость</div>
+    <NumInput class="col-left" v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disable="pageChangeAnimation == ''"/>
+</div>
+
+<!---------------------------------------------->
+<div class="part-header">Другое</div>
+
+<div class="item row">
+    <div class="label-5">Страница</div>
+    <q-checkbox v-model="keepLastToFirst" size="xs" label="Переносить последнюю строку">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Переносить последнюю строку страницы<br>
+            в начало следующей при листании
+        </q-tooltip>
+    </q-checkbox>
+</div>

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

@@ -0,0 +1,101 @@
+<div class="part-header">Управление синхронизацией данных</div>
+
+<div class="item row">
+    <div class="label-1"></div>
+    <q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
+</div>
+
+<div v-show="serverSyncEnabled">
+    <!---------------------------------------------->
+    <div class="part-header">Профили устройств</div>
+
+    <div class="item row">
+        <div class="label-1"></div>
+        <div class="text col">
+            Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
+            <br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
+        </div>
+    </div>
+     <div class="item row">
+        <div class="label-1">Устройство</div>
+        <div class="col">
+            <q-select v-model="currentProfile" :options="currentProfileOptions"
+                style="width: 275px"
+                dropdown-icon="la la-angle-down la-sm"
+                outlined dense emit-value map-options
+            />
+        </div>
+    </div>
+    <div class="item row">
+        <div class="label-1"></div>
+        <q-btn class="button" dense no-caps @click="addProfile">Добавить</q-btn>
+        <q-btn class="button" dense no-caps @click="delProfile">Удалить</q-btn>
+        <q-btn class="button" dense no-caps @click="delAllProfiles">Удалить все</q-btn>
+    </div>
+
+    <!---------------------------------------------->
+    <div class="part-header">Ключ доступа</div>
+    
+    <div class="item row">
+        <div class="label-1"></div>
+        <div class="text col">
+            Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
+            Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
+        </div>
+    </div>
+
+    <div class="item row">
+        <div class="label-1"></div>
+        <q-btn class="button" style="width: 250px" dense no-caps @click="showServerStorageKey">
+                <span v-show="serverStorageKeyVisible">Скрыть</span>
+                <span v-show="!serverStorageKeyVisible">Показать</span>
+                &nbsp;ключ доступа
+         </q-btn>
+    </div>
+
+    <div class="item row">
+        <div class="label-1"></div>
+        <div v-if="!serverStorageKeyVisible" class="col">
+            <hr/>
+            <b>{{ partialStorageKey }}</b> (часть вашего ключа)
+            <hr/>
+        </div>
+        <div v-else class="col" style="line-height: 100%">
+            <hr/>
+            <div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
+                <b>{{ serverStorageKey }}</b>
+                <q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
+                    <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                </q-icon>            
+            </div>
+            <div v-if="mode == 'omnireader'">
+                <br>Переход по ссылке позволит автоматически ввести ключ доступа:
+                <br><div class="text-center" style="margin-top: 5px">
+                    <a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
+                    <q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
+                        <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>                    
+                    </q-icon>            
+                </div>
+            </div>
+            <hr/>
+        </div>
+    </div>
+
+    <div class="item row">
+        <div class="label-1"></div>
+        <q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
+    </div>
+    <div class="item row">
+        <div class="label-1"></div>
+        <q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
+    </div>
+    <div class="item row">
+        <div class="label-1"></div>
+        <div class="text col">
+            Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
+            например, после переустановки ОС или чистки/смены браузера.<br>
+            <b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
+            и шифруются ключом доступа перед отправкой на сервер.
+        </div>
+    </div>
+</div>

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

@@ -0,0 +1,3 @@
+<div class="item row">
+    <q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
+</div>

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

@@ -0,0 +1,34 @@
+<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="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 == '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>

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

@@ -0,0 +1,58 @@
+<!---------------------------------------------->
+<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>
+
+        <span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
+    </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"
+            :disable="wallpaper != ''"
+        >
+            <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 class="q-px-sm"/>
+        <q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
+            dropdown-icon="la la-angle-down la-sm"
+            outlined dense emit-value map-options
+        />
+    </div>
+</div>

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

@@ -0,0 +1,56 @@
+<!---------------------------------------------->
+<div class="hidden part-header">Шрифт</div>
+
+<div class="item row">
+    <div class="label-2">Локальный/веб</div>
+    <div class="col row">
+        <q-select class="col-left" v-model="fontName" :options="fontsOptions" :disable="webFontName != ''"
+            dropdown-icon="la la-angle-down la-sm"
+            outlined dense emit-value map-options
+        />
+
+        <div class="q-px-sm"/>
+        <q-select class="col" v-model="webFontName" :options="webFontsOptions"
+            dropdown-icon="la la-angle-down la-sm"
+            outlined dense emit-value map-options
+        >
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Веб шрифты дают большое разнообразие,<br>
+                однако есть шанс, что шрифт будет загружаться<br>
+                очень медленно или вовсе не загрузится
+            </q-tooltip>
+        </q-select>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Размер</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="fontSize" :min="5" :max="200"/>
+
+        <div class="col q-pt-xs text-right">
+            <a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
+        </div>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Сдвиг</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="vertShift" :min="-100" :max="100">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Сдвиг шрифта по вертикали в процентах от размера.<br>
+                Отрицательное значение сдвигает вверх, положительное -<br>
+                вниз. Значение зависит от метрики шрифта.
+            </q-tooltip>
+        </NumInput>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Стиль</div>
+    <div class="col row">
+        <q-checkbox v-model="fontBold" size="xs" label="Жирный" />
+        <q-checkbox class="q-ml-sm" v-model="fontItalic" size="xs" label="Курсив" />
+    </div>
+</div>

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

@@ -0,0 +1,24 @@
+<!---------------------------------------------->
+<div class="hidden part-header">Строка статуса</div>
+
+<div class="item row">
+    <div class="label-2">Статус</div>
+    <div class="col row">
+        <q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
+        <q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Высота</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Прозрачность</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
+    </div>
+</div>

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

@@ -0,0 +1,160 @@
+<!---------------------------------------------->
+<div class="hidden part-header">Текст</div>
+
+<div class="item row">
+    <div class="label-2">Интервал</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="lineInterval" :min="0" :max="200"/>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Параграф</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="p" :min="0" :max="2000"/>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Отступ</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Слева/справа
+            </q-tooltip>
+        </NumInput>
+        <div class="q-px-sm"/>
+        <NumInput class="col" v-model="indentTB" :min="0" :max="2000">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Сверху/снизу
+            </q-tooltip>
+        </NumInput>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Сдвиг</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="textVertShift" :min="-100" :max="100">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Сдвиг текста по вертикали в процентах от размера шрифта.<br>
+                Отрицательное значение сдвигает вверх, положительное -<br>
+                вниз.
+            </q-tooltip>
+        </NumInput>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Скроллинг</div>
+    <div class="col row">
+        <NumInput class="col-left" v-model="scrollingDelay" :min="1" :max="10000">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Замедление скроллинга в миллисекундах.<br>
+                Определяет время, за которое текст<br>
+                прокручивается на одну строку.
+            </q-tooltip>
+        </NumInput>
+
+        <div class="q-px-sm"/>
+        <q-select class="col" v-model="scrollingType" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
+            dropdown-icon="la la-angle-down la-sm"
+            outlined dense emit-value map-options
+        >
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Вид скроллинга: линейный,<br>
+                ускорение-замедление и пр.
+            </q-tooltip>
+        </q-select>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Выравнивание</div>
+    <div class="col row">
+        <q-checkbox v-model="textAlignJustify" size="xs" label="По ширине" />
+        <q-checkbox class="q-ml-sm" v-model="wordWrap" size="xs" label="Перенос по слогам" />
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2"></div>
+    <div class="col-left column justify-center text-right">
+        Компактность
+    </div>
+    <div class="q-px-sm"/>
+    <NumInput class="col" v-model="compactTextPerc" :min="0" :max="100">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Степень компактности текста в процентах.<br>
+            Чем больше компактность, тем хуже выравнивание<br>
+            по правому краю.
+        </q-tooltip>
+    </NumInput>
+</div>
+
+<div class="item row">
+    <div class="label-2">Обработка</div>
+    <div class="col row">
+        <q-checkbox v-model="cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2"></div>
+    <div class="col-left column justify-center text-right">
+        Добавлять пустые
+    </div>
+    <div class="q-px-sm"/>
+    <NumInput class="col" v-model="addEmptyParagraphs" :min="0" :max="2"/>
+</div>
+
+<div class="item row">
+    <div class="label-2"></div>
+    <div class="col row">
+        <q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Html-фильтр вырезает лишние элементы со<br>
+                страницы для определенных сайтов, таких как:<br>
+                samlib.ru<br>
+                www.fanfiction.net<br>
+                archiveofourown.org<br>
+                и других
+            </q-tooltip>
+        </q-checkbox>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2">Изображения</div>
+    <div class="col row">
+        <q-checkbox v-model="showImages" size="xs" label="Показывать" />
+        <q-checkbox class="q-ml-sm" v-model="showInlineImagesInCenter" @input="needReload" :disable="!showImages" size="xs" label="Инлайн в центр">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Выносить все изображения в центр экрана
+            </q-tooltip>
+        </q-checkbox>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2"></div>
+    <div class="col row">
+        <q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-2"></div>
+    <div class="col-left column justify-center text-right">
+        Высота не более
+    </div>
+    <div class="q-px-sm"/>
+    <NumInput class="col" v-model="imageHeightLines" :min="1" :max="100" :disable="!showImages">
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Определяет высоту изображения количеством строк.<br>
+            В случае превышения высоты, изображение будет<br>
+            уменьшено с сохранением пропорций так, чтобы<br>
+            помещаться в указанное количество строк.
+        </q-tooltip>
+    </NumInput>
+</div>

+ 34 - 44
client/components/Reader/TextPage/TextPage.vue

@@ -38,8 +38,8 @@ import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
 import {loadCSS} from 'fg-loadcss';
 import {loadCSS} from 'fg-loadcss';
 import _ from 'lodash';
 import _ from 'lodash';
-import {sleep} from '../../../share/utils';
 
 
+import {sleep} from '../../../share/utils';
 import bookManager from '../share/bookManager';
 import bookManager from '../share/bookManager';
 import DrawHelper from './DrawHelper';
 import DrawHelper from './DrawHelper';
 import rstore from '../../../store/modules/reader';
 import rstore from '../../../store/modules/reader';
@@ -131,7 +131,11 @@ class TextPage extends Vue {
             await this.doPageAnimation();
             await this.doPageAnimation();
         }, 10);
         }, 10);
 
 
-        this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
+        this.$root.$on('resize', async() => {
+            this.$nextTick(this.onResize);
+            await sleep(500);
+            this.$nextTick(this.onResize);
+        });
     }
     }
 
 
     mounted() {
     mounted() {
@@ -242,6 +246,9 @@ class TextPage extends Vue {
             this.parsed.imageHeightLines = this.imageHeightLines;
             this.parsed.imageHeightLines = this.imageHeightLines;
             this.parsed.imageFitWidth = this.imageFitWidth;
             this.parsed.imageFitWidth = this.imageFitWidth;
             this.parsed.compactTextPerc = this.compactTextPerc;
             this.parsed.compactTextPerc = this.compactTextPerc;
+
+            this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
+            this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
         }
         }
 
 
         //scrolling page
         //scrolling page
@@ -268,25 +275,18 @@ class TextPage extends Vue {
     async checkLoadedFonts() {
     async checkLoadedFonts() {
         let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
         let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
         if (loaded.some(r => !r)) {
         if (loaded.some(r => !r)) {
-            loaded = await Promise.all(this.fontList.map(font => document.fonts.load(font)));
-            if (loaded.some(r => !r.length))
-                throw new Error('some font not loaded');
+            await Promise.all(this.fontList.map(font => document.fonts.load(font)));
         }
         }
     }
     }
 
 
     async loadFonts() {
     async loadFonts() {
         this.fontsLoading = true;
         this.fontsLoading = true;
 
 
-        let inst = null;
+        let close = null;
         (async() => {
         (async() => {
             await sleep(500);
             await sleep(500);
             if (this.fontsLoading)
             if (this.fontsLoading)
-                inst = this.$notify({
-                  title: '',
-                  dangerouslyUseHTMLString: true,
-                  message: 'Загрузка шрифта &nbsp;<i class="el-icon-loading"></i>',
-                  duration: 0
-                });
+                close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
         })();
         })();
 
 
         if (!this.fontsLoaded)
         if (!this.fontsLoaded)
@@ -298,29 +298,15 @@ class TextPage extends Vue {
             this.fontsLoaded[this.fontCssUrl] = 1;
             this.fontsLoaded[this.fontCssUrl] = 1;
         }
         }
 
 
-        const waitingTime = 10*1000;
-        const delay = 100;
-        let i = 0;
-        //ждем шрифты
-        while (i < waitingTime/delay) {
-            i++;
-            try {
-                await this.checkLoadedFonts();
-                i = waitingTime;
-            } catch (e) {
-                await sleep(delay);
-            }
-        }
-        if (i !== waitingTime) {
-            this.$notify.error({
-                title: 'Ошибка загрузки',
-                message: 'Некоторые шрифты не удалось загрузить'
-            });
+        try {
+            await this.checkLoadedFonts();
+        } catch (e) {
+            this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
         }
         }
 
 
         this.fontsLoading = false;
         this.fontsLoading = false;
-        if (inst)
-            inst.close();
+        if (close)
+            close();
     }
     }
 
 
     getSettings() {
     getSettings() {
@@ -351,11 +337,15 @@ class TextPage extends Vue {
         // ширина шрифта некоторое время выдается неверно, поэтому
         // ширина шрифта некоторое время выдается неверно, поэтому
         if (!omitLoadFonts) {
         if (!omitLoadFonts) {
             const parsed = this.parsed;
             const parsed = this.parsed;
-            await sleep(100);
+
+            let i = 0;
+            const t = this.parsed.testText;
+            while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
+                await sleep(100);
+
             if (this.parsed === parsed) {
             if (this.parsed === parsed) {
-                parsed.force = true;
+                this.parsed.testWidth = this.drawHelper.measureText(t, {});
                 this.draw();
                 this.draw();
-                parsed.force = false;
             }
             }
         }
         }
     }
     }
@@ -432,7 +422,7 @@ class TextPage extends Vue {
                     if (this.lazyParseEnabled)
                     if (this.lazyParseEnabled)
                         this.lazyParsePara();
                         this.lazyParsePara();
                 } catch (e) {
                 } catch (e) {
-                    this.$alert(e.message, 'Ошибка', {type: 'error'});
+                    this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
                 }
                 }
             })();
             })();
         }
         }
@@ -457,13 +447,13 @@ class TextPage extends Vue {
     }
     }
 
 
     async onResize() {
     async onResize() {
-        /*this.page1 = null;
-        this.page2 = null;
-        this.statusBar = null;*/
-
-        this.calcDrawProps();
-        this.setBackground();
-        this.draw();
+        try {
+            this.calcDrawProps();
+            this.setBackground();
+            this.draw();
+        } catch (e) {
+            //
+        }
     }
     }
 
 
     get settings() {
     get settings() {
@@ -1141,7 +1131,7 @@ class TextPage extends Vue {
         if (url && url.indexOf('file://') != 0) {
         if (url && url.indexOf('file://') != 0) {
             window.open(url, '_blank');
             window.open(url, '_blank');
         } else {
         } else {
-            this.$alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска', '', {type: 'warning'});
+            this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {type: 'info'});
         }
         }
     }
     }
 
 

+ 2 - 0
client/components/Reader/share/BookParser.js

@@ -605,6 +605,7 @@ export default class BookParser {
 
 
         if (!this.force &&
         if (!this.force &&
             para.parsed && 
             para.parsed && 
+            para.parsed.testWidth === this.testWidth &&
             para.parsed.w === this.w &&
             para.parsed.w === this.w &&
             para.parsed.p === this.p &&
             para.parsed.p === this.p &&
             para.parsed.wordWrap === this.wordWrap &&
             para.parsed.wordWrap === this.wordWrap &&
@@ -620,6 +621,7 @@ export default class BookParser {
             return para.parsed;
             return para.parsed;
 
 
         const parsed = {
         const parsed = {
+            testWidth: this.testWidth,
             w: this.w,
             w: this.w,
             p: this.p,
             p: this.p,
             wordWrap: this.wordWrap,
             wordWrap: this.wordWrap,

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

@@ -1,4 +1,16 @@
 export const versionHistory = [
 export const versionHistory = [
+{
+    showUntil: '2020-02-25',
+    header: '0.9.0 (2020-02-26)',
+    content:
+`
+<ul>
+    <li>переход на UI-фреймфорк Quasar</li>
+    <li>незначительные изменения интерфейса</li>
+</ul>
+`
+},
+
 {
 {
     showUntil: '2020-02-05',
     showUntil: '2020-02-05',
     header: '0.8.4 (2020-02-06)',
     header: '0.8.4 (2020-02-06)',

+ 2 - 2
client/components/Settings/Settings.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Settings в разработке
         Раздел Settings в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 2 - 2
client/components/Sources/Sources.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
-    <el-container>
+    <div>
         Раздел Sources в разработке
         Раздел Sources в разработке
-    </el-container>
+    </div>
 </template>
 </template>
 
 
 <script>
 <script>

+ 64 - 0
client/components/share/Dialog.vue

@@ -0,0 +1,64 @@
+<template>
+    <q-dialog v-model="active">
+        <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-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="col q-mx-md">
+                <slot></slot>
+            </div>
+
+            <div class="row justify-end q-pa-md">
+                <slot name="footer"></slot>
+            </div>
+        </div>
+    </q-dialog>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+const DialogProps = Vue.extend({
+    props: {
+        value: Boolean,
+    }
+})
+
+export default @Component({
+})
+class Dialog extends DialogProps {
+    get active() {
+        return this.value;
+    }
+
+    set active(value) {
+        this.$emit('input', value);
+    }
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.header {
+    height: 50px;
+}
+
+.caption {
+    font-size: 110%;
+    overflow: hidden;
+}
+
+.close-icon {
+    width: 50px;
+}
+</style>

+ 58 - 0
client/components/share/Notify.vue

@@ -0,0 +1,58 @@
+<template>
+    <div class="hidden"></div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+export default @Component({
+})
+class Notify extends Vue {
+    notify(opts) {
+        let {
+            caption = null,
+            captionColor = 'black',
+            color = 'positive',
+            icon = '',
+            iconColor = 'white',
+            message = '',
+            messageColor = 'black',
+        } = opts;
+
+        caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
+        return this.$q.notify({
+            position: 'top-right',
+            color,
+            textColor: iconColor,
+            icon,
+            actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
+            html: true,
+
+            message: 
+                `<div style="max-width: 350px;">
+                    ${caption}
+                    <div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
+                </div>`
+        });
+    }
+
+    success(message, caption) {
+        this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
+    }
+
+    warning(message, caption) {
+        this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
+    }
+
+    error(message, caption) {
+        this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
+    }
+
+    info(message, caption) {
+        this.notify({color: 'info', icon: 'la la-bell', message, caption});
+    }
+}
+//-----------------------------------------------------------------------------
+</script>

+ 185 - 0
client/components/share/NumInput.vue

@@ -0,0 +1,185 @@
+<template>
+    <q-input outlined dense
+        v-model="filteredValue"
+        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')" 
+                name="la la-minus-circle" 
+                class="button" 
+                v-ripple="validate(value - step)" 
+                @click="minus"
+                @mousedown.prevent.stop="onMouseDown($event, 'minus')"
+                @mouseup.prevent.stop="onMouseUp"
+                @mouseout.prevent.stop="onMouseUp"
+                @touchstart.stop="onTouchStart($event, 'minus')"
+                @touchend.stop="onTouchEnd"
+                @touchcancel.prevent.stop="onTouchEnd"
+            />
+        </template>
+        <template v-slot:append>
+            <q-icon :class="(validate(value + 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"
+                @mouseout.prevent.stop="onMouseUp"
+                @touchstart.stop="onTouchStart($event, 'plus')"
+                @touchend.stop="onTouchEnd"
+                @touchcancel.prevent.stop="onTouchEnd"
+            />
+        </template>
+    </q-input>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+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({
+    watch: {
+        filteredValue: function(newValue) {
+            if (this.validate(newValue)) {
+                this.error = false;
+                this.$emit('input', this.string2number(newValue));
+            } else {
+                this.error = true;
+            }
+        },
+        value: function(newValue) {
+            this.filteredValue = newValue;
+        },
+    }
+})
+class NumInput extends NumInputProps {
+    filteredValue = 0;
+    error = false;
+
+    created() {
+        this.filteredValue = this.value;
+    }
+
+    string2number(value) {
+        return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
+    }
+
+    validate(value) {
+        let n = this.string2number(value);
+        if (isNaN(n))
+            return false;
+        if (n < this.min)
+            return false;
+        if (n > this.max)
+            return false;
+        return true;
+    }
+
+    plus() {
+        const newValue = this.value + this.step;
+        if (this.validate(newValue))
+            this.filteredValue = newValue;
+    }
+
+    minus() {
+        const newValue = this.value - this.step;
+        if (this.validate(newValue))
+            this.filteredValue = newValue;
+    }
+
+    onMouseDown(event, way) {
+        this.startClickRepeat = true;
+        this.clickRepeat = false;
+
+        if (event.button == 0) {
+            (async() => {
+                await utils.sleep(300);
+                if (this.startClickRepeat) {
+                    this.clickRepeat = true;
+                    while (this.clickRepeat) {
+                        if (way == 'plus') {
+                            this.plus();
+                        } else {
+                            this.minus();
+                        }
+                        await utils.sleep(50);
+                    }
+                }
+            })();
+        }
+    }
+
+    onMouseUp() {
+        if (this.inTouch)
+            return;
+        this.startClickRepeat = false;
+        this.clickRepeat = false;
+    }
+
+    onTouchStart(event, way) {
+        if (!this.$isMobileDevice)
+            return;
+        if (event.touches.length == 1) {
+            this.inTouch = true;
+            this.onMouseDown({button: 0}, way);
+        }
+    }
+
+    onTouchEnd() {
+        if (!this.$isMobileDevice)
+            return;
+        this.inTouch = false;
+        this.onMouseUp();
+    }
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.no-mp {
+    margin: 0;
+    padding: 0;
+}
+
+.button {
+    font-size: 130%;
+    border-radius: 20px;
+    color: #bbb;
+    cursor: pointer;
+}
+
+.button:hover {
+    color: #616161;
+    background-color: #efebe9;
+}
+
+.error {
+    background-color: #ffabab;
+    border-radius: 3px;
+}
+
+.disable, .disable:hover {
+    cursor: not-allowed;
+    color: #bbb;
+    background-color: white;
+}
+</style>

+ 255 - 0
client/components/share/StdDialog.vue

@@ -0,0 +1,255 @@
+<template>
+    <q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
+        <slot></slot>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'alert'" class="dialog column bg-white no-wrap" style="min-height: 150px">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn flat round dense v-close-popup>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="col q-mx-md">
+                <div v-html="message"></div>
+            </div>
+
+            <div class="buttons row justify-end q-pa-md">
+                <q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
+            </div>
+        </div>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'confirm'" class="dialog column bg-white no-wrap" style="min-height: 150px">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn flat round dense v-close-popup>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="col q-mx-md">
+                <div v-html="message"></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>
+            </div>
+        </div>
+
+        <!--------------------------------------------------->
+        <div v-show="type == 'prompt'" class="dialog column bg-white no-wrap" style="min-height: 250px">
+            <div class="header row">
+                <div class="caption col row items-center q-ml-md">
+                    <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
+                    <div v-html="caption"></div>
+                </div>
+                <div class="close-icon column justify-center items-center">
+                    <q-btn flat round dense v-close-popup>
+                        <q-icon name="la la-times" size="18px"></q-icon>
+                    </q-btn>
+                </div>
+            </div>
+
+            <div class="col 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>
+            </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>
+            </div>
+        </div>
+    </q-dialog>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+//import * as utils from '../../share/utils';
+
+export default @Component({
+    watch: {
+        inputValue: function(newValue) {
+            this.validate(newValue);
+        },
+    }
+})
+class StdDialog extends Vue {
+    caption = '';
+    message = '';
+    active = false;
+    type = '';
+    inputValue = '';
+    error = '';
+    iconColor = '';
+
+    created() {
+        if (this.$root.addKeyHook) {
+            this.$root.addKeyHook(this.keyHook);
+        }
+    }
+
+    init(message, caption, opts) {
+        this.caption = caption;
+        this.message = message;
+
+        this.ok = false;        
+        this.type = '';
+        this.inputValidator = null;
+        this.inputValue = '';
+        this.error = '';
+
+        this.iconColor = 'text-warning';
+        if (opts && opts.type) {
+            this.iconColor = `text-${opts.type}`;
+        }
+    }
+
+    onHide() {
+        if (this.hideTrigger) {
+            this.hideTrigger();
+            this.hideTrigger = null;
+        }
+    }
+
+    onShow() {
+        if (this.type == 'prompt') {
+            this.enableValidator = true;
+            if (this.inputValue)
+                this.validate(this.inputValue);
+            this.$refs.input.focus();
+        }
+    }
+
+    validate(value) {
+        if (!this.enableValidator)
+            return false;
+
+        if (this.inputValidator) {
+            const result = this.inputValidator(value);
+            if (result !== true) {
+                this.error = result;
+                return false;
+            }
+        }
+        this.error = '';
+        return true;
+    }
+
+    okClick() {
+        if (this.type == 'prompt' && !this.validate(this.inputValue)) {
+            this.$refs.dialog.shake();
+            return;
+        }
+        this.ok = true;
+        this.$refs.dialog.hide();
+    }
+
+    alert(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve(true);
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'alert';
+            this.active = true;
+        });
+    }
+
+    confirm(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve(true);
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'confirm';
+            this.active = true;
+        });
+    }
+
+    prompt(message, caption, opts) {
+        return new Promise((resolve) => {
+            this.enableValidator = false;
+            this.init(message, caption, opts);
+
+            this.hideTrigger = () => {
+                if (this.ok) {
+                    resolve({value: this.inputValue});
+                } else {
+                    resolve(false);
+                }
+            };
+
+            this.type = 'prompt';
+            if (opts) {
+                this.inputValidator = opts.inputValidator || null;
+                this.inputValue = opts.inputValue || '';
+            }
+            this.active = true;
+        });
+    }
+
+    keyHook(event) {
+        if (this.active && event.code == 'Enter') {
+            this.okClick();
+            event.stopPropagation();
+            event.preventDefault();
+        }
+    }
+}
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.header {
+    height: 50px;
+}
+
+.caption {
+    font-size: 110%;
+    overflow: hidden;
+}
+
+.close-icon {
+    width: 50px;
+}
+
+.buttons {
+    height: 60px;
+}
+
+.error {
+    height: 20px;
+    font-size: 80%;
+    color: red;
+}
+</style>

+ 19 - 22
client/components/share/Window.vue

@@ -1,12 +1,13 @@
 <template>
 <template>
-    <div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
-        <div ref="windowBox" class="windowBox" @click.stop>
-            <div class="window">
-                <div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
+    <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 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">
                     @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
-                    <span class="header-text"><slot name="header"></slot></span>
-                    <span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
+                    <span class="header-text col"><slot name="header"></slot></span>
+                    <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
                 </div>
                 </div>
+
                 <slot></slot>
                 <slot></slot>
             </div>
             </div>
         </div>
         </div>
@@ -116,23 +117,20 @@ class Window extends Vue {
 
 
 <style scoped>
 <style scoped>
 .main {
 .main {
-    position: absolute;
-    width: 100%;
-    height: 100%;
+    background-color: transparent !important;
     z-index: 50;
     z-index: 50;
 }
 }
 
 
-.windowBox {
-    position: absolute;
-    display: flex;
+.xyfit {
     height: 100%;
     height: 100%;
     width: 100%;
     width: 100%;
 }
 }
 
 
-.window {
+.flexfit {
     flex: 1;
     flex: 1;
-    display: flex;
-    flex-direction: column;
+}
+
+.window {
     margin: 10px;
     margin: 10px;
     background-color: #ffffff;
     background-color: #ffffff;
     border: 3px double black;
     border: 3px double black;
@@ -141,23 +139,21 @@ class Window extends Vue {
 }
 }
 
 
 .header {
 .header {
-    display: flex;
-    justify-content: flex-end;
-    background-color: #59B04F;
+    background: linear-gradient(to bottom right, green, #59B04F);
     align-items: center;
     align-items: center;
     height: 30px;
     height: 30px;
 }
 }
 
 
 .header-text {
 .header-text {
-    flex: 1;
     margin-left: 10px;
     margin-left: 10px;
     margin-right: 10px;
     margin-right: 10px;
+    color: yellow;
+    text-shadow: 2px 1px 5px black, 2px 2px 5px black;
+    overflow: hidden;
+    white-space: nowrap;
 }
 }
 
 
 .close-button {
 .close-button {
-    display: flex;
-    justify-content: center;
-    align-items: center;
     width: 30px;
     width: 30px;
     height: 30px;
     height: 30px;
     cursor: pointer;
     cursor: pointer;
@@ -166,4 +162,5 @@ class Window extends Vue {
 .close-button:hover {
 .close-button:hover {
     background-color: #69C05F;
     background-color: #69C05F;
 }
 }
+
 </style>
 </style>

+ 0 - 69
client/element.js

@@ -1,69 +0,0 @@
-import Vue from 'vue';
-
-/*
-import ElementUI from 'element-ui';
-import './theme/index.css';
-import locale from 'element-ui/lib/locale/lang/ru-RU';
-
-Vue.use(ElementUI, { locale });
-*/
-
-//------------------------------------------------------
-import './theme/index.css';
-
-import ElMenu from 'element-ui/lib/menu'; 
-import ElMenuItem from 'element-ui/lib/menu-item';
-import ElButton from 'element-ui/lib/button';
-import ElButtonGroup from 'element-ui/lib/button-group';
-import ElCheckbox from 'element-ui/lib/checkbox';
-import ElTabs from 'element-ui/lib/tabs';
-import ElTabPane from 'element-ui/lib/tab-pane';
-import ElTooltip from 'element-ui/lib/tooltip';
-import ElRow from 'element-ui/lib/row';
-import ElCol from 'element-ui/lib/col';
-import ElContainer from 'element-ui/lib/container';
-import ElAside from 'element-ui/lib/aside';
-import ElHeader from 'element-ui/lib/header';
-import ElMain from 'element-ui/lib/main';
-import ElInput from 'element-ui/lib/input';
-import ElInputNumber from 'element-ui/lib/input-number';
-import ElSelect from 'element-ui/lib/select';
-import ElOption from 'element-ui/lib/option';
-import ElTable from 'element-ui/lib/table';
-import ElTableColumn from 'element-ui/lib/table-column';
-import ElProgress from 'element-ui/lib/progress';
-import ElSlider from 'element-ui/lib/slider';
-import ElForm from 'element-ui/lib/form';
-import ElFormItem from 'element-ui/lib/form-item';
-import ElColorPicker from 'element-ui/lib/color-picker';
-import ElDialog from 'element-ui/lib/dialog';
-
-import Notification from 'element-ui/lib/notification';
-import Loading from 'element-ui/lib/loading';
-import MessageBox from 'element-ui/lib/message-box';
-
-const components = {
-    ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
-    ElRow, ElCol, ElContainer, ElAside, ElMain, ElHeader,
-    ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
-    ElProgress, ElSlider, ElForm, ElFormItem,
-    ElColorPicker, ElDialog,
-};
-
-for (let name in components) {
-    Vue.component(name, components[name]);
-}
-
-//Vue.use(Loading.directive);
-
-Vue.prototype.$loading = Loading.service;
-Vue.prototype.$msgbox = MessageBox;
-Vue.prototype.$alert = MessageBox.alert;
-Vue.prototype.$confirm = MessageBox.confirm;
-Vue.prototype.$prompt = MessageBox.prompt;
-Vue.prototype.$notify = Notification;
-//Vue.prototype.$message = Message;
-
-import lang from 'element-ui/lib/locale/lang/ru-RU';
-import locale from 'element-ui/lib/locale';
-locale.use(lang);

+ 1 - 1
client/main.js

@@ -2,7 +2,7 @@ import Vue from 'vue';
 
 
 import router from './router';
 import router from './router';
 import store from './store';
 import store from './store';
-import './element';
+import './quasar';
 
 
 import App from './components/App.vue';
 import App from './components/App.vue';
 //Vue.config.productionTip = false;
 //Vue.config.productionTip = false;

+ 88 - 0
client/quasar.js

@@ -0,0 +1,88 @@
+import Vue from 'vue';
+
+import 'quasar/dist/quasar.css';
+import Quasar from 'quasar/src/vue-plugin.js'
+
+//config
+const config = {};
+
+//components
+//import {QLayout} from 'quasar/src/components/layout';
+//import {QPageContainer, QPage} from 'quasar/src/components/page';
+//import {QDrawer} from 'quasar/src/components/drawer';
+
+import {QCircularProgress} from 'quasar/src/components/circular-progress';
+import {QInput} from 'quasar/src/components/input';
+import {QBtn} from 'quasar/src/components/btn';
+import {QBtnGroup} from 'quasar/src/components/btn-group';
+import {QBtnToggle} from 'quasar/src/components/btn-toggle';
+import {QIcon} from 'quasar/src/components/icon';
+import {QSlider} from 'quasar/src/components/slider';
+import {QTabs, QTab} from 'quasar/src/components/tabs';
+//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
+import {QSeparator} from 'quasar/src/components/separator';
+import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
+import {QTooltip} from 'quasar/src/components/tooltip';
+import {QSpinner} from 'quasar/src/components/spinner';
+import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
+import {QCheckbox} from 'quasar/src/components/checkbox';
+import {QSelect} from 'quasar/src/components/select';
+import {QColor} from 'quasar/src/components/color';
+import {QPopupProxy} from 'quasar/src/components/popup-proxy';
+import {QDialog} from 'quasar/src/components/dialog';
+
+const components = {
+    //QLayout,
+    //QPageContainer, QPage,
+    //QDrawer,
+
+    QCircularProgress,
+    QInput,
+    QBtn,
+    QBtnGroup,
+    QBtnToggle,
+    QIcon,
+    QSlider,
+    QTabs, QTab,
+    //QTabPanels, QTabPanel,
+    QSeparator,
+    QList, QItem, QItemSection, QItemLabel,
+    QTooltip,
+    QSpinner,
+    QTable, QTh, QTr, QTd,
+    QCheckbox,
+    QSelect,
+    QColor,
+    QPopupProxy,
+    QDialog,
+};
+
+//directives 
+import Ripple from 'quasar/src/directives/Ripple';
+import ClosePopup from 'quasar/src/directives/ClosePopup';
+
+const directives = {Ripple, ClosePopup};
+
+//plugins
+import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
+import Notify from 'quasar/src/plugins/Notify';
+
+const plugins = {
+    AppFullscreen,
+    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 '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
+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);

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

@@ -164,7 +164,7 @@ const settingDefaults = {
     scrollingDelay: 3000,// замедление, ms
     scrollingDelay: 3000,// замедление, ms
     scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
     scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
 
 
-    pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
+    pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
     pageChangeAnimationSpeed: 80, //0-100%
     pageChangeAnimationSpeed: 80, //0-100%
 
 
     allowUrlParamBookPos: false,
     allowUrlParamBookPos: false,

BIN
client/theme/fonts/element-icons.ttf


BIN
client/theme/fonts/element-icons.woff


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
client/theme/index.css


+ 11 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Liberama",
   "name": "Liberama",
-  "version": "0.8.4",
+  "version": "0.9.0",
   "lockfileVersion": 1,
   "lockfileVersion": 1,
   "requires": true,
   "requires": true,
   "dependencies": {
   "dependencies": {
@@ -259,6 +259,11 @@
         "fastq": "^1.6.0"
         "fastq": "^1.6.0"
       }
       }
     },
     },
+    "@quasar/extras": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.5.0.tgz",
+      "integrity": "sha512-SE1npMOvXGSEI1nWnEd2/EQswkM2QAiZ2K1WSPMOeqZtDvc29uLqv5A9mecWPLxVN/bSpLiX65j+ZYilY6KNHQ=="
+    },
     "@sindresorhus/is": {
     "@sindresorhus/is": {
       "version": "0.14.0",
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -10426,6 +10431,11 @@
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
       "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
       "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
     },
     },
+    "quasar": {
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/quasar/-/quasar-1.8.5.tgz",
+      "integrity": "sha512-iU9pitB0tEPwlJtNAJLnzIOSpolhZTPnfHBPNgLLxJSNXrgt5uQAuCcLXuG61AxRNhyrKdmsGIEMOYl/Aakv2Q=="
+    },
     "querystring": {
     "querystring": {
       "version": "0.2.0",
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
       "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",

+ 3 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Liberama",
   "name": "Liberama",
-  "version": "0.8.4",
+  "version": "0.9.0",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "license": "CC0-1.0",
   "repository": "bookpauk/liberama",
   "repository": "bookpauk/liberama",
@@ -55,6 +55,7 @@
     "webpack-merge": "^4.2.2"
     "webpack-merge": "^4.2.2"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@quasar/extras": "^1.5.0",
     "appcache-webpack-plugin": "^1.4.0",
     "appcache-webpack-plugin": "^1.4.0",
     "axios": "^0.18.1",
     "axios": "^0.18.1",
     "base-x": "^3.0.6",
     "base-x": "^3.0.6",
@@ -73,6 +74,7 @@
     "multer": "^1.4.2",
     "multer": "^1.4.2",
     "pako": "^1.0.10",
     "pako": "^1.0.10",
     "path-browserify": "^1.0.0",
     "path-browserify": "^1.0.0",
+    "quasar": "^1.8.5",
     "safe-buffer": "^5.2.0",
     "safe-buffer": "^5.2.0",
     "sjcl": "^1.0.8",
     "sjcl": "^1.0.8",
     "sql-template-strings": "^2.2.2",
     "sql-template-strings": "^2.2.2",

+ 2 - 1
server/config/base.js

@@ -21,7 +21,8 @@ module.exports = {
     maxTempPublicDirSize: 512*1024*1024,//512Мб
     maxTempPublicDirSize: 512*1024*1024,//512Мб
     maxUploadPublicDirSize: 200*1024*1024,//100Мб
     maxUploadPublicDirSize: 200*1024*1024,//100Мб
 
 
-    useExternalBookConverter: false,    
+    useExternalBookConverter: false,
+    webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
 
 
     db: [
     db: [
         {
         {

+ 5 - 2
server/controllers/MiscController.js

@@ -3,8 +3,11 @@ const _ = require('lodash');
 
 
 class MiscController extends BaseController {
 class MiscController extends BaseController {
     async getConfig(req, res) {
     async getConfig(req, res) {
-        if (Array.isArray(req.body.params))
-            return _.pick(this.config, req.body.params);
+        if (Array.isArray(req.body.params)) {
+            const paramsSet = new Set(req.body.params);
+
+            return _.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x)));
+        }
         //bad request
         //bad request
         res.status(400).send({error: 'params is not an array'});
         res.status(400).send({error: 'params is not an array'});
         return false;
         return false;

+ 5 - 2
server/controllers/WebSocketController.js

@@ -98,7 +98,9 @@ class WebSocketController {
 
 
     async getConfig(req, ws) {
     async getConfig(req, ws) {
         if (Array.isArray(req.params)) {
         if (Array.isArray(req.params)) {
-            this.send(_.pick(this.config, req.params), req, ws);
+            const paramsSet = new Set(req.params);
+
+            this.send(_.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))), req, ws);
         } else {
         } else {
             throw new Error('params is not an array');
             throw new Error('params is not an array');
         }
         }
@@ -122,9 +124,10 @@ class WebSocketController {
         while (1) {// eslint-disable-line no-constant-condition
         while (1) {// eslint-disable-line no-constant-condition
             const prevProgress = state.progress || -1;
             const prevProgress = state.progress || -1;
             const prevState = state.state || '';
             const prevState = state.state || '';
+            const lastModified = state.lastModified || 0;
             state = this.workerState.getState(req.workerId);
             state = this.workerState.getState(req.workerId);
 
 
-            this.send((state ? state : {}), req, ws);
+            this.send((state && lastModified != state.lastModified ? state : {}), req, ws);
             if (!state) break;
             if (!state) break;
 
 
             if (state.state != 'finish' && state.state != 'error')
             if (state.state != 'finish' && state.state != 'error')

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä