소스 검색

Merge branch 'release/1.0.0'

Book Pauk 2 년 전
부모
커밋
b98a44def2
100개의 변경된 파일3902개의 추가작업 그리고 3005개의 파일을 삭제
  1. 5 5
      .gitignore
  2. 136 23
      README.md
  3. 0 31
      build/includer.js
  4. 0 51
      build/linux.js
  5. 51 0
      build/prepkg.js
  6. 33 0
      build/release.js
  7. 0 4
      build/webpack.base.config.js
  8. 3 3
      build/webpack.dev.config.js
  9. 1 2
      build/webpack.prod.config.js
  10. 0 45
      build/win.js
  11. 4 16
      client/api/misc.js
  12. 22 66
      client/api/reader.js
  13. 6 23
      client/components/App.vue
  14. 0 19
      client/components/CardIndex/Book/Book.vue
  15. 0 19
      client/components/CardIndex/Card/Card.vue
  16. 0 93
      client/components/CardIndex/CardIndex.vue
  17. 0 19
      client/components/CardIndex/History/History.vue
  18. 0 19
      client/components/CardIndex/Search/Search.vue
  19. 1 0
      client/components/ExternalLibs/BookmarkSettings/BookmarkSettings.vue
  20. 79 23
      client/components/ExternalLibs/ExternalLibs.vue
  21. 0 19
      client/components/Help/Help.vue
  22. 0 19
      client/components/Income/Income.vue
  23. 0 19
      client/components/NotFound404/NotFound404.vue
  24. 1 1
      client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue
  25. BIN
      client/components/Reader/HelpPage/DonateHelpPage/assets/bitcoin.png
  26. BIN
      client/components/Reader/HelpPage/DonateHelpPage/assets/litecoin.png
  27. BIN
      client/components/Reader/HelpPage/DonateHelpPage/assets/monero.png
  28. 1 1
      client/components/Reader/HelpPage/HotkeysHelpPage/HotkeysHelpPage.vue
  29. 1 1
      client/components/Reader/HelpPage/MouseHelpPage/MouseHelpPage.vue
  30. 9 4
      client/components/Reader/LibsPage/LibsPage.vue
  31. 2 23
      client/components/Reader/LoaderPage/LoaderPage.vue
  32. 1 1
      client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue
  33. 149 156
      client/components/Reader/Reader.vue
  34. 30 23
      client/components/Reader/ReaderDialogs/ReaderDialogs.vue
  35. 2 2
      client/components/Reader/RecentBooksPage/RecentBooksPage.vue
  36. 12 5
      client/components/Reader/SearchPage/SearchPage.vue
  37. 8 1
      client/components/Reader/ServerStorage/ServerStorage.vue
  38. 0 87
      client/components/Reader/SettingsPage/ConvertTab.inc
  39. 145 0
      client/components/Reader/SettingsPage/ConvertTab/ConvertTab.vue
  40. 0 33
      client/components/Reader/SettingsPage/KeysTab.inc
  41. 78 0
      client/components/Reader/SettingsPage/KeysTab/KeysTab.vue
  42. 3 4
      client/components/Reader/SettingsPage/KeysTab/UserHotKeys/UserHotKeys.vue
  43. 0 91
      client/components/Reader/SettingsPage/OthersTab.inc
  44. 148 0
      client/components/Reader/SettingsPage/OthersTab/OthersTab.vue
  45. 0 28
      client/components/Reader/SettingsPage/PageMoveTab.inc
  46. 96 0
      client/components/Reader/SettingsPage/PageMoveTab/PageMoveTab.vue
  47. 0 101
      client/components/Reader/SettingsPage/ProfilesTab.inc
  48. 362 0
      client/components/Reader/SettingsPage/ProfilesTab/ProfilesTab.vue
  49. 0 3
      client/components/Reader/SettingsPage/ResetTab.inc
  50. 41 0
      client/components/Reader/SettingsPage/ResetTab/ResetTab.vue
  51. 89 649
      client/components/Reader/SettingsPage/SettingsPage.vue
  52. 0 18
      client/components/Reader/SettingsPage/ToolBarTab.inc
  53. 76 0
      client/components/Reader/SettingsPage/ToolBarTab/ToolBarTab.vue
  54. 0 76
      client/components/Reader/SettingsPage/UpdateTab.inc
  55. 122 0
      client/components/Reader/SettingsPage/UpdateTab/UpdateTab.vue
  56. 0 121
      client/components/Reader/SettingsPage/ViewTab/Color.inc
  57. 329 0
      client/components/Reader/SettingsPage/ViewTab/Color/Color.vue
  58. 0 56
      client/components/Reader/SettingsPage/ViewTab/Font.inc
  59. 176 0
      client/components/Reader/SettingsPage/ViewTab/Font/Font.vue
  60. 0 124
      client/components/Reader/SettingsPage/ViewTab/Mode.inc
  61. 229 0
      client/components/Reader/SettingsPage/ViewTab/Mode/Mode.vue
  62. 0 64
      client/components/Reader/SettingsPage/ViewTab/Status.inc
  63. 153 0
      client/components/Reader/SettingsPage/ViewTab/Status/Status.vue
  64. 0 127
      client/components/Reader/SettingsPage/ViewTab/Text.inc
  65. 210 0
      client/components/Reader/SettingsPage/ViewTab/Text/Text.vue
  66. 75 0
      client/components/Reader/SettingsPage/ViewTab/ViewTab.vue
  67. 29 1
      client/components/Reader/SettingsPage/ViewTab/defPalette.js
  68. 9 0
      client/components/Reader/SettingsPage/ViewTab/helper.js
  69. 36 72
      client/components/Reader/TextPage/TextPage.vue
  70. 16 0
      client/components/Reader/versionHistory.js
  71. 0 19
      client/components/Settings/Settings.vue
  72. 0 19
      client/components/Sources/Sources.vue
  73. 0 1
      client/components/share/Notify.vue
  74. 94 24
      client/components/share/NumInput.vue
  75. 3 1
      client/components/share/Window.vue
  76. 24 15
      client/components/vueComponent.js
  77. 2 27
      client/router.js
  78. 5 18
      client/share/utils.js
  79. 64 37
      client/store/modules/reader.js
  80. 14 10
      docs/beta/beta.liberama
  81. 66 10
      docs/beta/beta.omnireader
  82. 15 11
      docs/beta/beta.omnireader_http
  83. 28 20
      docs/liberama.top/liberama
  84. 7 7
      docs/omnireader.ru/README.md
  85. 63 8
      docs/omnireader.ru/omnireader
  86. 15 11
      docs/omnireader.ru/omnireader_http
  87. 259 269
      package-lock.json
  88. 43 37
      package.json
  89. 6 11
      server/config/base.js
  90. 33 12
      server/config/index.js
  91. 3 8
      server/config/production.js
  92. 18 0
      server/controllers/WebSocketController.js
  93. 4 0
      server/core/AppLogger.js
  94. 6 2
      server/core/AsyncExit.js
  95. 5 2
      server/core/FileDownloader.js
  96. 24 10
      server/core/Logger.js
  97. 55 14
      server/core/Reader/JembaReaderStorage.js
  98. 6 9
      server/core/Reader/ReaderWorker.js
  99. 59 0
      server/core/Zip/ZipReader.js
  100. 2 2
      server/core/Zip/ZipStreamer.js

+ 5 - 5
.gitignore

@@ -1,5 +1,5 @@
-/node_modules
-/server/data
-/server/public
-/server/ipfs
-/dist
+/node_modules
+/server/.liberama*
+/dist
+dev*.sh
+

+ 136 - 23
README.md

@@ -1,43 +1,156 @@
 # Liberama
 # Liberama
 
 
-Браузерная онлайн-читалка книг и децентрализованная библиотека.
+Браузерная онлайн-читалка книг.
 
 
-Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
+Выглядит соледующим образом: <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru)
 
 
 ![](docs/assets/face.jpg)
 ![](docs/assets/face.jpg)
 ![](docs/assets/reader.jpg)
 ![](docs/assets/reader.jpg)
 
 
-## VPS
-Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
+При запуске приложения, по умолчанию веб-сервер доступен по адресу [http://127.0.0.1:44080](http://127.0.0.1:44080)
 
 
-## Сборка проекта
-Необходима версия node.js не ниже 14.
+Для указания местоположения рабочей директории, воспользуйтесь [параметрами командной строки](#cli).
+Дополнительные параметры сервера настраиваются в [конфигурационном файле](#config).
 
 
-```
-$ git clone https://github.com/bookpauk/liberama
-$ cd liberama
-$ npm i
-```
+[Отблагодарить автора проекта](https://donatty.com/liberama)
+
+## 
+* [Возможности читалки](#capabilities)
+* [Использование](#usage)
+    * [Параметры командной строки](#cli)
+    * [Конфигурация](#config)
+    * [Разворачивание на VPS](#vps)
+* [Сборка проекта](#build)
+* [Разработка](#development)
+
+<a id="capabilities" />
+
+## Возможности читалки
+- загрузка любой страницы интернета
+- синхронизация данных (настроек и читаемых книг) между различными устройствами
+- работа в автономном режиме (без связи)
+- изменение цвета фона, текста, размер и тип шрифта и прочее
+- установка и запоминание текущей позиции и настроек в браузере и на сервере
+- кэширование файлов книг на клиенте и на сервере
+- открытие книг с локального диска
+- плавный скроллинг текста
+- анимация перелистывания
+- поиск по тексту и копирование фрагмента
+- запоминание недавних книг, скачивание книги из читалки в формате fb2
+- управление кликом и с клавиатуры
+- регистрация не требуется
+- поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий
+- релизы сервера под Linux, MacOS и Windows
+
+<a id="usage" />
+
+## Использование
+Приложение представляет собой полноценный веб-сервер в виде единого исполнимого файла.
+При первом запуске, будет создана рабочая директория `.liberama` (по умолчанию - в той же папке, где исполнимый файл),
+в которой хранится конфигурационный файл `config.json`, файлы веб-приложения, файлы базы данных, журналы и прочее.
+Изменить рабочую директорию можно с помощью cli-параметра --app-dir
 
 
-### Windows
+По умолчанию веб-интерфейс будет доступен по адресу [http://127.0.0.1:44080](http://127.0.0.1:44080)
+
+<a id="cli" />
+
+### Параметры командной строки
+Запустите `liberama --help`, чтобы увидеть список опций:
+```console
+Usage: liberama [options]
+
+Options:
+  --help              Показать опции командной строки
+  --app-dir=<dirpath> Задать рабочую директорию, по умолчанию: <execDir>/.liberama
+  --auto-repair       Починить БД приложения при запуске, если она повреждена
 ```
 ```
-$ npm run build:win
+
+<a id="config" />
+
+### Конфигурация
+При первом запуске в рабочей директории будет создан конфигурационный файл `config.json`:
+```js
+{
+    // Максимальный размер файла загружаемой книги (в байтах)
+    "maxUploadFileSize": 52428800,
+
+    // Максимальный размер каталога <appDir>/public-files/tmp для хранения конвертированных
+    // файлов книг пользователей (в байтах)
+    "maxTempPublicDirSize": 536870912,
+
+    // Максимальный размер каталога <appDir>/public-files/upload для хранения
+    // загруженных в /upload (кнопка "Загрузить файл с диска") файлов книг пользователей (в байтах)
+    "maxUploadPublicDirSize": 209715200,
+
+    // Использование внешних конвертеров (только в среде Linux)
+    // Без них читалка может работать только с файлами формата fb2, txt, html, xml
+    // Инструкции установки внешних конвертеров см. в docs/omnireader.ru/README.md
+    "useExternalBookConverter": false,
+
+    // Настройки для списка серверов.
+    // Приложение может запускать одновременно несколько веб-серверов на разных портах
+    "servers": [
+        {
+            // Произвольное название сервера
+            "serverName": "1",
+
+            // Режим работы сервера:
+            //  "reader" - обычная читалка
+            //  "omnireader" - модификации для сайта omnireader.ru
+            //  "liberama" - модификации для сайта liberama.top
+            //  "book_update_checker" - сервер обновлений
+            "mode": "reader",
+
+            // Хост, порт сервера
+            "ip": "0.0.0.0",
+            "port": "44080"
+        }
+    ],
+
+    // Настройки удаленного хранилища
+    "remoteStorage": false,
+
+    // Для веб-приложения: включение/выключение работы с сервером обновлений
+    "bucEnabled": false,
+
+    // Подключение себя, как клиента, к серверу обновлений
+    "bucServer": false
+}
 ```
 ```
 
 
-### Linux
+При необходимости, можно настроить нужный параметр в этом файле вручную.
+
+<a id="vps" />
+
+## VPS
+Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
+
+<a id="build" />
+
+### Сборка проекта
+Сборка только в среде Linux.
+Необходима версия node.js не ниже 16.
+
+Для сборки linux-arm64 необходимо предварительно установить [QEMU](https://wiki.debian.org/QemuUserEmulation).
+
+```sh
+git clone https://github.com/bookpauk/liberama
+cd liberama
+npm i
 ```
 ```
-$ npm run build:linux
+
+#### Релизы
+```sh
+npm run release
 ```
 ```
 
 
-Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
+Результат сборки будет доступен в каталоге `dist/release`
+
+<a id="development" />
 
 
 ### Разработка
 ### Разработка
+```sh
+npm run dev
 ```
 ```
-$ npm run dev
-```
-
-## Помочь проекту
 
 
-* bitcoin: bc1q3tyumaj648pp2e69jalsez2lnt462ttc33nup9
-* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
-* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
+Связаться с автором проекта: [bookpauk@gmail.com](mailto:bookpauk@gmail.com)

+ 0 - 31
build/includer.js

@@ -1,31 +0,0 @@
-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');
-}

+ 0 - 51
build/linux.js

@@ -1,51 +0,0 @@
-const fs = require('fs-extra');
-const path = require('path');
-const util = require('util');
-const stream = require('stream');
-const pipeline = util.promisify(stream.pipeline);
-
-const axios = require('axios');
-const FileDecompressor = require('../server/core/FileDecompressor');
-
-const distDir = path.resolve(__dirname, '../dist');
-const publicDir = `${distDir}/tmp/public`;
-const outDir = `${distDir}/linux`;
-
-const tempDownloadDir = `${distDir}/tmp/download`;
-
-async function main() {
-    const decomp = new FileDecompressor();
-
-    await fs.emptyDir(outDir);
-    // перемещаем public на место
-    if (await fs.pathExists(publicDir))
-        await fs.move(publicDir, `${outDir}/public`);
-
-    await fs.ensureDir(tempDownloadDir);
-
-    //ipfs
-    const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
-    if (!await fs.pathExists(ipfsDecompressedFilename)) {
-        // Скачиваем ipfs
-        const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
-
-        const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
-        await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
-        console.log(`done downloading ${ipfsRemoteUrl}`);
-
-        //распаковываем
-        console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/ipfs.tar.gz`, tempDownloadDir));
-        console.log('files decompressed');
-    }
-
-    // копируем в дистрибутив
-    await fs.copy(ipfsDecompressedFilename, `${outDir}/ipfs`);
-    console.log(`copied ${tempDownloadDir}/go-ipfs/ipfs to ${outDir}/ipfs`);
-    //для development
-    const devIpfsFile = path.resolve(__dirname, '../server/ipfs');
-    if (!await fs.pathExists(devIpfsFile)) {
-        await fs.copy(ipfsDecompressedFilename, devIpfsFile);
-    }
-}
-
-main();

+ 51 - 0
build/prepkg.js

@@ -0,0 +1,51 @@
+const fs = require('fs-extra');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const showdown = require('showdown');
+
+const platform = process.argv[2];
+
+const distDir = path.resolve(__dirname, '../dist');
+const tmpDir = `${distDir}/tmp`;
+const publicDir = `${tmpDir}/public`;
+const outDir = `${distDir}/${platform}`;
+
+async function build() {
+    if (!platform)
+        throw new Error(`Please set platform`);
+
+    await fs.emptyDir(outDir);
+
+    //добавляем readme в релиз
+    let readme = await fs.readFile(path.resolve(__dirname, '../README.md'), 'utf-8');
+    const converter = new showdown.Converter();
+    readme = converter.makeHtml(readme);
+    await fs.writeFile(`${outDir}/readme.html`, readme);
+
+    // перемещаем public на место
+    if (await fs.pathExists(publicDir)) {
+
+        const zipFile = `${tmpDir}/public.zip`;
+        const jsonFile = `${distDir}/public.json`;//distDir !!!
+
+        await fs.remove(zipFile);
+        execSync(`zip -r ${zipFile} .`, {cwd: publicDir, stdio: 'inherit'});
+
+        const data = (await fs.readFile(zipFile)).toString('base64');
+        await fs.writeFile(jsonFile, JSON.stringify({data}));
+    } else {
+        throw new Error(`publicDir: ${publicDir} does not exist`);
+    }
+}
+
+async function main() {
+    try {
+        await build();
+    } catch(e) {
+        console.error(e);
+        process.exit(1);
+    }
+}
+
+main();

+ 33 - 0
build/release.js

@@ -0,0 +1,33 @@
+const fs = require('fs-extra');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const pckg = require('../package.json');
+
+const distDir = path.resolve(__dirname, '../dist');
+const outDir = `${distDir}/release`;
+
+async function makeRelease(target) {
+    const srcDir = `${distDir}/${target}`;
+
+    if (await fs.pathExists(srcDir)) {
+        const zipFile = `${outDir}/${pckg.name}-${pckg.version}-${target}.zip`;
+
+        execSync(`zip -r ${zipFile} .`, {cwd: srcDir, stdio: 'inherit'});
+    }
+}
+
+async function main() {
+    try {
+        await fs.emptyDir(outDir);
+        await makeRelease('win');
+        await makeRelease('linux');
+        await makeRelease('linux-arm64');
+        await makeRelease('macos');
+    } catch(e) {
+        console.error(e);
+        process.exit(1);
+    }
+}
+
+main();

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

@@ -30,10 +30,6 @@ module.exports = {
                     }
                     }
                 }*/
                 }*/
             },
             },
-            {
-                resourceQuery: /^\?vue/,
-                use: path.resolve(__dirname, 'includer.js')
-            },
             {
             {
                 test: /\.js$/,
                 test: /\.js$/,
                 loader: 'babel-loader',
                 loader: 'babel-loader',

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

@@ -1,5 +1,6 @@
 const path = require('path');
 const path = require('path');
 const webpack = require('webpack');
 const webpack = require('webpack');
+const pckg = require('../package.json');
 
 
 const { merge } = require('webpack-merge');
 const { merge } = require('webpack-merge');
 const baseWpConfig = require('./webpack.base.config');
 const baseWpConfig = require('./webpack.base.config');
@@ -8,16 +9,15 @@ baseWpConfig.entry.unshift('webpack-hot-middleware/client');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 
 
-const publicDir = path.resolve(__dirname, '../server/public');
+const publicDir = path.resolve(__dirname, `../server/.${pckg.name}/public`);
 const clientDir = path.resolve(__dirname, '../client');
 const clientDir = path.resolve(__dirname, '../client');
 
 
 module.exports = merge(baseWpConfig, {
 module.exports = merge(baseWpConfig, {
     mode: 'development',
     mode: 'development',
     devtool: 'inline-source-map',
     devtool: 'inline-source-map',
     output: {
     output: {
-        path: `${publicDir}/app`,
+        path: `${publicDir}${baseWpConfig.output.publicPath}`,
         filename: 'bundle.js',
         filename: 'bundle.js',
-        clean: true
     },
     },
 
 
     module: {
     module: {

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

@@ -17,9 +17,8 @@ const clientDir = path.resolve(__dirname, '../client');
 module.exports = merge(baseWpConfig, {
 module.exports = merge(baseWpConfig, {
     mode: 'production',
     mode: 'production',
     output: {
     output: {
-        path: `${publicDir}/app_new`,
+        path: `${publicDir}${baseWpConfig.output.publicPath}`,
         filename: 'bundle.[contenthash].js',
         filename: 'bundle.[contenthash].js',
-        clean: true        
     },
     },
     module: {
     module: {
         rules: [
         rules: [

+ 0 - 45
build/win.js

@@ -1,45 +0,0 @@
-const fs = require('fs-extra');
-const path = require('path');
-const util = require('util');
-const stream = require('stream');
-const pipeline = util.promisify(stream.pipeline);
-
-const axios = require('axios');
-const FileDecompressor = require('../server/core/FileDecompressor');
-
-const distDir = path.resolve(__dirname, '../dist');
-const publicDir = `${distDir}/tmp/public`;
-const outDir = `${distDir}/win`;
-
-const tempDownloadDir = `${distDir}/tmp/download`;
-
-async function main() {
-    const decomp = new FileDecompressor();
-
-    await fs.emptyDir(outDir);
-    // перемещаем public на место
-    if (await fs.pathExists(publicDir))
-        await fs.move(publicDir, `${outDir}/public`);
-
-    await fs.ensureDir(tempDownloadDir);
-
-    //ipfs
-    const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
-    if (!await fs.pathExists(ipfsDecompressedFilename)) {
-        // Скачиваем ipfs
-        const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
-
-        const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
-        await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
-        console.log(`done downloading ${ipfsRemoteUrl}`);
-
-        //распаковываем
-        console.log(await decomp.unpack(`${tempDownloadDir}/ipfs.zip`, tempDownloadDir));
-        console.log('files decompressed');
-    }
-    // копируем в дистрибутив
-    await fs.copy(ipfsDecompressedFilename, `${outDir}/ipfs.exe`);
-    console.log(`copied ${ipfsDecompressedFilename} to ${outDir}/ipfs.exe`);
-}
-
-main();

+ 4 - 16
client/api/misc.js

@@ -1,10 +1,5 @@
-import axios from 'axios';
 import wsc from './webSocketConnection';
 import wsc from './webSocketConnection';
 
 
-const api = axios.create({
-  baseURL: '/api'
-});
-
 class Misc {
 class Misc {
     async loadConfig() {
     async loadConfig() {
 
 
@@ -12,18 +7,11 @@ class Misc {
             'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch',
             'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch',
         ]};
         ]};
 
 
-        try {
-            const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
-            if (config.error)
-                throw new Error(config.error);
-            return config;
-        } catch (e) {
-            console.error(e);
-        }
+        const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
+        if (config.error)
+            throw new Error(config.error);
 
 
-        //если с WebSocket проблема, работаем по http
-        const response = await api.post('/config', query);
-        return response.data;
+        return config;
     }
     }
 }
 }
 
 

+ 22 - 66
client/api/reader.js

@@ -7,9 +7,9 @@ const api = axios.create({
     baseURL: '/api/reader'
     baseURL: '/api/reader'
 });
 });
 
 
-const workerApi = axios.create({
+/*const workerApi = axios.create({
     baseURL: '/api/worker'
     baseURL: '/api/worker'
-});
+});*/
 
 
 class Reader {
 class Reader {
     constructor() {
     constructor() {
@@ -19,58 +19,24 @@ class Reader {
         if (!callback) callback = () => {};
         if (!callback) callback = () => {};
 
 
         let response = {};
         let response = {};
-        try {
-            const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
-
-            let prevResponse = false;
-            while (1) {// eslint-disable-line no-constant-condition
-                response = await wsc.message(requestId);
-
-                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') {
-                    break;
-                }
-            }
-            return response;
-        } catch (e) {
-            console.error(e);
-        }
+        const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
 
 
-        //если с WebSocket проблема, работаем по http
-        const refreshPause = 500;
-        let i = 0;
-        response = {};
+        let prevResponse = false;
         while (1) {// eslint-disable-line no-constant-condition
         while (1) {// eslint-disable-line no-constant-condition
-            const prevProgress = response.progress || 0;
-            const prevState = response.state || 0;
-            response = await workerApi.post('/get-state', {workerId});
-            response = response.data;
-            callback(response);
-
-            if (!response.state)
-                throw new Error('Неверный ответ api');
+            response = await wsc.message(requestId);
+
+            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;
             }
             }
-
-            if (i > 0)
-                await utils.sleep(refreshPause);
-
-            i++;
-            if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
-                throw new Error('Слишком долгое время ожидания');
-            }
-            //проверка воркера
-            i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
         }
         }
 
 
         return response;
         return response;
@@ -79,14 +45,13 @@ class Reader {
     async loadBook(opts, callback) {
     async loadBook(opts, callback) {
         if (!callback) callback = () => {};
         if (!callback) callback = () => {};
 
 
-        let response = await api.post('/load-book', opts);
-
-        const workerId = response.data.workerId;
+        let response = await wsc.message(await wsc.send(Object.assign({action: 'load-book'}, opts)));
+        const workerId = response.workerId;
         if (!workerId)
         if (!workerId)
             throw new Error('Неверный ответ api');
             throw new Error('Неверный ответ api');
 
 
         callback({totalSteps: 4});
         callback({totalSteps: 4});
-        callback(response.data);
+        callback(response);
 
 
         response = await this.getWorkerStateFinish(workerId, callback);
         response = await this.getWorkerStateFinish(workerId, callback);
 
 
@@ -181,22 +146,13 @@ class Reader {
     }
     }
 
 
     async storage(request) {
     async storage(request) {
-        let response = null;
-        try {
-            response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
-        } catch (e) {
-            console.error(e);
-            //если с WebSocket проблема, работаем по http
-            response = await api.post('/storage', request);
-            response = response.data;
-        }
+        const response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
 
 
-        const state = response.state;
-        if (!state)
-            throw new Error('Неверный ответ api');
-        if (state == 'error') {
+        if (response.error)
             throw new Error(response.error);
             throw new Error(response.error);
-        }
+
+        if (!response.state)
+            throw new Error('Неверный ответ api');
 
 
         return response;
         return response;
     }
     }

+ 6 - 23
client/components/App.vue

@@ -39,16 +39,6 @@ class App {
     _options = componentOptions;
     _options = componentOptions;
     showPage = false;
     showPage = false;
 
 
-    itemRuText = {
-        '/cardindex': 'Картотека',
-        '/reader': 'Читалка',
-        '/forum': 'Форум-чат',
-        '/income': 'Поступления',
-        '/sources': 'Источники',
-        '/settings': 'Параметры',
-        '/help': 'Справка',
-    };
-
     created() {
     created() {
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
         this.state = this.$store.state;
         this.state = this.$store.state;
@@ -130,7 +120,7 @@ class App {
 
 
         this.setAppTitle();
         this.setAppTitle();
         (async() => {
         (async() => {
-            //загрузим конфиг сревера
+            //загрузим конфиг сервера
             try {
             try {
                 const config = await miscApi.loadConfig();
                 const config = await miscApi.loadConfig();
                 this.commit('config/setConfig', config);
                 this.commit('config/setConfig', config);
@@ -197,12 +187,12 @@ class App {
 
 
     setAppTitle(title) {
     setAppTitle(title) {
         if (!title) {
         if (!title) {
-            if (this.mode == 'liberama.top') {
+            if (this.mode == 'liberama') {
                 document.title = `Liberama Reader - всегда с вами`;
                 document.title = `Liberama Reader - всегда с вами`;
             } else if (this.mode == 'omnireader') {
             } else if (this.mode == 'omnireader') {
                 document.title = `Omni Reader - всегда с вами`;
                 document.title = `Omni Reader - всегда с вами`;
             } else if (this.config && this.mode !== null) {
             } else if (this.config && this.mode !== null) {
-                document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
+                document.title = `Универсальная читалка книг и ресурсов интернета`;
             }
             }
         } else {
         } else {
             document.title = title;
             document.title = title;
@@ -217,19 +207,12 @@ class App {
         return this.$store.state.config.mode;
         return this.$store.state.config.mode;
     }
     }
 
 
-    get showAsideBar() {
-        return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
-    }
-
-    set showAsideBar(value) {
-    }
-
     get isReaderActive() {
     get isReaderActive() {
         return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
         return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
     }
     }
 
 
     redirectIfNeeded() {
     redirectIfNeeded() {
-        if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
+        if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama')) {
             const search = window.location.search.substr(1);
             const search = window.location.search.substr(1);
 
 
             //распознавание параметра url вида "?url=<link>" и редирект при необходимости
             //распознавание параметра url вида "?url=<link>" и редирект при необходимости
@@ -271,8 +254,8 @@ body, html, #app {
     font: normal 12pt ReaderDefault;
     font: normal 12pt ReaderDefault;
 }
 }
 
 
-.notify-margin {
-    margin-top: 55px;
+.q-notifications__list--top {
+    top: 55px !important;
 }
 }
 
 
 .dborder {
 .dborder {

+ 0 - 19
client/components/CardIndex/Book/Book.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Book в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../../vueComponent.js';
-
-class Book {
-    created() {
-    }
-
-}
-
-export default vueComponent(Book);
-//-----------------------------------------------------------------------------
-</script>

+ 0 - 19
client/components/CardIndex/Card/Card.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Card в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../../vueComponent.js';
-
-class Card {
-    created() {
-    }
-
-}
-
-export default vueComponent(Card);
-//-----------------------------------------------------------------------------
-</script>

+ 0 - 93
client/components/CardIndex/CardIndex.vue

@@ -1,93 +0,0 @@
-<template>
-    <div>
-        <router-view v-slot="{ Component }">
-            <keep-alive>
-                <component :is="Component" />
-            </keep-alive>
-        </router-view>        
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../vueComponent.js';
-import _ from 'lodash';
-
-const selfRoute = '/cardindex';
-const tab2Route = [
-    '/cardindex/search',
-    '/cardindex/card',
-    '/cardindex/book',
-    '/cardindex/history',
-];
-let lastActiveTab = null;
-
-const componentOptions = {
-    watch: {
-        selectedTab: function(newValue) {
-            lastActiveTab = newValue;
-            this.setRouteByTab(newValue);
-        },
-        curRoute: function(newValue) {
-            this.setTabByRoute(newValue);
-        },
-    },
-};
-class CardIndex {
-    _options = componentOptions;
-    selectedTab = null;
-
-    created() {
-        this.$watch(
-            () => this.$route.path,
-            (newValue) => {
-                if (newValue == '/cardindex' && this.isReader) {
-                    this.$router.replace({ path: '/reader' });
-                }
-            }
-        )
-    }
-
-    mounted() {
-        this.setTabByRoute(this.curRoute);
-    }
-
-    setTabByRoute(route) {
-        const t = _.indexOf(tab2Route, route);
-        if (t >= 0) {
-            if (t !== this.selectedTab)
-                this.selectedTab = t.toString();
-        } else {
-            if (route == selfRoute && lastActiveTab !== null)
-                this.setRouteByTab(lastActiveTab);
-        }
-    }
-
-    setRouteByTab(tab) {
-        const t = Number(tab);
-        if (tab2Route[t] !== this.curRoute) {
-            this.$router.replace(tab2Route[t]);
-        }
-    }
-
-    get mode() {
-        return this.$store.state.config.mode;
-    }
-
-    get curRoute() {
-        const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
-        return (m ? m[1] : this.$route.path);
-    }
-
-    get isReader() {
-        return (this.mode !== null && (this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top'));
-    }
-
-}
-
-export default vueComponent(CardIndex);
-//-----------------------------------------------------------------------------
-</script>
-
-<style scoped>
-</style>

+ 0 - 19
client/components/CardIndex/History/History.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел History в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../../vueComponent.js';
-
-class History {
-    created() {
-    }
-
-}
-
-export default vueComponent(History);
-//-----------------------------------------------------------------------------
-</script>

+ 0 - 19
client/components/CardIndex/Search/Search.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Search в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../../vueComponent.js';
-
-class Search {
-    created() {
-    }
-
-}
-
-export default vueComponent(Search);
-//-----------------------------------------------------------------------------
-</script>

+ 1 - 0
client/components/ExternalLibs/BookmarkSettings/BookmarkSettings.vue

@@ -347,6 +347,7 @@ export default vueComponent(BookmarkSettings);
     padding: 0px 10px 10px 10px;
     padding: 0px 10px 10px 10px;
     overflow-x: auto;
     overflow-x: auto;
     overflow-y: auto;
     overflow-y: auto;
+    max-width: 520px;
 }
 }
 
 
 .selected {
 .selected {

+ 79 - 23
client/components/ExternalLibs/ExternalLibs.vue

@@ -110,7 +110,7 @@
 
 
             <div ref="frameBox" class="col fit" style="position: relative;">
             <div ref="frameBox" class="col fit" style="position: relative;">
                 <div ref="frameWrap" class="overflow-hidden">
                 <div ref="frameWrap" class="overflow-hidden">
-                    <iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
+                    <iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0" allow="clipboard-read; clipboard-write"></iframe>
                 </div>
                 </div>
                 <div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
                 <div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
             </div>
             </div>
@@ -304,6 +304,10 @@ class ExternalLibs {
     openInFrameOnAdd = false;
     openInFrameOnAdd = false;
     frameScale = 1;
     frameScale = 1;
 
 
+    inpxReady = false;
+    inpxTitle = '';
+    inpxUrl = '';
+
     created() {
     created() {
         this.oldStartLink = '';
         this.oldStartLink = '';
         this.justOpened = true;
         this.justOpened = true;
@@ -321,8 +325,6 @@ class ExternalLibs {
         this.debouncedGoToLink = _.debounce((link) => {
         this.debouncedGoToLink = _.debounce((link) => {
             this.goToLink(link);
             this.goToLink(link);
         }, 100, {'maxWait':200});
         }, 100, {'maxWait':200});
-        //this.commit = this.$store.commit;
-        //this.commit('reader/setLibs', rstore.libsDefaults);
     }
     }
 
 
     mounted() {
     mounted() {
@@ -334,10 +336,7 @@ class ExternalLibs {
                 i++;
                 i++;
             }
             }
 
 
-            if (this.mode != 'liberama.top') {
-                this.$router.replace('/404');
-                return;
-            }
+            this.libsDefaults = rstore.getLibsDefaults(this.mode);
 
 
             this.$refs.window.init();
             this.$refs.window.init();
 
 
@@ -348,17 +347,28 @@ class ExternalLibs {
             const openerOrigin2 = `https://${openerHost}`;
             const openerOrigin2 = `https://${openerHost}`;
 
 
             window.addEventListener('message', (event) => {
             window.addEventListener('message', (event) => {
+                //from inpx-web
+                if (_.isObject(event.data) && event.data.from === 'inpx-web') {
+                    //console.log(event);
+
+                    this.inpxOrigin = event.origin;
+
+                    this.recvInpxMessage(event.data);
+                    return;
+                }
+
+                //from parent
                 if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
                 if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
                     return;
                     return;
+
                 if (!_.isObject(event.data) || event.data.from != 'LibsPage')
                 if (!_.isObject(event.data) || event.data.from != 'LibsPage')
                     return;
                     return;
                 if (event.origin == openerOrigin1)
                 if (event.origin == openerOrigin1)
                     this.opener = window.opener;
                     this.opener = window.opener;
                 else
                 else
                     this.opener = event.source;
                     this.opener = event.source;
-                this.openerOrigin = event.origin;
 
 
-                //console.log(event);
+                this.openerOrigin = event.origin;
 
 
                 this.recvMessage(event.data);
                 this.recvMessage(event.data);
             });
             });
@@ -389,7 +399,8 @@ class ExternalLibs {
             }
             }
         } else if (d.type == 'libs') {
         } else if (d.type == 'libs') {
             this.ready = true;
             this.ready = true;
-            this.libs = _.cloneDeep(d.data);
+            if (d.data)
+                this.libs = _.cloneDeep(d.data);
         } else if (d.type == 'notify') {
         } else if (d.type == 'notify') {
             this.$root.notify.success(d.data, '', {position: 'bottom-right'});
             this.$root.notify.success(d.data, '', {position: 'bottom-right'});
         }
         }
@@ -403,6 +414,30 @@ class ExternalLibs {
         })();
         })();
     }
     }
 
 
+    recvInpxMessage(d) {
+        if (d.type == 'mes') {
+            switch(d.data) {
+                case 'hello-from-inpx-web':
+                    this.sendInpxMessage({type: 'mes', data: 'ready'});
+                    break;
+                case 'ready':
+                    this.inpxReady = true;
+                    break;
+            }
+        } else if (d.type == 'submitUrl') {
+            this.submitUrl(d.data);
+        } else if (d.type == 'titleChange') {
+            this.inpxTitle = d.data;
+        } else if (d.type == 'urlChange') {
+            this.inpxUrl = d.data;
+        }
+    }
+
+    sendInpxMessage(d) {
+        if (this.$refs.frame && this.inpxOrigin)
+            this.$refs.frame.contentWindow.postMessage(Object.assign({}, {from: 'ExternalLibs'}, d), this.inpxOrigin);
+    }
+
     async checkOpener() {
     async checkOpener() {
         if (this.opener.closed) {
         if (this.opener.closed) {
             await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
             await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
@@ -461,7 +496,10 @@ class ExternalLibs {
     get header() {
     get header() {
         let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
         let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
         if (this.ready && this.selectedLink) {
         if (this.ready && this.selectedLink) {
-            result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
+            let title = `${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
+            if (this.inpxReady && this.inpxTitle)
+                title = `${this.inpxTitle} ${lu.removeProtocol(this.inpxUrl)}`;
+            result += ` | ${title}`;
         }
         }
         this.$root.setAppTitle(result);
         this.$root.setAppTitle(result);
         return result;
         return result;
@@ -532,7 +570,7 @@ class ExternalLibs {
     get defaultRootLinkOptions() {
     get defaultRootLinkOptions() {
         let result = [];
         let result = [];
 
 
-        rstore.libsDefaults.groups.forEach(group => {
+        this.libsDefaults.groups.forEach(group => {
             result.push({label: lu.removeProtocol(group.r), value: group.r});
             result.push({label: lu.removeProtocol(group.r), value: group.r});
         });
         });
 
 
@@ -561,6 +599,11 @@ class ExternalLibs {
     }
     }
 
 
     goToLink(link) {
     goToLink(link) {
+        this.inpxReady = false;
+        this.inpxTitle = '';
+        this.inpxUrl = '';
+        this.inpxOrigin = false;
+
         if (!this.ready || !link)
         if (!this.ready || !link)
             return;
             return;
 
 
@@ -576,6 +619,7 @@ class ExternalLibs {
             this.frameVisible = true;
             this.frameVisible = true;
             this.$nextTick(() => {
             this.$nextTick(() => {
                 if (this.$refs.frame) {
                 if (this.$refs.frame) {
+                    this.$refs.frame.contentWindow.location.reload(true);
                     this.$refs.frame.contentWindow.focus();
                     this.$refs.frame.contentWindow.focus();
                     this.frameResize();
                     this.frameResize();
                 }
                 }
@@ -648,13 +692,17 @@ class ExternalLibs {
         this.updateStartLink(true);
         this.updateStartLink(true);
     }
     }
 
 
-    submitUrl() {
-        if (this.bookUrl) {
+    submitUrl(url) {
+        if (!url) {
+            url = this.bookUrl;
+            this.bookUrl = '';
+        }
+
+        if (url) {
             this.sendMessage({type: 'submitUrl', data: {
             this.sendMessage({type: 'submitUrl', data: {
-                url: this.bookUrl,
+                url,
                 force: true
                 force: true
             }});
             }});
-            this.bookUrl = '';
             if (this.closeAfterSubmit)
             if (this.closeAfterSubmit)
                 this.close();
                 this.close();
         }
         }
@@ -668,6 +716,12 @@ class ExternalLibs {
         } else {
         } else {
             this.bookmarkLink = this.bookUrl;
             this.bookmarkLink = this.bookUrl;
             this.bookmarkDesc = '';
             this.bookmarkDesc = '';
+
+            if (!this.bookmarkLink && this.inpxReady && this.inpxUrl) {
+                this.bookmarkLink = this.inpxUrl;
+                if (this.inpxTitle)
+                    this.bookmarkDesc = this.inpxTitle;
+            }
         }
         }
 
 
         this.addBookmarkMode = mode;
         this.addBookmarkMode = mode;
@@ -679,10 +733,10 @@ class ExternalLibs {
     }
     }
 
 
     updateBookmarkLink() {
     updateBookmarkLink() {
-        const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
+        const index = lu.getSafeRootIndexByUrl(this.libsDefaults.groups, this.defaultRootLink);
         if (index >= 0) {
         if (index >= 0) {
-            this.bookmarkLink = rstore.libsDefaults.groups[index].s;
-            this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
+            this.bookmarkLink = this.libsDefaults.groups[index].s;
+            this.bookmarkDesc = this.getCommentByLink(this.libsDefaults.groups[index].list, this.bookmarkLink);
         } else {
         } else {
             this.bookmarkLink = '';
             this.bookmarkLink = '';
             this.bookmarkDesc = '';
             this.bookmarkDesc = '';
@@ -837,20 +891,22 @@ class ExternalLibs {
 <p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
 <p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
 что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
 что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
 на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
 на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
-к сожалению, в нем открываются не все страницы.</p>
+к сожалению, в нем открываются не все страницы.</p>` + 
 
 
-<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
+(this.mode === 'liberama' ?
+`<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
 
 
 <br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
 <br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
 Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
 Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
 из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть 
 из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть 
 к третьим лицам.
 к третьим лицам.
 </p>
 </p>
+`
+: '') + 
 
 
-<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
+`<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
 На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера. 
 На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера. 
 </p>
 </p>
-
 <p>Приятного пользования ;-)
 <p>Приятного пользования ;-)
 </p>
 </p>
             `, 'Справка', {iconName: 'la la-info-circle'});
             `, 'Справка', {iconName: 'la la-info-circle'});

+ 0 - 19
client/components/Help/Help.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Help в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../vueComponent.js';
-
-class Help {
-    created() {
-    }
-
-}
-
-export default vueComponent(Help);
-//-----------------------------------------------------------------------------
-</script>

+ 0 - 19
client/components/Income/Income.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Income в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../vueComponent.js';
-
-class Income {
-    created() {
-    }
-
-}
-
-export default vueComponent(Income);
-//-----------------------------------------------------------------------------
-</script>

+ 0 - 19
client/components/NotFound404/NotFound404.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Страница не найдена
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../vueComponent.js';
-
-class NotFound404 {
-    created() {
-    }
-
-}
-
-export default vueComponent(NotFound404);
-//-----------------------------------------------------------------------------
-</script>

+ 1 - 1
client/components/Reader/HelpPage/CommonHelpPage/CommonHelpPage.vue

@@ -24,7 +24,7 @@
         </p>
         </p>
         <p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
         <p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
 
 
-        <div v-show="mode == 'omnireader' || mode == 'liberama.top'">
+        <div v-show="mode == 'omnireader' || mode == 'liberama'">
             <p>
             <p>
                 Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
                 Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
                 <br><strong>{{ bookmarkText }}</strong>
                 <br><strong>{{ bookmarkText }}</strong>

BIN
client/components/Reader/HelpPage/DonateHelpPage/assets/bitcoin.png


BIN
client/components/Reader/HelpPage/DonateHelpPage/assets/litecoin.png


BIN
client/components/Reader/HelpPage/DonateHelpPage/assets/monero.png


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

@@ -19,7 +19,7 @@
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 import vueComponent from '../../../vueComponent.js';
 import vueComponent from '../../../vueComponent.js';
 
 
-import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
+import UserHotKeys from '../../SettingsPage/KeysTab/UserHotKeys/UserHotKeys.vue';
 
 
 const componentOptions = {
 const componentOptions = {
     components: {
     components: {

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

@@ -13,7 +13,7 @@
             <li>Жесты для тачскрина:</li>
             <li>Жесты для тачскрина:</li>
             <ul>
             <ul>
                 <li style="list-style-type: square">
                 <li style="list-style-type: square">
-                    от центра вверх: на весь экран
+                    от центра вверх/двойной тап по центру: на весь экран
                 </li>
                 </li>
                 <li style="list-style-type: square">
                 <li style="list-style-type: square">
                     от центра вниз: плавный скроллинг
                     от центра вниз: плавный скроллинг

+ 9 - 4
client/components/Reader/LibsPage/LibsPage.vue

@@ -8,7 +8,7 @@ import vueComponent from '../../vueComponent.js';
 
 
 import Window from '../../share/Window.vue';
 import Window from '../../share/Window.vue';
 import * as utils from '../../../share/utils';
 import * as utils from '../../../share/utils';
-//import rstore from '../../../store/modules/reader';
+import rstore from '../../../store/modules/reader';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
 const componentOptions = {
 const componentOptions = {
@@ -28,13 +28,18 @@ class LibsPage {
         this.popupWindow = null;
         this.popupWindow = null;
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
         this.messageListener = null;
         this.messageListener = null;
-        //this.commit('reader/setLibs', rstore.libsDefaults);
     }
     }
 
 
-    init() {
-        if (this.mode != 'liberama.top')
+    async init() {
+        if (!this.mode)
             return;
             return;
 
 
+        //TODO: убрать второе условие в 24г
+        if (!this.libs || (this.mode === 'omnireader' && this.libs.mode !== this.mode)) {
+            const defaults = rstore.getLibsDefaults(this.mode);
+            this.commit('reader/setLibs', defaults);
+        }
+
         this.childReady = false;
         this.childReady = false;
         const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
         const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
         this.origin = `http://${subdomain}${window.location.host}`;
         this.origin = `http://${subdomain}${window.location.host}`;

+ 2 - 23
client/components/Reader/LoaderPage/LoaderPage.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
     <div ref="main" class="column no-wrap" style="min-height: 500px">
     <div ref="main" class="column no-wrap" style="min-height: 500px">
-        <div v-if="mode != 'liberama.top'" class="relative-position">
+        <div v-if="mode != 'liberama'" class="relative-position">
             <GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
             <GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
         </div>
         </div>
         <div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
         <div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
@@ -55,7 +55,6 @@
         </div>
         </div>
 
 
         <div class="col column justify-end items-center no-wrap overflow-hidden">
         <div class="col column justify-end items-center no-wrap overflow-hidden">
-            <span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
             <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>
 
 
@@ -64,18 +63,6 @@
         </div>
         </div>
 
 
         <PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
         <PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
-
-        <Dialog ref="dialog1" v-model="findBookVisible">
-            <template #header>
-                Подсказка ;-)
-            </template>
-
-            <div style="word-break: normal">
-                Если вы хотите найти определенную книгу, добро пожаловать в
-                раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
-                <a href="https://liberama.top" target="_blank">liberama.top</a>
-            </div>
-        </Dialog>
     </div>
     </div>
 </template>
 </template>
 
 
@@ -103,7 +90,6 @@ class LoaderPage {
     bookUrl = null;
     bookUrl = null;
     loadPercent = 0;
     loadPercent = 0;
     pasteTextActive = false;
     pasteTextActive = false;
-    findBookVisible = false;
 
 
     created() {
     created() {
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
@@ -122,7 +108,7 @@ class LoaderPage {
     get title() {
     get title() {
         if (this.mode == 'omnireader')
         if (this.mode == 'omnireader')
             return 'Omni Reader - браузерная онлайн-читалка.';
             return 'Omni Reader - браузерная онлайн-читалка.';
-        if (this.mode == 'liberama.top')
+        if (this.mode == 'liberama')
             return 'Liberama Reader - браузерная онлайн-читалка.';
             return 'Liberama Reader - браузерная онлайн-читалка.';
         return 'Универсальная читалка книг и ресурсов интернета.';
         return 'Универсальная читалка книг и ресурсов интернета.';
 
 
@@ -193,10 +179,6 @@ class LoaderPage {
         this.$emit('do-action', {action: 'donate'});
         this.$emit('do-action', {action: 'donate'});
     }
     }
     
     
-    findBook() {
-        this.findBookVisible = true;
-    }
-
     openComments() {
     openComments() {
         window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
         window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
     }
     }
@@ -213,9 +195,6 @@ class LoaderPage {
     }
     }
 
 
     keyHook(event) {
     keyHook(event) {
-        if (this.$refs.dialog1.active)
-            return true;
-
         if (this.pasteTextActive) {
         if (this.pasteTextActive) {
             return this.$refs.pasteTextPage.keyHook(event);
             return this.$refs.pasteTextPage.keyHook(event);
         }
         }

+ 1 - 1
client/components/Reader/LoaderPage/PasteTextPage/PasteTextPage.vue

@@ -60,7 +60,7 @@ class PasteTextPage {
 
 
     calcTitle(event) {
     calcTitle(event) {
         if (this.bookTitle == '') {
         if (this.bookTitle == '') {
-            this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`;
+            this.bookTitle = `Из буфера обмена ${utils.dateFormat(new Date())}`;
             if (event) {
             if (event) {
                 let text = event.clipboardData.getData('text');
                 let text = event.clipboardData.getData('text');
                 this.bookTitle += ': ' + _.compact([
                 this.bookTitle += ': ' + _.compact([

+ 149 - 156
client/components/Reader/Reader.vue

@@ -1,139 +1,138 @@
 <template>
 <template>
     <div class="column no-wrap">
     <div class="column no-wrap">
         <div v-show="toolBarActive" ref="header" class="header">
         <div v-show="toolBarActive" ref="header" class="header">
-            <div ref="buttons" class="row justify-between no-wrap">
-                <div class="row no-wrap">
-                    <button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
-                        <q-icon name="la la-arrow-left" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
-                            {{ rstore.readerActions['loader'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
-                        <q-icon name="la la-caret-square-up" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
-                            {{ rstore.readerActions['loadFile'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
-                        <q-icon name="la la-comment" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
-                            {{ rstore.readerActions['loadBuffer'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
-                        <q-icon name="la la-question" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
-                            {{ rstore.readerActions['help'] }}
-                        </q-tooltip>
-                    </button>
-                </div>
-
-                <div class="row no-wrap">
-                    <div class="space"></div>
-                    <button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
-                        <q-icon name="la la-angle-left" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['undoAction'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
-                        <q-icon name="la la-angle-right" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['redoAction'] }}
-                        </q-tooltip>
-                    </button>
-                    <div class="space"></div>
-                    <button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
-                        <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['fullScreen'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
-                        <q-icon name="la la-film" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['scrolling'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
-                        <q-icon name="la la-angle-double-right" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['setPosition'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
-                        <q-icon name="la la-search" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['search'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
-                        <q-icon name="la la-copy" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['copyText'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
-                        <q-icon name="la la-magic" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['convOptions'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
-                        <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['refresh'] }}
-                        </q-tooltip>
-                    </button>
-                    <div class="space"></div>
-                    <button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
-                        <q-icon name="la la-list" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['contents'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="mode == 'liberama.top' && showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
-                        <q-icon name="la la-sitemap" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['libs'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
-                        <div v-show="bothBucEnabled && needBookUpdateCount > 0" style="position: absolute">
-                            <div class="need-book-update-count">
-                                {{ needBookUpdateCount }}
-                            </div>
+            <div ref="buttons" class="row" :class="{'no-wrap': !toolBarMultiLine}">
+                <button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
+                    <q-icon name="la la-arrow-left" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
+                        {{ rstore.readerActions['loader'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
+                    <q-icon name="la la-caret-square-up" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
+                        {{ rstore.readerActions['loadFile'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
+                    <q-icon name="la la-comment" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
+                        {{ rstore.readerActions['loadBuffer'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
+                    <q-icon name="la la-question" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
+                        {{ rstore.readerActions['help'] }}
+                    </q-tooltip>
+                </button>
+
+                <div class="col"></div>
+
+                <div class="space"></div>
+                <button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
+                    <q-icon name="la la-angle-left" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['undoAction'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
+                    <q-icon name="la la-angle-right" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['redoAction'] }}
+                    </q-tooltip>
+                </button>
+                <div class="space"></div>
+                <button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
+                    <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['fullScreen'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
+                    <q-icon name="la la-film" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['scrolling'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
+                    <q-icon name="la la-angle-double-right" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['setPosition'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
+                    <q-icon name="la la-search" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['search'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
+                    <q-icon name="la la-copy" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['copyText'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
+                    <q-icon name="la la-magic" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['convOptions'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
+                    <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['refresh'] }}
+                    </q-tooltip>
+                </button>
+                <div v-show="showToolButton['libs']" class="space"></div>
+                <button v-show="showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
+                    <q-icon name="la la-sitemap" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['libs'] }}
+                    </q-tooltip>
+                </button>
+                <div class="space"></div>
+                <button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
+                    <q-icon name="la la-list" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['contents'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
+                    <div v-show="bothBucEnabled && needBookUpdateCount > 0" style="position: absolute">
+                        <div class="need-book-update-count">
+                            {{ needBookUpdateCount }}
                         </div>
                         </div>
-
-                        <q-icon name="la la-book-open" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['recentBooks'] }}
-                        </q-tooltip>
-                    </button>
-                    <div class="space"></div>
-                </div>
-
-                <div class="row no-wrap">
-                    <button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
-                        <q-icon name="la la-mouse" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['clickControl'] }}
-                        </q-tooltip>
-                    </button>
-                    <button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
-                        <q-icon name="la la-unlink" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
-                            {{ rstore.readerActions['offlineMode'] }}
-                        </q-tooltip>
-                    </button>
-                    <button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
-                        <q-icon name="la la-cog" size="32px" />
-                        <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">
-                            {{ rstore.readerActions['settings'] }}
-                        </q-tooltip>
-                    </button>
-                </div>
+                    </div>
+
+                    <q-icon name="la la-book-open" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['recentBooks'] }}
+                    </q-tooltip>
+                </button>
+                <div class="space"></div>
+
+                <div class="col"></div>
+
+                <button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
+                    <q-icon name="la la-mouse" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['clickControl'] }}
+                    </q-tooltip>
+                </button>
+                <button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
+                    <q-icon name="la la-unlink" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        {{ rstore.readerActions['offlineMode'] }}
+                    </q-tooltip>
+                </button>
+                <button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
+                    <q-icon name="la la-cog" size="32px" />
+                    <q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">
+                        {{ rstore.readerActions['settings'] }}
+                    </q-tooltip>
+                </button>
             </div>
             </div>
         </div>
         </div>
 
 
@@ -304,6 +303,8 @@ class Reader {
     showRefreshIcon = true;
     showRefreshIcon = true;
     mostRecentBookReactive = null;
     mostRecentBookReactive = null;
     showToolButton = {};
     showToolButton = {};
+    toolBarHideOnScroll = false;
+    toolBarMultiLine = false;
 
 
     actionList = [];
     actionList = [];
     actionCur = -1;
     actionCur = -1;
@@ -466,6 +467,7 @@ class Reader {
         this.blinkCachedLoad = settings.blinkCachedLoad;
         this.blinkCachedLoad = settings.blinkCachedLoad;
         this.showToolButton = settings.showToolButton;
         this.showToolButton = settings.showToolButton;
         this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
         this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
+        this.toolBarMultiLine = settings.toolBarMultiLine;
         this.enableSitesFilter = settings.enableSitesFilter;
         this.enableSitesFilter = settings.enableSitesFilter;
         this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
         this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
         this.splitToPara = settings.splitToPara;
         this.splitToPara = settings.splitToPara;
@@ -543,9 +545,7 @@ class Reader {
 
 
             //обновим settings, если загружали обои из /upload/
             //обновим settings, если загружали обои из /upload/
             if (updated) {
             if (updated) {
-                const newSettings = _.cloneDeep(this.settings);
-                newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
-                this.commit('reader/setSettings', newSettings);
+                this.commit('reader/setSettings', {});
             }
             }
 
 
             dynamicCss.replace('wallpapers', newCss);
             dynamicCss.replace('wallpapers', newCss);
@@ -807,7 +807,7 @@ class Reader {
     }
     }
 
 
     get offlineModeActive() {
     get offlineModeActive() {
-        return this.reader.offlineModeActive;        
+        return this.reader.offlineModeActive;
     }
     }
 
 
     mostRecentBook() {
     mostRecentBook() {
@@ -840,8 +840,7 @@ class Reader {
     }
     }
 
 
     fullScreenToggle() {
     fullScreenToggle() {
-        this.fullScreenActive = !this.fullScreenActive;
-        if (this.fullScreenActive) {
+        if (!this.$q.fullscreen.isActive) {
             this.$q.fullscreen.request();
             this.$q.fullscreen.request();
         } else {
         } else {
             this.$q.fullscreen.exit();
             this.$q.fullscreen.exit();
@@ -1009,7 +1008,7 @@ class Reader {
     libsToogle() {
     libsToogle() {
         this.libsActive = !this.libsActive;
         this.libsActive = !this.libsActive;
         if (this.libsActive) {
         if (this.libsActive) {
-            this.$refs.libsPage.init();
+            this.$refs.libsPage.init();//no await
         } else {
         } else {
             this.$refs.libsPage.done();
             this.$refs.libsPage.done();
         }
         }
@@ -1023,7 +1022,6 @@ class Reader {
 
 
     offlineModeToggle() {
     offlineModeToggle() {
         this.commit('reader/setOfflineModeActive', !this.offlineModeActive);
         this.commit('reader/setOfflineModeActive', !this.offlineModeActive);
-        this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
     }
     }
 
 
     settingsToggle() {
     settingsToggle() {
@@ -1652,33 +1650,27 @@ export default vueComponent(Reader);
 
 
 <style scoped>
 <style scoped>
 .header {
 .header {
-    height: 50px;
-    padding-left: 5px;
-    padding-right: 5px;
+    padding: 5px 5px 0px 5px;
     background-color: #1B695F;
     background-color: #1B695F;
     color: #000;
     color: #000;
     overflow-x: auto;
     overflow-x: auto;
     overflow-y: hidden;
     overflow-y: hidden;
-    scrollbar-color: #c49a60 #e4e4e4;
+    scrollbar-color: #c4aa60 #e4e4e4;
 }
 }
 
 
 .header::-webkit-scrollbar {
 .header::-webkit-scrollbar {
-    height: 10px;
+    height: 5px;
 }
 }
  
  
 .header::-webkit-scrollbar-track {
 .header::-webkit-scrollbar-track {
-    background-color: #e4e4e4;
-    border-radius: 4px;
+    background-color: #1B695F;
+    border-radius: 1px;
 }
 }
  
  
 .header::-webkit-scrollbar-thumb {
 .header::-webkit-scrollbar-thumb {
-    background-color: #c49a60;
-    border-radius: 4px;
-    border: 2px solid #e4e4e4;
-}
-
-.header::-webkit-scrollbar-thumb:hover {
-    background-color: #b48a50;
+    background-color: #c4aa60;
+    border-radius: 1px;
+    border: 1px solid #1B695F;
 }
 }
 
 
 .main {
 .main {
@@ -1687,11 +1679,12 @@ export default vueComponent(Reader);
 }
 }
 
 
 .tool-button {
 .tool-button {
-    margin: 0px 2px 0 2px;
+    margin: 0px 2px 7px 2px;
     padding: 0;
     padding: 0;
     color: #3E843E;
     color: #3E843E;
     background-color: #E6EDF4;
     background-color: #E6EDF4;
-    margin-top: 5px;
+    min-height: 38px;
+    min-width: 38px;
     height: 38px;
     height: 38px;
     width: 38px;
     width: 38px;
     border: 0;
     border: 0;

+ 30 - 23
client/components/Reader/ReaderDialogs/ReaderDialogs.vue

@@ -20,11 +20,11 @@
 
 
         <q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
         <q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
             <div class="column bg-white no-wrap q-pa-md">
             <div class="column bg-white no-wrap q-pa-md">
-                <div class="row justify-center q-mb-md" style="font-size: 110%">
+                <div class="row justify-center q-mb-md">
                     Здравствуйте, дорогие читатели!
                     Здравствуйте, дорогие читатели!
                 </div>
                 </div>
 
 
-                <div class="q-mx-md column" style="word-break: normal">
+                <div class="q-mx-md column" style="font-size: 90%; word-break: normal">
                     <div>
                     <div>
                         Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
                         Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
 
 
@@ -43,19 +43,31 @@
                         Однако на оплату хостинга читалки и сервера обновлений автор тратит свои 
                         Однако на оплату хостинга читалки и сервера обновлений автор тратит свои 
                         собственные средства, а также тратит свое время и силы на улучшение проекта.
                         собственные средства, а также тратит свое время и силы на улучшение проекта.
                         <br><br>
                         <br><br>
-                        Поддержим же материально наш ресурс, чтобы и дальше спокойно существовать и развиваться:
+                        Давайте поддержим наш ресурс, чтобы и дальше спокойно существовать и развиваться:
                     </div>
                     </div>
 
 
-                    <q-btn style="margin: 10px 50px 10px 50px" color="green-8" size="14px" no-caps @click="makeDonation">
+                    <q-btn style="margin: 10px 20px 10px 20px" color="green-8" no-caps @click="makeDonation">
                         <q-icon class="q-mr-xs" name="la la-donate" size="24px" />
                         <q-icon class="q-mr-xs" name="la la-donate" size="24px" />
                         Поддержать проект
                         Поддержать проект
                     </q-btn>
                     </q-btn>
 
 
-                    <q-btn style="margin: 0 50px 20px 50px" size="14px" no-caps @click="donationDialogRemind">
-                        Напомнить в следующем месяце
-                    </q-btn>
+                    <div class="row justify-center q-mt-sm">
+                        Напомнить снова через:
+                    </div>
+
+                    <div class="row justify-between" style="margin: 0 20px 10px 20px">
+                        <q-btn style="width: 140px; margin-top: 5px" no-caps @click="donationDialogRemindLater(30)">
+                            1 месяц
+                        </q-btn>
+                        <q-btn style="width: 140px; margin-top: 5px" no-caps @click="donationDialogRemindLater(60)">
+                            2 месяца
+                        </q-btn>
+                        <q-btn style="width: 140px; margin-top: 5px" no-caps @click="donationDialogRemindLater(90)">
+                            3 месяца
+                        </q-btn>
+                    </div>
 
 
-                    <div class="row justify-center">
+                    <div class="row justify-center q-mt-md">
                         <div class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
                         <div class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
                             Помочь проекту можно в любое время
                             Помочь проекту можно в любое время
                         </div>
                         </div>
@@ -71,12 +83,7 @@
             </template>
             </template>
 
 
             <div style="word-break: normal">
             <div style="word-break: normal">
-                Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
-                раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
-                <a href="https://liberama.top" target="_blank">liberama.top</a>
-
-                <br><br>
-                Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
+                Если вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
                 <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
                 <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
                     <q-icon class="q-mr-xs" name="la la-comment" size="24px" />
                     <q-icon class="q-mr-xs" name="la la-comment" size="24px" />
                     Из буфера обмена
                     Из буфера обмена
@@ -94,6 +101,7 @@ import vueComponent from '../../vueComponent.js';
 import Dialog from '../../share/Dialog.vue';
 import Dialog from '../../share/Dialog.vue';
 import * as utils from '../../../share/utils';
 import * as utils from '../../../share/utils';
 import {versionHistory} from '../versionHistory';
 import {versionHistory} from '../versionHistory';
+import rstore from '../../../store/modules/reader';
 
 
 const componentOptions = {
 const componentOptions = {
     components: {
     components: {
@@ -135,7 +143,7 @@ class ReaderDialogs {
     async showWhatsNew() {
     async showWhatsNew() {
         const whatsNew = versionHistory[0];
         const whatsNew = versionHistory[0];
         if (this.showWhatsNewDialog &&
         if (this.showWhatsNewDialog &&
-            whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
+            whatsNew.showUntil >= utils.dateFormat(new Date(), 'YYYY-MM-DD') &&
             this.whatsNewHeader != this.whatsNewContentHash) {
             this.whatsNewHeader != this.whatsNewContentHash) {
             await utils.sleep(2000);
             await utils.sleep(2000);
             this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
             this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
@@ -144,9 +152,7 @@ class ReaderDialogs {
     }
     }
 
 
     async showDonation() {
     async showDonation() {
-        const today = utils.formatDate(new Date(), 'coMonth');
-
-        if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && this.showDonationDialog && this.donationRemindDate != today) {
+        if ((this.mode == 'omnireader' || this.mode == 'liberama') && this.showDonationDialog && this.donationNextPopup <= Date.now()) {
             await utils.sleep(3000);
             await utils.sleep(3000);
             this.donationVisible = true;
             this.donationVisible = true;
         }
         }
@@ -161,14 +167,15 @@ class ReaderDialogs {
         this.urlHelpVisible = false;
         this.urlHelpVisible = false;
     }
     }
 
 
-    donationDialogRemind() {
+    donationDialogRemindLater(remindAfter = 30) {
         this.donationVisible = false;
         this.donationVisible = false;
-        this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coMonth'));
+
+        this.commit('reader/setDonationNextPopup', Date.now() + rstore.dayMs*remindAfter);
     }
     }
 
 
     makeDonation() {
     makeDonation() {
         utils.makeDonation();
         utils.makeDonation();
-        this.donationDialogRemind();
+        this.donationDialogRemindLater();
     }
     }
 
 
     openDonate() {
     openDonate() {
@@ -209,8 +216,8 @@ class ReaderDialogs {
         return this.$store.state.reader.whatsNewContentHash;
         return this.$store.state.reader.whatsNewContentHash;
     }
     }
 
 
-    get donationRemindDate() {
-        return this.$store.state.reader.donationRemindDate;
+    get donationNextPopup() {
+        return this.$store.state.reader.donationNextPopup;
     }
     }
 
 
     keyHook() {
     keyHook() {

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

@@ -367,10 +367,10 @@ class RecentBooksPage {
 
 
                 let d = new Date();
                 let d = new Date();
                 d.setTime(book.touchTime);
                 d.setTime(book.touchTime);
-                const touchTime = utils.formatDate(d);
+                const touchTime = utils.dateFormat(d, 'DD.MM.YYYY HH:mm');
                 const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
                 const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
                 d.setTime(loadTimeRaw);
                 d.setTime(loadTimeRaw);
-                const loadTime = utils.formatDate(d);
+                const loadTime = utils.dateFormat(d, 'DD.MM.YYYY HH:mm');
 
 
                 let readPart = 0;
                 let readPart = 0;
                 let perc = '';
                 let perc = '';

+ 12 - 5
client/components/Reader/SearchPage/SearchPage.vue

@@ -20,10 +20,10 @@
             </div>
             </div>
             <q-btn-group v-show="!initStep" class="button-group row no-wrap">
             <q-btn-group v-show="!initStep" class="button-group row no-wrap">
                 <q-btn class="button" dense stretch @click="showNext">
                 <q-btn class="button" dense stretch @click="showNext">
-                    <q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
+                    <q-icon style="top: -2px" name="la la-angle-down" dense size="22px" />
                 </q-btn>
                 </q-btn>
                 <q-btn class="button" dense stretch @click="showPrev">
                 <q-btn class="button" dense stretch @click="showPrev">
-                    <q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
+                    <q-icon name="la la-angle-up" dense size="22px" />
                 </q-btn>
                 </q-btn>
             </q-btn-group>
             </q-btn-group>
         </div>
         </div>
@@ -108,10 +108,15 @@ class SearchPage {
 
 
         this.header = 'Поиск в тексте';
         this.header = 'Поиск в тексте';
         await this.$nextTick();
         await this.$nextTick();
-        this.$refs.input.focus();
+        this.focusInput();
         this.$refs.input.select();
         this.$refs.input.select();
     }
     }
 
 
+    focusInput() {
+        if (!this.$root.isMobileDevice)
+            this.$refs.input.focus();
+    }
+
     get foundText() {
     get foundText() {
         if (this.foundList.length && this.foundCur >= 0)
         if (this.foundList.length && this.foundCur >= 0)
             return `${this.foundCur + 1}/${this.foundList.length}`;
             return `${this.foundCur + 1}/${this.foundList.length}`;
@@ -149,7 +154,8 @@ class SearchPage {
         } else {
         } else {
             this.$emit('stop-text-search');
             this.$emit('stop-text-search');
         }
         }
-        this.$refs.input.focus();
+
+        this.focusInput();
     }
     }
 
 
     showPrev() {
     showPrev() {
@@ -165,7 +171,8 @@ class SearchPage {
         } else {
         } else {
             this.$emit('stop-text-search');
             this.$emit('stop-text-search');
         }
         }
-        this.$refs.input.focus();
+
+        this.focusInput();
     }
     }
 
 
     close() {
     close() {

+ 8 - 1
client/components/Reader/ServerStorage/ServerStorage.vue

@@ -49,6 +49,7 @@ class ServerStorage {
         this.keyInited = false;
         this.keyInited = false;
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
         this.prevServerStorageKey = null;
         this.prevServerStorageKey = null;
+        this.identity = utils.randomHexString(20);
         this.lock = new LockQueue(100);
         this.lock = new LockQueue(100);
 
 
         this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
         this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
@@ -204,6 +205,10 @@ class ServerStorage {
         return this.$store.state.reader.libsRev;
         return this.$store.state.reader.libsRev;
     }
     }
 
 
+    get offlineModeActive() {
+        return this.$store.state.reader.offlineModeActive;
+    }
+
     checkCurrentProfile() {
     checkCurrentProfile() {
         if (!this.profiles[this.currentProfile]) {
         if (!this.profiles[this.currentProfile]) {
             this.commit('reader/setCurrentProfile', '');
             this.commit('reader/setCurrentProfile', '');
@@ -643,6 +648,8 @@ class ServerStorage {
                     await this.setCachedRecentPatch(newRecentPatch);
                     await this.setCachedRecentPatch(newRecentPatch);
                 if (needSaveRecentMod && newRecentMod.rev)
                 if (needSaveRecentMod && newRecentMod.rev)
                     await this.setCachedRecentMod(newRecentMod);
                     await this.setCachedRecentMod(newRecentMod);
+            } else {
+                this.prevItemKey = null;
             }
             }
         } finally {
         } finally {
             this.lock.ret();
             this.lock.ret();
@@ -665,7 +672,7 @@ class ServerStorage {
     }
     }
 
 
     async storageApi(action, items, force) {
     async storageApi(action, items, force) {
-        const request = {action, items};
+        const request = {action, identity: this.identity, items};
         if (force)
         if (force)
             request.force = true;
             request.force = true;
         const encodedRequest = await this.encodeStorageItems(request);
         const encodedRequest = await this.encodeStorageItems(request);

+ 0 - 87
client/components/Reader/SettingsPage/ConvertTab.inc

@@ -1,87 +0,0 @@
-<!---------------------------------------------->
-<div class="q-mt-sm column items-center">
-    <span>Настройки конвертирования применяются ко всем</span>
-    <span>вновь загружаемым или обновляемым файлам</span>
-</div>
-
-<!---------------------------------------------->
-<div class="part-header">HTML, XML, TXT</div>
-
-<div class="item row">
-    <div class="label-7">Текст</div>
-    <div class="col row">
-        <q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
-            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-                Опция принудительно включает эвристику разбиения текста на<br>
-                параграфы в случае, если формат файла определен как html,<br>
-                xml или txt. Возможна нечитабельная разметка текста.
-            </q-tooltip>
-        </q-checkbox>
-    </div>
-</div>
-
-<div class="item row">
-    <div class="label-7">Сайты</div>
-    <div class="col row">
-        <q-checkbox v-model="enableSitesFilter" 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 v-if="isExternalConverter">
-    <div class="part-header">PDF</div>
-
-    <div class="item row">
-        <div class="label-7">Формат</div>
-        <div class="col row">
-            <q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
-                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-                    Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
-                    Размер получаемого fb2-файла при этом относительно небольшой.<br>
-                    При отключении этой опции, pdf будет представлен как набор<br>
-                    изображений (аналогично ковертированию djvu).
-                </q-tooltip>
-            </q-checkbox>
-        </div>
-    </div>
-
-    <div class="item row">
-        <div class="label-7">Качество</div>
-        <div class="col row">
-            <NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
-                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-                    Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
-                    размер итогового файла. Если сервер отказывается конвертировать<br>
-                    слишком большой файл, то попробуйте понизить качество.
-                </q-tooltip>
-            </NumInput>
-        </div>
-    </div>
-</div>
-
-<!---------------------------------------------->
-<div v-if="isExternalConverter">
-    <div class="part-header">DJVU</div>
-
-    <div class="item row">
-        <div class="label-7">Качество</div>
-        <div class="col row">
-            <NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
-                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-                    Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
-                    размер итогового файла. Если сервер отказывается конвертировать<br>
-                    слишком большой файл, то попробуйте понизить качество.
-                </q-tooltip>
-            </NumInput>
-        </div>
-    </div>
-</div>

+ 145 - 0
client/components/Reader/SettingsPage/ConvertTab/ConvertTab.vue

@@ -0,0 +1,145 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <!---------------------------------------------->
+        <div class="q-mt-sm column items-center">
+            <span>Настройки конвертирования применяются ко всем</span>
+            <span>вновь загружаемым или обновляемым файлам</span>
+        </div>
+
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            HTML, XML, TXT
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Текст
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="form.splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
+                    <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                        Опция принудительно включает эвристику разбиения текста на<br>
+                        параграфы в случае, если формат файла определен как html,<br>
+                        xml или txt. Возможна нечитабельная разметка текста.
+                    </q-tooltip>
+                </q-checkbox>
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Сайты
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="form.enableSitesFilter" 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 v-if="isExternalConverter">
+            <div class="sets-part-header">
+                PDF
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Формат
+                </div>
+                <div class="col row">
+                    <q-checkbox v-model="form.pdfAsText" size="xs" label="Извлекать текст из PDF">
+                        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                            Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
+                            Размер получаемого fb2-файла при этом относительно небольшой.<br>
+                            При отключении этой опции, pdf будет представлен как набор<br>
+                            изображений (аналогично ковертированию djvu).
+                        </q-tooltip>
+                    </q-checkbox>
+                </div>
+            </div>
+
+            <div v-if="!form.pdfAsText" class="sets-item row">
+                <div class="sets-label label">
+                    Качество
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.pdfQuality" class="col-5" :min="10" :max="100">
+                        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                            Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
+                            размер итогового файла. Если сервер отказывается конвертировать<br>
+                            слишком большой файл, то попробуйте понизить качество.
+                        </q-tooltip>
+                    </NumInput>
+                </div>
+            </div>
+        </div>
+
+        <!---------------------------------------------->
+        <div v-if="isExternalConverter">
+            <div class="sets-part-header">
+                DJVU
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Качество
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.djvuQuality" class="col-5" :min="10" :max="100">
+                        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                            Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
+                            размер итогового файла. Если сервер отказывается конвертировать<br>
+                            слишком большой файл, то попробуйте понизить качество.
+                        </q-tooltip>
+                    </NumInput>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+import NumInput from '../../../share/NumInput.vue';
+
+const componentOptions = {
+    components: {
+        NumInput
+    },
+};
+class ConvertTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+    get isExternalConverter() {
+        return this.$store.state.config.useExternalBookConverter;
+    }
+}
+
+export default vueComponent(ConvertTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 75px;
+}
+
+</style>

+ 0 - 33
client/components/Reader/SettingsPage/KeysTab.inc

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

+ 78 - 0
client/components/Reader/SettingsPage/KeysTab/KeysTab.vue

@@ -0,0 +1,78 @@
+<template>
+    <div class="fit column">
+        <div class="bg-grey-3 row">
+            <q-tabs
+                v-model="selectedTab"
+                active-color="black"
+                active-bg-color="white"
+                indicator-color="white"
+                dense
+                no-caps
+                class="bg-grey-4 text-grey-7"
+            >
+                <q-tab name="mouse" label="Мышь/тачскрин" />
+                <q-tab name="keyboard" label="Клавиатура" />
+            </q-tabs>
+        </div>
+
+        <div class="q-mb-sm" />
+
+        <div class="col sets-tab-panel">
+            <div v-if="selectedTab == 'mouse'">
+                <div class="sets-item row">
+                    <div class="sets-label label"></div>
+                    <div class="col row">
+                        <q-checkbox v-model="form.clickControl" size="xs" label="Включить управление кликом" />
+                    </div>
+                </div>
+            </div>
+
+            <div v-if="selectedTab == 'keyboard'">
+                <div class="sets-item row">
+                    <UserHotKeys v-model="form.userHotKeys" />
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+
+import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
+
+const componentOptions = {
+    components: {
+        UserHotKeys,
+    },
+};
+class KeysTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    selectedTab = 'mouse';
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+    get mode() {
+        return this.$store.state.config.mode;
+    }
+}
+
+export default vueComponent(KeysTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+</style>

+ 3 - 4
client/components/Reader/SettingsPage/UserHotKeys/UserHotKeys.vue → client/components/Reader/SettingsPage/KeysTab/UserHotKeys/UserHotKeys.vue

@@ -73,10 +73,9 @@
 
 
 <script>
 <script>
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
-import vueComponent from '../../../vueComponent.js';
+import vueComponent from '../../../../vueComponent.js';
 
 
-import rstore from '../../../../store/modules/reader';
-//import * as utils from '../../share/utils';
+import rstore from '../../../../../store/modules/reader';
 
 
 const componentOptions = {
 const componentOptions = {
     watch: {
     watch: {
@@ -116,7 +115,7 @@ class UserHotKeys {
     }
     }
 
 
     updateTableData() {
     updateTableData() {
-        let result = rstore.hotKeys.map(hk => hk.name).filter(name => (this.mode == 'liberama.top' || name != 'libs'));
+        let result = rstore.hotKeys.map(hk => hk.name);
 
 
         const search = this.search.toLowerCase();
         const search = this.search.toLowerCase();
         const codesIncludeSearch = (action) => {
         const codesIncludeSearch = (action) => {

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

@@ -1,91 +0,0 @@
-<!---------------------------------------------->
-<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="showDonationDialog">
-        Показывать форму доната
-        <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">Обработка</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">Парам. в 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="copyFullText" label="Загружать весь текст">
-        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-            Загружать весь текст в окно<br>
-            копирования текста со страницы
-        </q-tooltip>
-    </q-checkbox>
-</div>

+ 148 - 0
client/components/Reader/SettingsPage/OthersTab/OthersTab.vue

@@ -0,0 +1,148 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            Подсказки, уведомления
+        </div>
+
+        <div class="sets-item row no-wrap">
+            <div class="sets-label label">
+                Подсказка
+            </div>
+            <q-checkbox v-model="form.showClickMapPage" size="xs" label="Показывать области управления кликом" :disable="!form.clickControl">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Показывать или нет подсказку при каждой загрузке книги
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Подсказка
+            </div>
+            <q-checkbox v-model="form.blinkCachedLoad" size="xs" label="Предупреждать о загрузке из кэша">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Мерцать сообщением в строке статуса и на кнопке<br>
+                    обновления при загрузке книги из кэша
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div class="sets-item row no-wrap">
+            <div class="sets-label label">
+                Уведомление
+            </div>
+            <q-checkbox v-model="form.showServerStorageMessages" size="xs" label="Показывать сообщения синхронизации">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Показывать уведомления и ошибки от<br>
+                    синхронизатора данных с сервером
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Уведомление
+            </div>
+            <q-checkbox v-model="form.showWhatsNewDialog" size="xs">
+                Показывать уведомление "Что нового"
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Показывать уведомления "Что нового"<br>
+                    при появлении новой версии читалки
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Уведомление
+            </div>
+            <q-checkbox v-model="form.showDonationDialog" size="xs">
+                Показывать форму доната
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Показывать диалог для сбора пожертвований
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            Другое
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Обработка
+            </div>
+            <q-checkbox v-model="form.lazyParseEnabled" size="xs" 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="sets-item row">
+            <div class="sets-label label">
+                Парам. в URL
+            </div>
+            <q-checkbox v-model="form.allowUrlParamBookPos" size="xs">
+                Добавлять параметр "__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="sets-item row">
+            <div class="sets-label label">
+                Копирование
+            </div>
+            <q-checkbox v-model="form.copyFullText" size="xs" label="Загружать весь текст">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Загружать весь текст в окно<br>
+                    копирования текста со страницы
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+
+const componentOptions = {
+    components: {
+    },
+};
+class OthersTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+}
+
+export default vueComponent(OthersTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 100px;
+}
+
+</style>

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

@@ -1,28 +0,0 @@
-<!---------------------------------------------->
-<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>

+ 96 - 0
client/components/Reader/SettingsPage/PageMoveTab/PageMoveTab.vue

@@ -0,0 +1,96 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            Анимация
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Тип
+            </div>
+            <q-select
+                v-model="form.pageChangeAnimation" class="col-left" :options="pageChangeAnimationOptions"
+                dropdown-icon="la la-angle-down la-sm"
+                outlined dense emit-value map-options
+            />
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Скорость
+            </div>
+            <NumInput v-model="form.pageChangeAnimationSpeed" class="col-left" :min="0" :max="100" :disable="form.pageChangeAnimation == ''" />
+        </div>
+
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            Другое
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Страница
+            </div>
+            <q-checkbox v-model="form.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>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+import NumInput from '../../../share/NumInput.vue';
+
+const componentOptions = {
+    components: {
+        NumInput,
+    },
+};
+class PageMoveTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+    get pageChangeAnimationOptions() {
+        let result = [
+            {label: 'Нет', value: ''},
+            {label: 'Вверх-вниз', value: 'downShift'},
+            (!this.form.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
+            {label: 'Протаивание', value: 'thaw'},
+            {label: 'Мерцание', value: 'blink'},
+            {label: 'Вращение', value: 'rotate'},
+            (this.form.wallpaper == '' && !this.form.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
+        ];        
+
+        result = result.filter(v => v);
+
+        return result;
+    }
+}
+
+export default vueComponent(PageMoveTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+.col-left {
+    width: 150px;
+}
+</style>

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

@@ -1,101 +0,0 @@
-<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 display-value-sanitize options-sanitize
-            />
-        </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' || mode == 'liberama.top'">
-                <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>

+ 362 - 0
client/components/Reader/SettingsPage/ProfilesTab/ProfilesTab.vue

@@ -0,0 +1,362 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <div class="sets-part-header">
+            Управление синхронизацией данных
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="serverSyncEnabled" class="col" size="xs" label="Включить синхронизацию с сервером" />
+        </div>
+
+        <div v-show="serverSyncEnabled">
+            <!---------------------------------------------->
+            <div class="sets-part-header">
+                Профили устройств
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label"></div>
+                <div class="text col">
+                    Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
+                    <br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
+                </div>
+            </div>
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Устройство
+                </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 display-value-sanitize options-sanitize
+                    />
+                </div>
+            </div>
+            <div class="sets-item row">
+                <div class="sets-label label"></div>
+                <q-btn class="sets-button" dense no-caps @click="addProfile">
+                    Добавить
+                </q-btn>
+                <q-btn class="sets-button" dense no-caps @click="delProfile">
+                    Удалить
+                </q-btn>
+                <q-btn class="sets-button" dense no-caps @click="delAllProfiles">
+                    Удалить все
+                </q-btn>
+            </div>
+
+            <!---------------------------------------------->
+            <div class="sets-part-header">
+                Ключ доступа
+            </div>
+            
+            <div class="sets-item row">
+                <div class="sets-label label"></div>
+                <div class="text col">
+                    Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
+                    Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
+                </div>
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label"></div>
+                <q-btn class="sets-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="sets-item row">
+                <div class="sets-label label"></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' || mode == 'liberama'">
+                        <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="sets-item row">
+                <div class="sets-label label"></div>
+                <q-btn class="sets-button" style="width: 250px" dense no-caps @click="enterServerStorageKey">
+                    Ввести ключ доступа
+                </q-btn>
+            </div>
+            <div class="sets-item row">
+                <div class="sets-label label"></div>
+                <q-btn class="sets-button" style="width: 250px" dense no-caps @click="generateServerStorageKey">
+                    Сгенерировать новый ключ
+                </q-btn>
+            </div>
+            <div class="sets-item row">
+                <div class="sets-label label"></div>
+                <div class="text col">
+                    Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
+                    например, после переустановки ОС или чистки/смены браузера.<br>
+                    <b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
+                    и шифруются ключом доступа перед отправкой на сервер.
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+
+import _ from 'lodash';
+
+import * as utils from '../../../../share/utils';
+import rstore from '../../../../store/modules/reader';
+
+const componentOptions = {
+    watch: {
+    },
+};
+class ProfilesTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    rstore = rstore;
+
+    serverStorageKeyVisible = false;
+
+    created() {
+        this.commit = this.$store.commit;
+    }
+
+    mounted() {
+    }
+
+    get mode() {
+        return this.$store.state.config.mode;
+    }
+
+    get serverSyncEnabled() {
+        return this.$store.state.reader.serverSyncEnabled;
+    }
+
+    set serverSyncEnabled(newValue) {
+        this.commit('reader/setServerSyncEnabled', newValue);
+    }
+
+    get currentProfile() {
+        return this.$store.state.reader.currentProfile;
+    }
+
+    set currentProfile(newValue) {
+        this.commit('reader/setCurrentProfile', newValue);
+    }
+
+    get profiles() {
+        return this.$store.state.reader.profiles;
+    }
+
+    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 partialStorageKey() {
+        return this.serverStorageKey.substr(0, 7) + '***';
+    }
+
+    get serverStorageKey() {
+        return this.$store.state.reader.serverStorageKey;
+    }
+
+    get setStorageKeyLink() {
+        return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
+    }
+
+    async addProfile() {
+        try {
+            if (Object.keys(this.profiles).length >= 100) {
+                this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
+                return;
+            }
+            const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
+                inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
+            });
+            if (result && result.value) {
+                if (this.profiles[result.value]) {
+                    this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
+                } else {
+                    const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
+                    this.commit('reader/setAllowProfilesSave', true);
+                    await this.$nextTick();//ждем обработчики watch
+                    this.commit('reader/setProfiles', newProfiles);
+                    await this.$nextTick();//ждем обработчики watch
+                    this.commit('reader/setAllowProfilesSave', false);
+                    this.currentProfile = result.value;
+                }
+            }
+        } catch (e) {
+            //
+        }
+    }
+
+    async delProfile() {
+        if (!this.currentProfile)
+            return;
+
+        try {
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
+                    `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
+                    `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
+                inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
+            });
+
+            if (result && result.value && result.value.toLowerCase() == 'да') {
+                if (this.profiles[this.currentProfile]) {
+                    const newProfiles = Object.assign({}, this.profiles);
+                    delete newProfiles[this.currentProfile];
+                    this.commit('reader/setAllowProfilesSave', true);
+                    await this.$nextTick();//ждем обработчики watch
+                    this.commit('reader/setProfiles', newProfiles);
+                    await this.$nextTick();//ждем обработчики watch
+                    this.commit('reader/setAllowProfilesSave', false);
+                    this.currentProfile = '';
+                }
+            }
+        } catch (e) {
+            //
+        }
+    }
+
+    async delAllProfiles() {
+        if (!Object.keys(this.profiles).length)
+            return;
+
+        try {
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
+                    `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
+                inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
+            });
+
+            if (result && result.value && result.value.toLowerCase() == 'да') {
+                this.commit('reader/setAllowProfilesSave', true);
+                await this.$nextTick();//ждем обработчики watch
+                this.commit('reader/setProfiles', {});
+                await this.$nextTick();//ждем обработчики watch
+                this.commit('reader/setAllowProfilesSave', false);
+                this.currentProfile = '';
+            }
+        } catch (e) {
+            //
+        }
+    }
+
+    async showServerStorageKey() {
+        this.serverStorageKeyVisible = !this.serverStorageKeyVisible;
+    }
+
+    async enterServerStorageKey(key) {
+        try {
+            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),
+            });
+
+            if (result && result.value && utils.fromBase58(result.value).length == 32) {
+                this.commit('reader/setServerStorageKey', result.value);
+            }
+        } catch (e) {
+            //
+        }
+    }
+
+    async generateServerStorageKey() {
+        try {
+            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
+                    `<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
+                inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
+            });
+
+            if (result && result.value && result.value.toLowerCase() == 'да') {
+                if (this.$root.generateNewServerStorageKey)
+                    this.$root.generateNewServerStorageKey();
+            }
+        } catch (e) {
+            //
+        }
+
+    }
+
+    async copyToClip(text, prefix) {
+        const result = await utils.copyTextToClipboard(text);
+        const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
+        const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
+        if (result)
+            this.$root.notify.success(msg);
+        else
+            this.$root.notify.error(msg);
+    }
+}
+
+export default vueComponent(ProfilesTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 75px;
+}
+
+.text {
+    font-size: 90%;
+    line-height: 130%;
+}
+
+.copy-icon {
+    margin-left: 5px;
+    cursor: pointer;
+    font-size: 120%;
+    color: blue;
+}
+</style>

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

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

+ 41 - 0
client/components/Reader/SettingsPage/ResetTab/ResetTab.vue

@@ -0,0 +1,41 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <div class="sets-item row">
+            <q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">
+                Установить по умолчанию
+            </q-btn>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+
+const componentOptions = {
+    components: {
+    },
+};
+class ResetTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+    setDefaults() {
+        this.$emit('tab-event', {action: 'set-defaults'});
+    }
+}
+
+export default vueComponent(ResetTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+</style>

+ 89 - 649
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -5,13 +5,12 @@
         </template>
         </template>
 
 
         <div class="col row">
         <div class="col row">
-            <a ref="download" style="display: none;" target="_blank"></a>
-
             <div class="full-height">
             <div class="full-height">
                 <q-tabs
                 <q-tabs
                     ref="tabs"
                     ref="tabs"
                     v-model="selectedTab"
                     v-model="selectedTab"
-                    class="bg-grey-3 text-black"
+                    class="bg-grey-3 text-grey-9"
+                    style="max-width: 130px"
                     
                     
                     left-icon="la la-caret-up"
                     left-icon="la la-caret-up"
                     right-icon="la la-caret-down"
                     right-icon="la la-caret-down"
@@ -23,95 +22,34 @@
                     stretch
                     stretch
                     inline-label
                     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="toolbar" 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="convert" icon="la la-magic" label="Конвертир." />
-                    <q-tab class="tab" name="update" icon="la la-sync" 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-tab v-for="item in tabs" :key="item.name" class="tab row items-center" :name="item.name">
+                        <q-icon :name="item.icon" :color="selectedTab == item.name ? 'yellow' : 'teal-7'" size="24px" />
+                        <div class="q-ml-xs" style="font-size: 90%">
+                            {{ item.label }}
+                        </div>
+                    </q-tab>
                 </q-tabs>
                 </q-tabs>
             </div>
             </div>
 
 
             <div class="col fit">
             <div class="col fit">
                 <!-- Профили --------------------------------------------------------------------->
                 <!-- Профили --------------------------------------------------------------------->
-                <div v-if="selectedTab == 'profiles'" class="fit tab-panel">
-                    @@include('./ProfilesTab.inc');
-                </div>
+                <ProfilesTab v-if="selectedTab == 'profiles'" :form="form" />
                 <!-- Вид ------------------------------------------------------------------------->                    
                 <!-- Вид ------------------------------------------------------------------------->                    
-                <div v-if="selectedTab == 'view'" class="fit column">
-                    <q-tabs
-                        v-model="selectedViewTab"
-                        active-color="black"
-                        active-bg-color="white"
-                        indicator-color="white"
-                        dense
-                        no-caps
-                        class="no-mp bg-grey-4 text-grey-7"
-                    >
-                        <q-tab name="mode" label="Режим" />
-                        <q-tab name="color" label="Цвет" />
-                        <q-tab name="font" label="Шрифт" />
-                        <q-tab name="text" label="Текст" />
-                        <q-tab name="status" label="Строка статуса" />
-                    </q-tabs>
-
-                    <div class="q-mb-sm" />
-
-                    <div class="col tab-panel">
-                        <div v-if="selectedViewTab == 'mode'">
-                            @@include('./ViewTab/Mode.inc');
-                        </div>
-
-                        <div v-if="selectedViewTab == 'color'">
-                            @@include('./ViewTab/Color.inc');
-                        </div>
-
-                        <div v-if="selectedViewTab == 'font'">
-                            @@include('./ViewTab/Font.inc');
-                        </div>
-
-                        <div v-if="selectedViewTab == 'text'">
-                            @@include('./ViewTab/Text.inc');
-                        </div>
-
-                        <div v-if="selectedViewTab == 'status'">
-                            @@include('./ViewTab/Status.inc');
-                        </div>
-                    </div>
-                </div>
+                <ViewTab v-if="selectedTab == 'view'" :form="form" />
                 <!-- Кнопки ---------------------------------------------------------------------->
                 <!-- Кнопки ---------------------------------------------------------------------->
-                <div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
-                    @@include('./ToolBarTab.inc');
-                </div>
+                <ToolBarTab v-if="selectedTab == 'toolbar'" :form="form" />
                 <!-- Управление ------------------------------------------------------------------>
                 <!-- Управление ------------------------------------------------------------------>
-                <div v-if="selectedTab == 'keys'" class="fit column">
-                    @@include('./KeysTab.inc');
-                </div>
+                <KeysTab v-if="selectedTab == 'keys'" :form="form" />
                 <!-- Листание -------------------------------------------------------------------->
                 <!-- Листание -------------------------------------------------------------------->
-                <div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
-                    @@include('./PageMoveTab.inc');
-                </div>
+                <PageMoveTab v-if="selectedTab == 'pagemove'" :form="form" />
                 <!-- Конвертирование ------------------------------------------------------------->
                 <!-- Конвертирование ------------------------------------------------------------->
-                <div v-if="selectedTab == 'convert'" class="fit tab-panel">
-                    @@include('./ConvertTab.inc');
-                </div>
+                <ConvertTab v-if="selectedTab == 'convert'" :form="form" />
                 <!-- Обновление ------------------------------------------------------------------>
                 <!-- Обновление ------------------------------------------------------------------>
-                <div v-if="selectedTab == 'update'" class="fit tab-panel">
-                    @@include('./UpdateTab.inc');
-                </div>
+                <UpdateTab v-if="selectedTab == 'update'" :form="form" />
                 <!-- Прочее ---------------------------------------------------------------------->
                 <!-- Прочее ---------------------------------------------------------------------->
-                <div v-if="selectedTab == 'others'" class="fit tab-panel">
-                    @@include('./OthersTab.inc');
-                </div>
-                <!-- Сброс ----------------------------------------------------------------------->
-                <div v-if="selectedTab == 'reset'" class="fit tab-panel">
-                    @@include('./ResetTab.inc');
-                </div>
+                <OthersTab v-if="selectedTab == 'others'" :form="form" />
+                <!-- Сброс ----------------------------------------------------------------------->                
+                <ResetTab v-if="selectedTab == 'reset'" :form="form" @tab-event="tabEvent" />
             </div>
             </div>
         </div>
         </div>
     </Window>
     </Window>
@@ -119,152 +57,86 @@
 
 
 <script>
 <script>
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
-import { ref, watch } from 'vue';
 import vueComponent from '../../vueComponent.js';
 import vueComponent from '../../vueComponent.js';
+import { reactive } from 'vue';
 
 
 import _ from 'lodash';
 import _ from 'lodash';
 
 
-import * as utils from '../../../share/utils';
-import * as cryptoUtils from '../../../share/cryptoUtils';
+//stuff
 import Window from '../../share/Window.vue';
 import Window from '../../share/Window.vue';
-import NumInput from '../../share/NumInput.vue';
-import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
-import wallpaperStorage from '../share/wallpaperStorage';
 
 
-import readerApi from '../../../api/reader';
 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})?$/;
+//pages
+import ProfilesTab from './ProfilesTab/ProfilesTab.vue';
+import ViewTab from './ViewTab/ViewTab.vue';
+import ToolBarTab from './ToolBarTab/ToolBarTab.vue';
+import KeysTab from './KeysTab/KeysTab.vue';
+import PageMoveTab from './PageMoveTab/PageMoveTab.vue';
+import ConvertTab from './ConvertTab/ConvertTab.vue';
+import UpdateTab from './UpdateTab/UpdateTab.vue';
+import OthersTab from './OthersTab/OthersTab.vue';
+import ResetTab from './ResetTab/ResetTab.vue';
 
 
 const componentOptions = {
 const componentOptions = {
     components: {
     components: {
         Window,
         Window,
-        NumInput,
-        UserHotKeys,
-    },
-    data: function() {
-        return Object.assign({}, rstore.settingDefaults);
+        //pages
+        ProfilesTab,
+        ViewTab,
+        ToolBarTab,
+        KeysTab,
+        PageMoveTab,
+        ConvertTab,
+        UpdateTab,
+        OthersTab,
+        ResetTab,
     },
     },
     watch: {
     watch: {
         settings: function() {
         settings: function() {
-            this.settingsChanged();
-        },
-        form: function(newValue) {
-            if (this.inited) {
-                this.commit('reader/setSettings', _.cloneDeep(newValue));
-            }
-        },
-        fontBold: function(newValue) {
-            this.fontWeight = (newValue ? 'bold' : '');
-        },
-        fontItalic: function(newValue) {
-            this.fontStyle = (newValue ? 'italic' : '');
-        },
-        vertShift: function(newValue) {
-            const font = (this.webFontName ? this.webFontName : this.fontName);
-            if (this.fontShifts[font] != newValue || this.fontVertShift != newValue) {
-                this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
-                this.fontVertShift = newValue;
-            }
-        },
-        fontName: function(newValue) {
-            const font = (this.webFontName ? this.webFontName : newValue);
-            this.vertShift = this.fontShifts[font] || 0;
-        },
-        webFontName: function(newValue) {
-            const font = (newValue ? newValue : this.fontName);
-            this.vertShift = this.fontShifts[font] || 0;
-        },
-        wallpaper: function(newValue) {
-            if (newValue != '' && this.pageChangeAnimation == 'flip')
-                this.pageChangeAnimation = '';
-        },
-        dualPageMode(newValue) {
-            if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
-                this.pageChangeAnimation = '';
-        },
-        textColor: function(newValue) {
-            this.textColorFiltered = newValue;
-        },
-        textColorFiltered: function(newValue) {
-            if (hex.test(newValue))
-                this.textColor = newValue;
+            this.settingsChanged();//no await
         },
         },
-        backgroundColor: function(newValue) {
-            this.bgColorFiltered = newValue;
-        },
-        bgColorFiltered: function(newValue) {
-            if (hex.test(newValue))
-                this.backgroundColor = newValue;
-        },
-        dualDivColor(newValue) {
-            this.dualDivColorFiltered = newValue;
-        },
-        dualDivColorFiltered(newValue) {
-            if (hex.test(newValue))
-                this.dualDivColor = newValue;
-        },
-        statusBarColor(newValue) {
-            this.statusBarColorFiltered = newValue;
-        },
-        statusBarColorFiltered(newValue) {
-            if (hex.test(newValue))
-                this.statusBarColor = newValue;
+        form: {
+            handler() {
+                if (this.inited && !this.isSetsChanged) {
+                    this.debouncedCommitSettings();
+                }
+            },
+            deep: true,
         },
         },
     },
     },
 };
 };
 class SettingsPage {
 class SettingsPage {
     _options = componentOptions;
     _options = componentOptions;
 
 
+    form = {};
+
+    tabs = [
+        { name: 'profiles', icon: 'la la-users', label: 'Профили' },
+        { name: 'view', icon: 'la la-eye', label: 'Вид'},
+        { name: 'toolbar', icon: 'la la-grip-horizontal', label: 'Панель'},
+        { name: 'keys', icon: 'la la-gamepad', label: 'Управление'},
+        { name: 'pagemove', icon: 'la la-school', label: 'Листание'},
+        { name: 'convert', icon: 'la la-magic', label: 'Конвертир.'},
+        { name: 'update', icon: 'la la-retweet', label: 'Обновление'},
+        { name: 'others', icon: 'la la-list-ul', label: 'Прочее'},
+        { name: 'reset', icon: 'la la-broom', label: 'Сброс'},
+    ];
     selectedTab = 'profiles';
     selectedTab = 'profiles';
-    selectedViewTab = 'mode';
-    selectedKeysTab = 'mouse';
-    fontBold = false;
-    fontItalic = false;
-    vertShift = 0;
-    tabsScrollable = false;
-    textColorFiltered = '';
-    bgColorFiltered = '';
-    dualDivColorFiltered = '';
-
-    webFonts = [];
-    fonts = [];
 
 
-    serverStorageKeyVisible = false;
-    toolButtons = [];
-    rstore = {};
-
-    setup() {
-        const settingsProps = { form: ref({}) };
-
-        for (let prop in rstore.settingDefaults) {
-            settingsProps[prop] = ref(_.cloneDeep(rstore.settingDefaults[prop]));
-            watch(settingsProps[prop], (newValue) => {
-                settingsProps.form.value = Object.assign({}, settingsProps.form.value, {[prop]: newValue});
-            }, {deep: true});
-        }
-
-        return settingsProps;
-    }
+    isSetsChanged = false;
 
 
     created() {
     created() {
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
-        this.reader = this.$store.state.reader;
 
 
-        this.form = {};
-        this.rstore = rstore;
-        this.toolButtons = rstore.toolButtons;
-        this.settingsChanged();
+        this.debouncedCommitSettings = _.debounce(() => {            
+            this.commit('reader/setSettings', _.cloneDeep(this.form));
+        }, 50);
+
+        this.settingsChanged();//no await
     }
     }
 
 
     mounted() {
     mounted() {
-        this.$watch(
-            '$refs.tabs.scrollable',
-            (newValue) => {
-                this.tabsScrollable = newValue && !this.$root.isMobileDevice;
-            }
-        );
     }
     }
 
 
     init() {
     init() {
@@ -272,194 +144,20 @@ class SettingsPage {
         this.inited = true;
         this.inited = true;
     }
     }
 
 
-    settingsChanged() {
-        if (_.isEqual(this.form, this.settings))
-            return;
-
-        this.form = Object.assign({}, this.settings);
-        for (const prop in rstore.settingDefaults) {
-            this[prop] = _.cloneDeep(this.form[prop]);
+    async settingsChanged() {
+        this.isSetsChanged = true;
+        try {
+            this.form = reactive(_.cloneDeep(this.settings));
+        } finally {
+            await this.$nextTick();
+            this.isSetsChanged = false;
         }
         }
-
-        this.fontBold = (this.fontWeight == 'bold');
-        this.fontItalic = (this.fontStyle == 'italic');
-
-        this.fonts = rstore.fonts;
-        this.webFonts = rstore.webFonts;
-        const font = (this.webFontName ? this.webFontName : this.fontName);
-        this.vertShift = this.fontShifts[font] || 0;
-        this.textColorFiltered = this.textColor;
-        this.bgColorFiltered = this.backgroundColor;
-        this.dualDivColorFiltered = this.dualDivColor;
-        this.statusBarColorFiltered = this.statusBarColor;
-    }
-
-    get mode() {
-        return this.$store.state.config.mode;
-    }
-
-    get isExternalConverter() {
-        return this.$store.state.config.useExternalBookConverter;
     }
     }
 
 
     get settings() {
     get settings() {
         return this.$store.state.reader.settings;
         return this.$store.state.reader.settings;
     }
     }
 
 
-    get serverSyncEnabled() {
-        return this.$store.state.reader.serverSyncEnabled;
-    }
-
-    set serverSyncEnabled(newValue) {
-        this.commit('reader/setServerSyncEnabled', newValue);
-    }
-
-    get profiles() {
-        return this.$store.state.reader.profiles;
-    }
-
-    get configBucEnabled() {
-        return this.$store.state.config.bucEnabled;
-    }
-
-    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: ''}];
-
-        const userWallpapers = _.cloneDeep(this.userWallpapers);
-        userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
-
-        for (const wp of userWallpapers) {
-            if (wallpaperStorage.keyExists(wp.cssClass))
-                result.push({label: wp.label, value: wp.cssClass});
-        }
-
-        for (let i = 1; i <= 17; 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'},
-            (!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
-            {label: 'Протаивание', value: 'thaw'},
-            {label: 'Мерцание', value: 'blink'},
-            {label: 'Вращение', value: 'rotate'},
-            (this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
-        ];        
-
-        result = result.filter(v => v);
-
-        return result;
-    }
-
-    get currentProfile() {
-        return this.$store.state.reader.currentProfile;
-    }
-
-    set currentProfile(newValue) {
-        this.commit('reader/setCurrentProfile', newValue);
-    }
-
-    get partialStorageKey() {
-        return this.serverStorageKey.substr(0, 7) + '***';
-    }
-
-    get serverStorageKey() {
-        return this.$store.state.reader.serverStorageKey;
-    }
-
-    get setStorageKeyLink() {
-        return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
-    }
-
-    get predefineTextColors() {
-        return defPalette.concat([
-          '#ffffff',
-          '#000000',
-          '#202020',
-          '#323232',
-          '#aaaaaa',
-          '#00c0c0',
-          '#ebe2c9',
-          '#cfdc99',
-          '#478355',
-          '#909080',
-        ]);
-    }
-
-    get predefineBackgroundColors() {
-        return defPalette.concat([
-          '#ffffff',
-          '#000000',
-          '#202020',
-          '#ebe2c9',
-          '#cfdc99',
-          '#478355',
-          '#a6caf0',
-          '#909080',
-          '#808080',
-          '#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;
-            case 'div':
-                result += `background-color: ${this.dualDivColor};`
-                break;
-            case 'statusbar':
-                result += `background-color: ${this.statusBarColor};`
-                break;
-        }
-        return result;
-    }
-
-    needReload() {
-        this.$root.notify.warning('Необходимо обновить страницу (F5), чтобы изменения возымели эффект');
-    }
-
-    needTextReload() {
-        this.$root.notify.warning('Необходимо обновить книгу в обход кэша, чтобы изменения возымели эффект');
-    }
-
     close() {
     close() {
         this.$emit('do-action', {action: 'settings'});
         this.$emit('do-action', {action: 'settings'});
     }
     }
@@ -467,242 +165,19 @@ class SettingsPage {
     async setDefaults() {
     async setDefaults() {
         try {
         try {
             if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) {
             if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) {
-                this.form = Object.assign({}, rstore.settingDefaults);
-                for (let prop in rstore.settingDefaults) {
-                    this[prop] = this.form[prop];
-                }
-            }
-        } catch (e) {
-            //
-        }
-    }
-
-    async addProfile() {
-        try {
-            if (Object.keys(this.profiles).length >= 100) {
-                this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
-                return;
-            }
-            const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
-                inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
-            });
-            if (result && result.value) {
-                if (this.profiles[result.value]) {
-                    this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
-                } else {
-                    const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
-                    this.commit('reader/setAllowProfilesSave', true);
-                    await this.$nextTick();//ждем обработчики watch
-                    this.commit('reader/setProfiles', newProfiles);
-                    await this.$nextTick();//ждем обработчики watch
-                    this.commit('reader/setAllowProfilesSave', false);
-                    this.currentProfile = result.value;
-                }
-            }
-        } catch (e) {
-            //
-        }
-    }
-
-    async delProfile() {
-        if (!this.currentProfile)
-            return;
-
-        try {
-            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
-                    `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
-                    `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
-                inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
-            });
-
-            if (result && result.value && result.value.toLowerCase() == 'да') {
-                if (this.profiles[this.currentProfile]) {
-                    const newProfiles = Object.assign({}, this.profiles);
-                    delete newProfiles[this.currentProfile];
-                    this.commit('reader/setAllowProfilesSave', true);
-                    await this.$nextTick();//ждем обработчики watch
-                    this.commit('reader/setProfiles', newProfiles);
-                    await this.$nextTick();//ждем обработчики watch
-                    this.commit('reader/setAllowProfilesSave', false);
-                    this.currentProfile = '';
-                }
+                this.form = _.cloneDeep(rstore.settingDefaults);
             }
             }
         } catch (e) {
         } catch (e) {
             //
             //
         }
         }
     }
     }
 
 
-    async delAllProfiles() {
-        if (!Object.keys(this.profiles).length)
+    tabEvent(event) {
+        if (!event || !event.action)
             return;
             return;
 
 
-        try {
-            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
-                    `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
-                inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
-            });
-
-            if (result && result.value && result.value.toLowerCase() == 'да') {
-                this.commit('reader/setAllowProfilesSave', true);
-                await this.$nextTick();//ждем обработчики watch
-                this.commit('reader/setProfiles', {});
-                await this.$nextTick();//ждем обработчики watch
-                this.commit('reader/setAllowProfilesSave', false);
-                this.currentProfile = '';
-            }
-        } catch (e) {
-            //
-        }
-    }
-
-    async copyToClip(text, prefix) {
-        const result = await utils.copyTextToClipboard(text);
-        const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
-        const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
-        if (result)
-            this.$root.notify.success(msg);
-        else
-            this.$root.notify.error(msg);
-    }
-
-    async showServerStorageKey() {
-        this.serverStorageKeyVisible = !this.serverStorageKeyVisible;
-    }
-
-    async enterServerStorageKey(key) {
-        try {
-            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),
-            });
-
-            if (result && result.value && utils.fromBase58(result.value).length == 32) {
-                this.commit('reader/setServerStorageKey', result.value);
-            }
-        } catch (e) {
-            //
-        }
-    }
-
-    async generateServerStorageKey() {
-        try {
-            const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
-                    `<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
-                inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
-            });
-
-            if (result && result.value && result.value.toLowerCase() == 'да') {
-                if (this.$root.generateNewServerStorageKey)
-                    this.$root.generateNewServerStorageKey();
-            }
-        } catch (e) {
-            //
-        }
-
-    }
-
-    loadWallpaperFileClick() {
-        this.$refs.file.click();
-    }
-
-    loadWallpaperFile() {
-        const file = this.$refs.file.files[0];        
-        if (file.size > 10*1024*1024) {
-            this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
-            return;
-        }
-
-        if (file.type != 'image/png' && file.type != 'image/jpeg') {
-            this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
-            return;
-        }
-
-        if (this.userWallpapers.length >= 100) {
-            this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
-            return;
-        }
-
-        this.$refs.file.value = '';
-        if (file) {
-            const reader = new FileReader();
-
-            reader.onload = (e) => {
-                (async() => {
-                    const data = e.target.result;
-                    const key = utils.toHex(cryptoUtils.sha256(data));
-                    const label = `#${key.substring(0, 4)}`;
-                    const cssClass = `user-paper${key}`;
-
-                    const newUserWallpapers = _.cloneDeep(this.userWallpapers);
-                    const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
-
-                    if (index < 0)
-                        newUserWallpapers.push({label, cssClass});
-                    if (!wallpaperStorage.keyExists(cssClass)) {
-                        await wallpaperStorage.setData(cssClass, data);
-                        //отправим data на сервер в файл `/upload/${key}`
-                        try {
-                            //const res = 
-                            await readerApi.uploadFileBuf(data);
-                            //console.log(res);
-                        } catch (e) {
-                            console.error(e);
-                        }
-                    }
-
-                    this.userWallpapers = newUserWallpapers;
-                    this.wallpaper = cssClass;
-                })();
-            }
-
-            reader.readAsDataURL(file);
-        }
-    }
-
-    async delWallpaper() {
-        if (this.wallpaper.indexOf('user-paper') == 0) {
-            const newUserWallpapers = [];
-            for (const wp of this.userWallpapers) {
-                if (wp.cssClass != this.wallpaper) {
-                    newUserWallpapers.push(wp);
-                }
-            }
-
-            await wallpaperStorage.removeData(this.wallpaper);
-
-            this.userWallpapers = newUserWallpapers;
-            this.wallpaper = '';
-        }
-    }
-
-    async downloadWallpaper() {
-        if (this.wallpaper.indexOf('user-paper') != 0)
-            return;
-
-        try {
-            const d = this.$refs.download;
-
-            const dataUrl = await wallpaperStorage.getData(this.wallpaper);
-
-            if (!dataUrl)
-                throw new Error('Файл обоев не найден');
-
-            d.href = dataUrl;
-            d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
-
-            d.click();
-        } catch (e) {
-            this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
+        switch (event.action) {
+            case 'set-defaults': this.setDefaults(); break;
         }
         }
     }
     }
 
 
@@ -722,15 +197,17 @@ export default vueComponent(SettingsPage);
 .tab {
 .tab {
     justify-content: initial;
     justify-content: initial;
 }
 }
+</style>
 
 
-.tab-panel {
+<style>
+.sets-tab-panel {
     overflow-x: hidden;
     overflow-x: hidden;
     overflow-y: auto;
     overflow-y: auto;
     font-size: 90%;
     font-size: 90%;
     padding: 0 10px 15px 10px;
     padding: 0 10px 15px 10px;
 }
 }
 
 
-.part-header {
+.sets-part-header {
     border-top: 2px solid #bbbbbb;
     border-top: 2px solid #bbbbbb;
     font-weight: bold;
     font-weight: bold;
     font-size: 110%;
     font-size: 110%;
@@ -738,25 +215,7 @@ export default vueComponent(SettingsPage);
     margin-bottom: 5px;
     margin-bottom: 5px;
 }
 }
 
 
-.item {
-    width: 100%;
-    margin-top: 5px;
-    margin-bottom: 5px;
-}
-
-.label-1, .label-3, .label-7 {
-    width: 75px;
-}
-
-.label-2, .label-4, .label-5 {
-    width: 110px;
-}
-
-.label-6 {
-    width: 100px;
-}
-
-.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
+.sets-label {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     justify-content: center;
     justify-content: center;
@@ -765,33 +224,14 @@ export default vueComponent(SettingsPage);
     overflow: hidden;
     overflow: hidden;
 }
 }
 
 
-.text {
-    font-size: 90%;
-    line-height: 130%;
+.sets-item {
+    width: 100%;
+    margin-top: 5px;
+    margin-bottom: 5px;
 }
 }
 
 
-.button {
+.sets-button {
     margin: 3px 15px 3px 0;
     margin: 3px 15px 3px 0;
     padding: 0 5px 0 5px;
     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;
-}
-
-.col-left {
-    width: 150px;
-}
 </style>
 </style>

+ 0 - 18
client/components/Reader/SettingsPage/ToolBarTab.inc

@@ -1,18 +0,0 @@
-<div class="part-header">Отображение</div>
-
-<div class="item row no-wrap">
-    <div class="label-3"></div>
-    <q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
-        <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 no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
-    <div class="label-3"></div>
-        <q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
-        />
-</div>

+ 76 - 0
client/components/Reader/SettingsPage/ToolBarTab/ToolBarTab.vue

@@ -0,0 +1,76 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <div class="sets-part-header">
+            Отображение
+        </div>
+
+        <div class="item row no-wrap">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="form.toolBarMultiLine" size="xs" label="Многострочная панель">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Размещать кнопки на панели в несколько рядов, если они не помещаются в одну строку
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div class="item row no-wrap">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="form.toolBarHideOnScroll" size="xs" label="Скрывать/показывать панель при прокрутке">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Скрывать/показывть панель при прокрутке текста вперед/назад
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div class="sets-part-header">
+            Показывать кнопки
+        </div>
+
+        <div v-for="item in rstore.toolButtons" :key="item.name">
+            <div class="sets-item row no-wrap">
+                <div class="sets-label label"></div>
+                <q-checkbox v-model="form.showToolButton[item.name]" size="xs" :label="rstore.readerActions[item.name]" />
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+
+import rstore from '../../../../store/modules/reader';
+
+const componentOptions = {
+    watch: {
+    },
+};
+class ToolBarTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    rstore = rstore;
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+    get mode() {
+        return this.$store.state.config.mode;
+    }
+}
+
+export default vueComponent(ToolBarTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 75px;
+}
+
+</style>

+ 0 - 76
client/components/Reader/SettingsPage/UpdateTab.inc

@@ -1,76 +0,0 @@
-<!---------------------------------------------->
-<div class="part-header">Обновление читалки</div>
-<div class="item row">
-    <div class="label-6"></div>
-    <q-checkbox size="xs" v-model="showNeedUpdateNotify">
-        Проверять наличие новой версии
-        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-            Напоминать о необходимости обновления страницы<br>
-            при появлении новой версии читалки
-        </q-tooltip>
-    </q-checkbox>
-</div>
-
-<!---------------------------------------------->
-<div class="part-header">Обновление книг</div>
-<div v-show="!configBucEnabled" class="item row">
-    <div class="label-6"></div>
-    <div>Сервер обновлений временно не работает</div>
-</div>
-
-<div v-show="configBucEnabled" class="item row">
-    <div class="label-6"></div>
-    <q-checkbox size="xs" v-model="bucEnabled">
-        Проверять обновления книг
-    </q-checkbox>
-</div>
-
-<div v-show="configBucEnabled && bucEnabled" class="item row">
-    <div class="label-6"></div>
-    <div class="col-5 column justify-center items-end q-pr-xs">Разница размеров</div>
-    <div class="col row">
-        <NumInput class="col-left" v-model="bucSizeDiff" />
-
-        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-            Уведомлять о наличии обновления книги в списке загруженных<br>
-            при указанной разнице в размерах старого и нового файлов.<br>
-            Разница указывается в байтах и может быть отрицательной.
-        </q-tooltip>
-    </div>
-</div>
-
-<div v-show="configBucEnabled && bucEnabled" class="item row">
-    <div class="label-6"></div>
-    <q-checkbox size="xs" v-model="bucSetOnNew">
-        Автопроверка для вновь загружаемых
-        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-            Автоматически устанавливать флаг проверки<br>
-            обновлений для всех вновь загружаемых книг
-        </q-tooltip>
-    </q-checkbox>
-</div>
-
-<div v-show="configBucEnabled && bucEnabled" class="item row">
-    <div class="label-6"></div>
-    <q-checkbox size="xs" v-model="bucCancelEnabled">
-        Отменять проверку через {{ bucCancelDays }} дней{{ (bucCancelEnabled ? ':' : '') }}
-        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-            Снимать флаг проверки с книги, если не было<br>
-            обновлений в течение {{ bucCancelDays }} дней
-        </q-tooltip>
-    </q-checkbox>
-</div>
-
-<div v-show="configBucEnabled && bucEnabled && bucCancelEnabled" class="item row">
-    <div class="label-6"></div>
-    <div class="col-5"></div>
-    <div class="col row">
-        <NumInput class="col-left" v-model="bucCancelDays" :min="1" :max="10000"/>
-
-        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-            Снимать флаг проверки с книги, если не было<br>
-            обновлений в течение {{ bucCancelDays }} дней
-        </q-tooltip>
-    </div>
-</div>
-

+ 122 - 0
client/components/Reader/SettingsPage/UpdateTab/UpdateTab.vue

@@ -0,0 +1,122 @@
+<template>
+    <div class="fit sets-tab-panel">
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            Обновление читалки
+        </div>
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="form.showNeedUpdateNotify" size="xs">
+                Проверять наличие новой версии
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Напоминать о необходимости обновления страницы<br>
+                    при появлении новой версии читалки
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <!---------------------------------------------->
+        <div class="sets-part-header">
+            Обновление книг
+        </div>
+        <div v-show="!configBucEnabled" class="sets-item row">
+            <div class="sets-label label"></div>
+            <div>Сервер обновлений временно не работает</div>
+        </div>
+
+        <div v-show="configBucEnabled" class="sets-item row">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="form.bucEnabled" size="xs">
+                Проверять обновления книг
+            </q-checkbox>
+        </div>
+
+        <div v-show="configBucEnabled && form.bucEnabled" class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col-4 column justify-center items-end q-pr-xs">
+                Разница размеров
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.bucSizeDiff" style="width: 200px" />
+
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Уведомлять о наличии обновления книги в списке загруженных<br>
+                    при указанной разнице в размерах старого и нового файлов.<br>
+                    Разница указывается в байтах и может быть отрицательной.
+                </q-tooltip>
+            </div>
+        </div>
+
+        <div v-show="configBucEnabled && form.bucEnabled" class="sets-item row">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="form.bucSetOnNew" size="xs">
+                Автопроверка для вновь загружаемых
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Автоматически устанавливать флаг проверки<br>
+                    обновлений для всех вновь загружаемых книг
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div v-show="configBucEnabled && form.bucEnabled" class="sets-item row">
+            <div class="sets-label label"></div>
+            <q-checkbox v-model="form.bucCancelEnabled" size="xs">
+                Отменять проверку через {{ form.bucCancelDays }} дней{{ (form.bucCancelEnabled ? ':' : '') }}
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Снимать флаг проверки с книги, если не было<br>
+                    обновлений в течение {{ form.bucCancelDays }} дней
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+
+        <div v-show="configBucEnabled && form.bucEnabled && form.bucCancelEnabled" class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col-4"></div>
+            <div class="col row">
+                <NumInput v-model="form.bucCancelDays" :min="1" :max="10000" />
+
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Снимать флаг проверки с книги, если не было<br>
+                    обновлений в течение {{ form.bucCancelDays }} дней
+                </q-tooltip>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+import NumInput from '../../../share/NumInput.vue';
+
+const componentOptions = {
+    components: {
+        NumInput
+    },
+};
+class UpdateTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+    get configBucEnabled() {
+        return this.$store.state.config.bucEnabled;
+    }
+}
+
+export default vueComponent(UpdateTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 100px;
+}
+</style>

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

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

+ 329 - 0
client/components/Reader/SettingsPage/ViewTab/Color/Color.vue

@@ -0,0 +1,329 @@
+<template>
+    <div>
+        <!---------------------------------------------->
+        <div class="hidden sets-part-header">
+            Цвет
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Текст
+            </div>
+            <div class="col row">
+                <q-input
+                    v-model="textColorFiltered"
+                    class="col-left no-mp"
+                    outlined dense
+                    
+                    :rules="['hexColor']"
+                    style="max-width: 150px"
+                >
+                    <template #prepend>
+                        <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.textColor)">
+                            <q-popup-proxy anchor="bottom middle" self="top middle">
+                                <div>
+                                    <q-color
+                                        v-model="form.textColor"
+                                        no-header default-view="palette" :palette="defPalette.predefineTextColors"
+                                    />
+                                </div>
+                            </q-popup-proxy>
+                        </q-icon>
+                    </template>
+                </q-input>
+            </div>
+        </div>
+
+        <div class="q-mt-md" />
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Фон
+            </div>
+            <div class="col row">
+                <q-input 
+                    v-model="bgColorFiltered"
+                    class="col-left no-mp"
+                    outlined dense
+                    
+                    :rules="['hexColor']"
+                    style="max-width: 150px"
+                >
+                    <template #prepend>
+                        <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.backgroundColor)">
+                            <q-popup-proxy anchor="bottom middle" self="top middle">
+                                <div>
+                                    <q-color v-model="form.backgroundColor" no-header default-view="palette" :palette="defPalette.predefineBackgroundColors" />
+                                </div>
+                            </q-popup-proxy>
+                        </q-icon>
+                    </template>
+                </q-input>
+            </div>
+        </div>
+
+        <div class="q-mt-md" />
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Обои
+            </div>
+            <div class="col row items-center">
+                <q-select 
+                    v-model="form.wallpaper"
+                    class="col-left no-mp"
+                    :options="wallpaperOptions"
+                    dropdown-icon="la la-angle-down la-sm"
+                    outlined dense emit-value map-options
+                >
+                    <template #selected-item="scope">
+                        <div>
+                            {{ scope.opt.label }}
+                        </div>
+                        <div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
+                    </template>
+
+                    <template #option="scope">
+                        <q-item
+                            v-bind="scope.itemProps"
+                        >
+                            <q-item-section style="min-width: 50px;">
+                                <q-item-label>
+                                    {{ scope.opt.label }}
+                                </q-item-label>
+                            </q-item-section>
+                            <q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
+                        </q-item>
+                    </template>
+                </q-select>
+
+                <div class="q-px-xs" />
+                <q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        Добавить файл обоев
+                    </q-tooltip>
+                </q-btn>
+                <q-btn v-show="form.wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        Удалить выбранные обои
+                    </q-tooltip>
+                </q-btn>
+                <q-btn v-show="form.wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
+                    <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
+                        Скачать выбранные обои
+                    </q-tooltip>
+                </q-btn>
+            </div>
+        </div>
+
+        <div class="q-mt-sm" />
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col row items-center">
+                <q-checkbox v-model="form.wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
+            </div>
+        </div>
+
+        <input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />
+        <a ref="download" style="display: none;" target="_blank"></a>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../../vueComponent.js';
+
+import _ from 'lodash';
+
+import * as helper from '../helper';
+import defPalette from '../defPalette';
+
+import * as utils from '../../../../../share/utils';
+import * as cryptoUtils from '../../../../../share/cryptoUtils';
+import wallpaperStorage from '../../../share/wallpaperStorage';
+import readerApi from '../../../../../api/reader';
+
+const componentOptions = {
+    components: {
+    },
+    watch: {
+        form: {
+            handler() {
+                this.formChanged();//no await
+            },
+            deep: true,
+        },
+        textColorFiltered(newValue) {
+            if (!this.isFormChanged && this.helper.isHexColor(newValue))
+                this.form.textColor = newValue;
+        },
+        bgColorFiltered(newValue) {
+            if (!this.isFormChanged && this.helper.isHexColor(newValue))
+                this.form.backgroundColor = newValue;
+        },
+    },
+};
+class Color {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    helper = helper;
+    defPalette = defPalette;
+
+    isFormChanged = false;
+    textColorFiltered = '';
+    bgColorFiltered = '';
+
+    created() {
+        this.formChanged();//no await
+    }
+
+    mounted() {
+    }
+
+    async formChanged() {
+        this.isFormChanged = true;
+        try {
+            this.textColorFiltered = this.form.textColor;
+            this.bgColorFiltered = this.form.backgroundColor;
+
+            if (this.form.wallpaper != '' && this.form.pageChangeAnimation == 'flip')
+                this.form.pageChangeAnimation = '';
+        } finally {
+            await this.$nextTick();
+            this.isFormChanged = false;
+        }
+    }
+
+    get wallpaperOptions() {
+        let result = [{label: 'Нет', value: ''}];
+
+        const userWallpapers = _.cloneDeep(this.form.userWallpapers);
+        userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
+
+        for (const wp of userWallpapers) {
+            if (wallpaperStorage.keyExists(wp.cssClass))
+                result.push({label: wp.label, value: wp.cssClass});
+        }
+
+        for (let i = 1; i <= 17; i++) {
+            result.push({label: i, value: `paper${i}`});
+        }
+
+        return result;
+    }
+
+    loadWallpaperFileClick() {
+        this.$refs.file.click();
+    }
+
+    loadWallpaperFile() {
+        const file = this.$refs.file.files[0];        
+        if (file.size > 10*1024*1024) {
+            this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
+            return;
+        }
+
+        if (file.type != 'image/png' && file.type != 'image/jpeg') {
+            this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
+            return;
+        }
+
+        if (this.form.userWallpapers.length >= 100) {
+            this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
+            return;
+        }
+
+        this.$refs.file.value = '';
+        if (file) {
+            const reader = new FileReader();
+
+            reader.onload = (e) => {
+                (async() => {
+                    const data = e.target.result;
+                    const key = utils.toHex(cryptoUtils.sha256(data));
+                    const label = `#${key.substring(0, 4)}`;
+                    const cssClass = `user-paper${key}`;
+
+                    const newUserWallpapers = _.cloneDeep(this.form.userWallpapers);
+                    const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
+
+                    if (index < 0)
+                        newUserWallpapers.push({label, cssClass});
+                    if (!wallpaperStorage.keyExists(cssClass)) {
+                        await wallpaperStorage.setData(cssClass, data);
+                        //отправим data на сервер в файл `/upload/${key}`
+                        try {
+                            //const res = 
+                            await readerApi.uploadFileBuf(data);
+                            //console.log(res);
+                        } catch (e) {
+                            console.error(e);
+                        }
+                    }
+
+                    this.form.userWallpapers = newUserWallpapers;
+                    this.form.wallpaper = cssClass;
+                })();
+            }
+
+            reader.readAsDataURL(file);
+        }
+    }
+
+    async delWallpaper() {
+        if (this.form.wallpaper.indexOf('user-paper') == 0) {
+            const newUserWallpapers = [];
+            for (const wp of this.form.userWallpapers) {
+                if (wp.cssClass != this.form.wallpaper) {
+                    newUserWallpapers.push(wp);
+                }
+            }
+
+            await wallpaperStorage.removeData(this.form.wallpaper);
+
+            this.form.userWallpapers = newUserWallpapers;
+            this.form.wallpaper = '';
+        }
+    }
+
+    async downloadWallpaper() {
+        if (this.form.wallpaper.indexOf('user-paper') != 0)
+            return;
+
+        try {
+            const d = this.$refs.download;
+
+            const dataUrl = await wallpaperStorage.getData(this.form.wallpaper);
+
+            if (!dataUrl)
+                throw new Error('Файл обоев не найден');
+
+            d.href = dataUrl;
+            d.download = `wallpaper-#${this.form.wallpaper.replace('user-paper', '').substring(0, 4)}`;
+
+            d.click();
+        } catch (e) {
+            this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
+        }
+    }
+}
+
+export default vueComponent(Color);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+.col-left {
+    width: 145px;
+}
+
+.no-mp {
+    margin: 0;
+    padding: 0;
+}
+</style>

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

@@ -1,56 +0,0 @@
-<!---------------------------------------------->
-<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>

+ 176 - 0
client/components/Reader/SettingsPage/ViewTab/Font/Font.vue

@@ -0,0 +1,176 @@
+<template>
+    <div>
+        <!---------------------------------------------->
+        <div class="hidden sets-part-header">
+            Шрифт
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Локальный/веб
+            </div>
+            <div class="col row">
+                <q-select
+                    v-model="form.fontName" class="col-left" :options="fontsOptions" :disable="form.webFontName != ''"
+                    dropdown-icon="la la-angle-down la-sm"
+                    outlined dense emit-value map-options
+                />
+
+                <div class="q-px-sm" />
+                <q-select
+                    v-model="form.webFontName" class="col" :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="sets-item row">
+            <div class="sets-label label">
+                Размер
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.fontSize" class="col-left" :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="sets-item row">
+            <div class="sets-label label">
+                Сдвиг
+            </div>
+            <div class="col row">
+                <NumInput v-model="vertShift" class="col-left" :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="sets-item row">
+            <div class="sets-label label">
+                Стиль
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="fontBold" size="xs" label="Жирный" />
+                <q-checkbox v-model="fontItalic" class="q-ml-sm" size="xs" label="Курсив" />
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../../vueComponent.js';
+
+import NumInput from '../../../../share/NumInput.vue';
+import rstore from '../../../../../store/modules/reader';
+
+const componentOptions = {
+    components: {
+        NumInput,
+    },
+    watch: {
+        form: {
+            handler() {
+                this.formChanged();//no await
+            },
+            deep: true,
+        },
+        fontBold: function(newValue) {
+            if (!this.isFormChanged)
+                this.form.fontWeight = (newValue ? 'bold' : '');
+        },
+        fontItalic: function(newValue) {
+            if (!this.isFormChanged)
+                this.form.fontStyle = (newValue ? 'italic' : '');
+        },
+        vertShift: function(newValue) {
+            if (!this.isFormChanged) {
+                const font = (this.form.webFontName ? this.form.webFontName : this.form.fontName);
+                if (this.form.fontShifts[font] != newValue || this.form.fontVertShift != newValue) {
+                    this.form.fontShifts = Object.assign({}, this.form.fontShifts, {[font]: newValue});
+                    this.form.fontVertShift = newValue;
+                }
+            }
+        },
+    },
+};
+class Font {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    fontBold = false;
+    fontItalic = false;
+    vertShift = 0;
+    webFonts = [];
+    fonts = [];    
+
+    created() {
+        this.formChanged();//no await
+    }
+
+    mounted() {
+    }
+
+    async formChanged() {
+        this.isFormChanged = true;
+        try {
+            this.fontBold = (this.form.fontWeight == 'bold');
+            this.fontItalic = (this.form.fontStyle == 'italic');
+
+            this.fonts = rstore.fonts;
+            this.webFonts = rstore.webFonts;
+            const font = (this.form.webFontName ? this.form.webFontName : this.form.fontName);
+            this.vertShift = this.form.fontShifts[font] || 0;
+        } finally {
+            await this.$nextTick();
+            this.isFormChanged = false;
+        }
+    }
+
+    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;
+    }
+
+}
+
+export default vueComponent(Font);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+.col-left {
+    width: 145px;
+}
+</style>

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

@@ -1,124 +0,0 @@
-<!---------------------------------------------->
-<div class="hidden part-header">Режим</div>
-
-<div class="item row">
-    <div class="label-2"></div>
-    <div class="col row">
-        <q-checkbox v-model="dualPageMode" size="xs" label="Двухстраничный режим" />
-    </div>
-</div>
-
-<div class="part-header">Страницы</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 v-show="dualPageMode" class="item row">
-    <div class="label-2">Отступ внутри</div>
-    <div class="col row">
-        <NumInput class="col-left" v-model="dualIndentLR" :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 v-show="dualPageMode">
-    <div class="part-header">Разделитель</div>
-
-    <div class="item row no-wrap">
-        <div class="label-2">Цвет</div>
-        <div class="col-left row">
-            <q-input class="col-left no-mp"
-                outlined dense
-                v-model="dualDivColorFiltered"
-                :rules="['hexColor']"
-                style="max-width: 150px"
-                :disable="dualDivColorAsText"
-            >
-                <template v-slot:prepend>
-                    <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('div')">
-                        <q-popup-proxy anchor="bottom middle" self="top middle">
-                            <div>
-                                <q-color v-model="dualDivColor"
-                                    no-header default-view="palette" :palette="predefineTextColors"
-                                />
-                            </div>
-                        </q-popup-proxy>
-                    </q-icon>
-                </template>
-            </q-input>
-        </div>
-        
-        <div class="q-px-xs"/>
-        <q-checkbox v-model="dualDivColorAsText" size="xs" label="Как у текста" />
-    </div>
-
-    <div class="item row">
-        <div class="label-2">Прозрачность</div>
-        <div class="col row">
-            <NumInput class="col-left" v-model="dualDivColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
-        </div>
-    </div>
-
-    <div class="item row">
-        <div class="label-2">Ширина (px)</div>
-        <div class="col row">
-            <NumInput class="col-left" v-model="dualDivWidth" :min="0" :max="100">
-                <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="dualDivHeight" :min="0" :max="100">
-                <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="dualDivStrokeFill" :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="dualDivStrokeGap" :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="dualDivShadowWidth" :min="0" :max="100"/>
-        </div>
-    </div>    
-
-</div>

+ 229 - 0
client/components/Reader/SettingsPage/ViewTab/Mode/Mode.vue

@@ -0,0 +1,229 @@
+<template>
+    <div>
+        <!---------------------------------------------->
+        <div class="hidden sets-part-header">
+            Режим
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col row">
+                <q-checkbox v-model="form.dualPageMode" size="xs" label="Двухстраничный режим" />
+            </div>
+        </div>
+
+        <div class="sets-part-header">
+            Страницы
+        </div>
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Отступ границ
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.indentLR" class="col-left" :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 v-model="form.indentTB" class="col" :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 v-show="form.dualPageMode" class="sets-item row">
+            <div class="sets-label label">
+                Отступ внутри
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.dualIndentLR" class="col-left" :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 v-show="form.dualPageMode">
+            <div class="sets-part-header">
+                Разделитель
+            </div>
+
+            <div class="sets-item row no-wrap">
+                <div class="sets-label label">
+                    Цвет
+                </div>
+                <div class="col-left row">
+                    <q-input 
+                        v-model="dualDivColorFiltered"
+                        class="col-left no-mp"
+                        outlined dense
+                        :rules="['hexColor']"
+                        style="max-width: 150px"
+                        :disable="form.dualDivColorAsText"
+                    >
+                        <template #prepend>
+                            <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.dualDivColor)">
+                                <q-popup-proxy anchor="bottom middle" self="top middle">
+                                    <div>
+                                        <q-color
+                                            v-model="form.dualDivColor"
+                                            no-header default-view="palette" :palette="defPalette.predefineTextColors"
+                                        />
+                                    </div>
+                                </q-popup-proxy>
+                            </q-icon>
+                        </template>
+                    </q-input>
+                </div>
+                
+                <div class="q-px-xs" />
+                <q-checkbox v-model="form.dualDivColorAsText" size="xs" label="Как у текста" />
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Прозрачность
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.dualDivColorAlpha" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
+                </div>
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Ширина (px)
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.dualDivWidth" class="col-left" :min="0" :max="100">
+                        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                            Ширина разделителя
+                        </q-tooltip>
+                    </NumInput>
+                </div>
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Высота (%)
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.dualDivHeight" class="col-left" :min="0" :max="100">
+                        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                            Высота разделителя
+                        </q-tooltip>
+                    </NumInput>
+                </div>
+            </div>
+
+            <div class="sets-item row">
+                <div class="sets-label label">
+                    Пунктир
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.dualDivStrokeFill" class="col-left" :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 v-model="form.dualDivStrokeGap" class="col" :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="sets-item row">
+                <div class="sets-label label">
+                    Ширина тени
+                </div>
+                <div class="col row">
+                    <NumInput v-model="form.dualDivShadowWidth" class="col-left" :min="0" :max="100" />
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../../vueComponent.js';
+import NumInput from '../../../../share/NumInput.vue';
+import * as helper from '../helper';
+import defPalette from '../defPalette';
+
+const componentOptions = {
+    components: {
+        NumInput
+    },
+    watch: {
+        form: {
+            handler() {
+                this.formChanged();//no await
+            },
+            deep: true,
+        },
+        dualDivColorFiltered(newValue) {
+            if (!this.isFormChanged && this.helper.isHexColor(newValue))
+                this.form.dualDivColor = newValue;
+        },
+    }
+};
+class Mode {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    helper = helper;
+    defPalette = defPalette;
+
+    isFormChanged = false;
+    dualDivColorFiltered = '';
+
+    created() {
+        this.formChanged();//no await
+    }
+
+    mounted() {
+    }
+
+    async formChanged() {
+        this.isFormChanged = true;
+        try {
+            this.dualDivColorFiltered = this.form.dualDivColor;
+
+            if (this.form.dualPageMode 
+                && (this.form.pageChangeAnimation == 'flip' || this.form.pageChangeAnimation == 'rightShift')
+                )
+                this.form.pageChangeAnimation = '';
+        } finally {
+            await this.$nextTick();
+            this.isFormChanged = false;
+        }
+    }
+}
+
+export default vueComponent(Mode);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+.col-left {
+    width: 145px;
+}
+
+.no-mp {
+    margin: 0;
+    padding: 0;
+}
+</style>

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

@@ -1,64 +0,0 @@
-<!---------------------------------------------->
-<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 v-show="showStatusBar" class="q-ml-sm" v-model="statusBarTop" size="xs" label="Вверху/внизу" />
-    </div>
-</div>
-
-<div v-show="showStatusBar" class="item row no-wrap">
-    <div class="label-2">Цвет</div>
-    <div class="col-left row">
-        <q-input class="col-left no-mp"
-            outlined dense
-            v-model="statusBarColorFiltered"
-            :rules="['hexColor']"
-            style="max-width: 150px"
-            :disable="statusBarColorAsText"
-        >
-            <template v-slot:prepend>
-                <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('statusbar')">
-                    <q-popup-proxy anchor="bottom middle" self="top middle">
-                        <div>
-                            <q-color v-model="statusBarColor"
-                                no-header default-view="palette" :palette="predefineTextColors"
-                            />
-                        </div>
-                    </q-popup-proxy>
-                </q-icon>
-            </template>
-        </q-input>
-    </div>
-    
-    <div class="q-px-xs"/>
-    <q-checkbox v-model="statusBarColorAsText" size="xs" label="Как у текста"/>
-</div>
-
-<div v-show="showStatusBar" 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"/>
-    </div>
-</div>
-
-<div v-show="showStatusBar" class="item row">
-    <div class="label-2">Высота</div>
-    <div class="col row">
-        <NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100"/>
-    </div>
-</div>
-
-<div v-show="showStatusBar" class="item row">
-    <div class="label-2"></div>
-    <div class="col row">
-        <q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
-            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-                По клику на автора-название в строке статуса<br>
-                открывать оригинал произведения в новой вкладке
-            </q-tooltip>
-        </q-checkbox>
-    </div>
-</div>

+ 153 - 0
client/components/Reader/SettingsPage/ViewTab/Status/Status.vue

@@ -0,0 +1,153 @@
+<template>
+    <div>
+        <!---------------------------------------------->
+        <div class="hidden sets-part-header">
+            Строка статуса
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Статус
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="form.showStatusBar" size="xs" label="Показывать" />
+                <q-checkbox v-show="form.showStatusBar" v-model="form.statusBarTop" class="q-ml-sm" size="xs" label="Вверху/внизу" />
+            </div>
+        </div>
+
+        <div v-show="form.showStatusBar" class="sets-item row no-wrap">
+            <div class="sets-label label">
+                Цвет
+            </div>
+            <div class="col-left row">
+                <q-input
+                    v-model="statusBarColorFiltered"
+                    class="col-left no-mp"
+                    outlined dense
+                    :rules="['hexColor']"
+                    style="max-width: 150px"
+                    :disable="form.statusBarColorAsText"
+                >
+                    <template #prepend>
+                        <q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.statusBarColor)">
+                            <q-popup-proxy anchor="bottom middle" self="top middle">
+                                <div>
+                                    <q-color
+                                        v-model="form.statusBarColor"
+                                        no-header default-view="palette" :palette="defPalette.predefineTextColors"
+                                    />
+                                </div>
+                            </q-popup-proxy>
+                        </q-icon>
+                    </template>
+                </q-input>
+            </div>
+            
+            <div class="q-px-xs" />
+            <q-checkbox v-model="form.statusBarColorAsText" size="xs" label="Как у текста" />
+        </div>
+
+        <div v-show="form.showStatusBar" class="sets-item row">
+            <div class="sets-label label">
+                Прозрачность
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.statusBarColorAlpha" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
+            </div>
+        </div>
+
+        <div v-show="form.showStatusBar" class="sets-item row">
+            <div class="sets-label label">
+                Высота
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.statusBarHeight" class="col-left" :min="5" :max="100" />
+            </div>
+        </div>
+
+        <div v-show="form.showStatusBar" class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col row">
+                <q-checkbox v-model="form.statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
+                    <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                        По клику на автора-название в строке статуса<br>
+                        открывать оригинал произведения в новой вкладке
+                    </q-tooltip>
+                </q-checkbox>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../../vueComponent.js';
+import NumInput from '../../../../share/NumInput.vue';
+import * as helper from '../helper';
+import defPalette from '../defPalette';
+
+const componentOptions = {
+    components: {
+        NumInput,
+    },
+    watch: {
+        form: {
+            handler() {
+                this.formChanged();//no await
+            },
+            deep: true,
+        },
+        statusBarColorFiltered(newValue) {
+            if (!this.isFormChanged && this.helper.isHexColor(newValue))
+                this.form.statusBarColor = newValue;
+        },
+    },
+};
+class Text {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    helper = helper;
+    defPalette = defPalette;
+
+    statusBarColorFiltered = '';
+
+    created() {
+        this.formChanged();//no await
+    }
+
+    mounted() {
+    }
+
+    async formChanged() {
+        this.isFormChanged = true;
+        try {
+            this.statusBarColorFiltered = this.form.statusBarColor;
+        } finally {
+            await this.$nextTick();
+            this.isFormChanged = false;
+        }
+    }
+
+}
+
+export default vueComponent(Text);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+.col-left {
+    width: 145px;
+}
+
+.no-mp {
+    margin: 0;
+    padding: 0;
+}
+</style>

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

@@ -1,127 +0,0 @@
-<!---------------------------------------------->
-<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="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="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" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
-    </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>

+ 210 - 0
client/components/Reader/SettingsPage/ViewTab/Text/Text.vue

@@ -0,0 +1,210 @@
+<template>
+    <div>
+        <!---------------------------------------------->
+        <div class="hidden sets-part-header">
+            Текст
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Интервал
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.lineInterval" class="col-left" :min="0" :max="200" />
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Параграф
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.p" class="col-left" :min="0" :max="2000" />
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Сдвиг
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.textVertShift" class="col-left" :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="sets-item row">
+            <div class="sets-label label">
+                Скроллинг
+            </div>
+            <div class="col row">
+                <NumInput v-model="form.scrollingDelay" class="col-left" :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
+                    v-model="form.scrollingType" class="col" :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="sets-item row">
+            <div class="sets-label label">
+                Выравнивание
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="form.textAlignJustify" size="xs" label="По ширине" />
+                <q-checkbox v-model="form.wordWrap" class="q-ml-sm" size="xs" label="Перенос по слогам" />
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col-left column justify-center text-right">
+                Компактность
+            </div>
+            <div class="q-px-sm" />
+            <NumInput v-model="form.compactTextPerc" class="col" :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="sets-item row">
+            <div class="sets-label label">
+                Обработка
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="form.cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col-left column justify-center text-right">
+                Добавлять пустые
+            </div>
+            <div class="q-px-sm" />
+            <NumInput v-model="form.addEmptyParagraphs" class="col" :min="0" :max="2" />
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label">
+                Изображения
+            </div>
+            <div class="col row">
+                <q-checkbox v-model="form.showImages" size="xs" label="Показывать" />
+                <q-checkbox v-model="form.showInlineImagesInCenter" class="q-ml-sm" :disable="!form.showImages" size="xs" label="Инлайн в центр" @update:modelValue="needReload">
+                    <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                        Выносить все изображения в центр экрана
+                    </q-tooltip>
+                </q-checkbox>
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col row">
+                <q-checkbox v-model="form.imageFitWidth" size="xs" label="Ширина не более размера страницы" :disable="!form.showImages || form.dualPageMode" />
+            </div>
+        </div>
+
+        <div class="sets-item row">
+            <div class="sets-label label"></div>
+            <div class="col-left column justify-center text-right">
+                Высота не более
+            </div>
+            <div class="q-px-sm" />
+            <NumInput v-model="form.imageHeightLines" class="col" :min="1" :max="100" :disable="!form.showImages">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Определяет высоту изображения количеством строк.<br>
+                    В случае превышения высоты, изображение будет<br>
+                    уменьшено с сохранением пропорций так, чтобы<br>
+                    помещаться в указанное количество строк.
+                </q-tooltip>
+            </NumInput>
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../../vueComponent.js';
+import NumInput from '../../../../share/NumInput.vue';
+
+const componentOptions = {
+    components: {
+        NumInput,
+    },
+    watch: {
+        form: {
+            handler() {
+                this.formChanged();//no await
+            },
+            deep: true,
+        },
+    },
+};
+class Text {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    statusBarColorFiltered = '';
+
+    created() {
+        this.formChanged();//no await
+    }
+
+    mounted() {
+    }
+
+    async formChanged() {
+        this.isFormChanged = true;
+        try {
+            //
+        } finally {
+            await this.$nextTick();
+            this.isFormChanged = false;
+        }
+    }
+
+    needReload() {
+        this.$root.notify.warning('Необходимо обновить страницу (F5), чтобы изменения возымели эффект');
+    }
+}
+
+export default vueComponent(Text);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 110px;
+}
+
+.col-left {
+    width: 145px;
+}
+</style>

+ 75 - 0
client/components/Reader/SettingsPage/ViewTab/ViewTab.vue

@@ -0,0 +1,75 @@
+<template>
+    <div class="fit column">
+        <q-tabs
+            v-model="selectedTab"
+            active-color="black"
+            active-bg-color="white"
+            indicator-color="white"
+            dense
+            no-caps
+            class="no-mp bg-grey-4 text-grey-7"
+        >
+            <q-tab name="mode" label="Режим" />
+            <q-tab name="color" label="Цвет" />
+            <q-tab name="font" label="Шрифт" />
+            <q-tab name="text" label="Текст" />
+            <q-tab name="status" label="Строка статуса" />
+        </q-tabs>
+
+        <div class="q-mb-sm" />
+
+        <div class="col sets-tab-panel">
+            <Mode v-if="selectedTab == 'mode'" :form="form" />
+            <Color v-if="selectedTab == 'color'" :form="form" />
+            <Font v-if="selectedTab == 'font'" :form="form" />
+            <Text v-if="selectedTab == 'text'" :form="form" />
+            <Status v-if="selectedTab == 'status'" :form="form" />
+        </div>
+    </div>
+</template>
+
+<script>
+//-----------------------------------------------------------------------------
+import vueComponent from '../../../vueComponent.js';
+
+import Mode from './Mode/Mode.vue';
+import Color from './Color/Color.vue';
+import Font from './Font/Font.vue';
+import Text from './Text/Text.vue';
+import Status from './Status/Status.vue';
+
+const componentOptions = {
+    components: {
+        Mode,
+        Color,
+        Font,
+        Text,
+        Status,
+    },
+};
+class ViewTab {
+    _options = componentOptions;
+    _props = {
+        form: Object,
+    };
+
+    selectedTab = 'mode';
+
+    created() {
+    }
+
+    mounted() {
+    }
+
+}
+
+export default vueComponent(ViewTab);
+//-----------------------------------------------------------------------------
+</script>
+
+<style scoped>
+.label {
+    width: 75px;
+}
+
+</style>

+ 29 - 1
client/components/Reader/SettingsPage/defPalette.js → client/components/Reader/SettingsPage/ViewTab/defPalette.js

@@ -14,4 +14,32 @@ const defPalette = [
     '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)'
     '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;
+export default {
+    predefinePalette: defPalette,
+
+    predefineTextColors: defPalette.concat([
+        '#ffffff',
+        '#000000',
+        '#202020',
+        '#323232',
+        '#aaaaaa',
+        '#00c0c0',
+        '#ebe2c9',
+        '#cfdc99',
+        '#478355',
+        '#909080',
+    ]),
+
+    predefineBackgroundColors: defPalette.concat([
+        '#ffffff',
+        '#000000',
+        '#202020',
+        '#ebe2c9',
+        '#cfdc99',
+        '#478355',
+        '#a6caf0',
+        '#909080',
+        '#808080',
+        '#c8c8c8',
+    ]),
+};

+ 9 - 0
client/components/Reader/SettingsPage/ViewTab/helper.js

@@ -0,0 +1,9 @@
+const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
+
+export function colorPanStyle(bgColor) {
+    return `width: 30px; height: 30px; border: 1px solid black; border-radius: 4px; background-color: ${bgColor}`;
+}
+
+export function isHexColor(value) {
+    return hex.test(value);
+}

+ 36 - 72
client/components/Reader/TextPage/TextPage.vue

@@ -81,9 +81,6 @@ const componentOptions = {
         settings: function() {
         settings: function() {
             this.debouncedLoadSettings();
             this.debouncedLoadSettings();
         },
         },
-        toggleLayout: function() {
-            this.updateLayout();
-        },
         inAnimation: function() {
         inAnimation: function() {
             this.updateLayout();
             this.updateLayout();
         },
         },
@@ -92,7 +89,6 @@ const componentOptions = {
 class TextPage {
 class TextPage {
     _options = componentOptions;
     _options = componentOptions;
 
 
-    toggleLayout = false;
     showStatusBar = false;
     showStatusBar = false;
     clickControl = true;
     clickControl = true;
 
 
@@ -130,10 +126,6 @@ class TextPage {
             this.startClickRepeat(x, y);
             this.startClickRepeat(x, y);
         }, 800);
         }, 800);
 
 
-        this.debouncedPrepareNextPage = _.debounce(() => {
-            this.prepareNextPage();
-        }, 100);
-
         this.debouncedDrawStatusBar = _.throttle(() => {
         this.debouncedDrawStatusBar = _.throttle(() => {
             this.drawStatusBar();
             this.drawStatusBar();
         }, 60);
         }, 60);
@@ -147,17 +139,11 @@ class TextPage {
         }, 50);
         }, 50);
 
 
         this.debouncedUpdatePage = _.debounce(async(lines) => {
         this.debouncedUpdatePage = _.debounce(async(lines) => {
-            if (!this.pageChangeAnimation)
-                this.toggleLayout = !this.toggleLayout;
-            else {
+            if (this.pageChangeAnimation) {
                 this.page2 = this.page1;
                 this.page2 = this.page1;
-                this.toggleLayout = true;
             }
             }
 
 
-            if (this.toggleLayout)
-                this.page1 = this.drawHelper.drawPage(lines);
-            else
-                this.page2 = this.drawHelper.drawPage(lines);
+            this.page1 = this.drawHelper.drawPage(lines);
 
 
             await this.doPageAnimation();
             await this.doPageAnimation();
         }, 10);
         }, 10);
@@ -174,7 +160,12 @@ class TextPage {
     }
     }
 
 
     hex2rgba(hex, alpha = 1) {
     hex2rgba(hex, alpha = 1) {
-        const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
+        let [r, g, b] = [0, 0, 0];
+        if (hex.length <= 4) {
+            [r, g, b] = hex.match(/\w/g).map(x => parseInt(x + x, 16));
+        } else {
+            [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
+        }
         return `rgba(${r},${g},${b},${alpha})`;
         return `rgba(${r},${g},${b},${alpha})`;
     }
     }
 
 
@@ -425,7 +416,6 @@ class TextPage {
     showBook() {
     showBook() {
         this.$refs.main.focus();
         this.$refs.main.focus();
 
 
-        this.toggleLayout = false;
         this.updateLayout();
         this.updateLayout();
         this.book = null;
         this.book = null;
         this.meta = null;
         this.meta = null;
@@ -483,12 +473,9 @@ class TextPage {
         if (this.inAnimation) {
         if (this.inAnimation) {
             this.$refs.scrollBox1.style.visibility = 'visible';
             this.$refs.scrollBox1.style.visibility = 'visible';
             this.$refs.scrollBox2.style.visibility = 'visible';
             this.$refs.scrollBox2.style.visibility = 'visible';
-        } else if (this.toggleLayout) {
+        } else {
             this.$refs.scrollBox1.style.visibility = 'visible';
             this.$refs.scrollBox1.style.visibility = 'visible';
             this.$refs.scrollBox2.style.visibility = 'hidden';
             this.$refs.scrollBox2.style.visibility = 'hidden';
-        } else {
-            this.$refs.scrollBox1.style.visibility = 'hidden';
-            this.$refs.scrollBox2.style.visibility = 'visible';
         }
         }
     }
     }
 
 
@@ -589,28 +576,25 @@ class TextPage {
 
 
         const transitionFinish = this.generateWaitingFunc('resolveTransition1Finish', 'stopScrolling');
         const transitionFinish = this.generateWaitingFunc('resolveTransition1Finish', 'stopScrolling');
 
 
-        if (!this.toggleLayout)
-            this.page1 = this.page2;
-        this.toggleLayout = true;
-        await this.$nextTick();
-        await utils.sleep(50);
-
         this.cachedPos = -1;
         this.cachedPos = -1;
         this.draw();
         this.draw();
 
 
         const page = this.$refs.scrollingPage1;
         const page = this.$refs.scrollingPage1;
         let i = 0;
         let i = 0;
         while (!this.stopScrolling) {
         while (!this.stopScrolling) {
-                page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
-                page.style.transform = `translateY(-${this.lineHeight}px)`;
-
                 if (i > 0) {
                 if (i > 0) {
                     this.doDown();
                     this.doDown();
+                    await utils.sleep(1);
+                    await this.$nextTick();
                     if (this.linesDown.length <= this.pageLineCount + 1) {
                     if (this.linesDown.length <= this.pageLineCount + 1) {
                         this.stopScrolling = true;
                         this.stopScrolling = true;
                     }
                     }
                 }
                 }
+
+                page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
+                page.style.transform = `translateY(-${this.lineHeight}px)`;
                 await transitionFinish(this.scrollingDelay);
                 await transitionFinish(this.scrollingDelay);
+
                 page.style.transition = '';
                 page.style.transition = '';
                 page.style.transform = 'none';
                 page.style.transform = 'none';
                 page.offsetHeight;
                 page.offsetHeight;
@@ -678,21 +662,11 @@ class TextPage {
             return;
             return;
         }
         }
 
 
-        //fast draw prepared
-        if (!this.pageChangeAnimation && this.pageChangeDirectionDown && this.pagePrepared && this.bookPos == this.bookPosPrepared) {
-            this.toggleLayout = !this.toggleLayout;
-            this.linesDown = this.linesDownNext;
-            this.linesUp = this.linesUpNext;
-        } else {//normal debounced draw
-            const lines = this.getLines(this.bookPos);
-            this.linesDown = lines.linesDown;
-            this.linesUp = lines.linesUp;
-            this.debouncedUpdatePage(lines.linesDown);
-        }
+        const lines = this.getLines(this.bookPos);
+        this.linesDown = lines.linesDown;
+        this.linesUp = lines.linesUp;
+        this.debouncedUpdatePage(lines.linesDown);
 
 
-        this.pagePrepared = false;
-        if (!this.pageChangeAnimation)
-            this.debouncedPrepareNextPage();
         this.debouncedDrawStatusBar();
         this.debouncedDrawStatusBar();
         this.debouncedDrawPageDividerAndOrnament();
         this.debouncedDrawPageDividerAndOrnament();
 
 
@@ -907,30 +881,6 @@ class TextPage {
         }
         }
     }
     }
 
 
-    prepareNextPage() {
-        // подготовка следующей страницы заранее        
-        if (!this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1)
-            return;
-        
-        let i = this.pageLineCount;
-        if (this.keepLastToFirst)
-            i--;
-        if (i >= 0 && this.linesDown.length > i) {
-            this.bookPosPrepared = this.linesDown[i].begin;
-            
-            const lines = this.getLines(this.bookPosPrepared);
-            this.linesDownNext = lines.linesDown;
-            this.linesUpNext =  lines.linesUp;
-
-            if (this.toggleLayout)
-                this.page2 = this.drawHelper.drawPage(lines.linesDown);//наоборот
-            else
-                this.page1 = this.drawHelper.drawPage(lines.linesDown);
-
-            this.pagePrepared = true;
-        }
-    }
-
     doDown() {
     doDown() {
         if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
         if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
             this.userBookPosChange = true;
             this.userBookPosChange = true;
@@ -1117,6 +1067,7 @@ class TextPage {
             if (this.startTouch) {
             if (this.startTouch) {
                 const dy = this.startTouch.y - y;
                 const dy = this.startTouch.y - y;
                 const dx = this.startTouch.x - x;
                 const dx = this.startTouch.x - x;
+                this.startTouch = null;
                 const moveDelta = 30;
                 const moveDelta = 30;
                 const touchDelta = 15;
                 const touchDelta = 15;
                 if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
                 if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
@@ -1132,10 +1083,23 @@ class TextPage {
                     //движение вправо
                     //движение вправо
                     this.doScrollingSpeedUp();
                     this.doScrollingSpeedUp();
                 } else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
                 } else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
-                    this.doToolBarToggle(event);
-                }
+                    if (this.touchMode) {
+                        this.touchMode = 2;
+                        return;
+                    }
 
 
-                this.startTouch = null;
+                    (async() => {
+                        this.touchMode = 1;
+                        let i = 20;
+                        while (i-- > 0 && this.touchMode === 1)
+                            await utils.sleep(10);
+                        if (this.touchMode === 1)
+                            this.doToolBarToggle();
+                        else
+                            this.doFullScreenToggle();
+                        this.touchMode = 0;
+                    })();
+                }
             }
             }
         }
         }
     }
     }

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

@@ -1,4 +1,20 @@
 export const versionHistory = [
 export const versionHistory = [
+{
+    version: '1.0.0',
+    releaseDate: '2022-12-18',
+    showUntil: '2022-12-25',
+    content:
+`
+<ul>
+    <li>на мобильных устройствах переход в полноэкранный режим теперь возможен через двойной тап по центру</li>
+    <li>добавлено окно "Сетевая библиотека" для omnireader.ru</li>
+    <li>улучшена работа синхронизации с сервером при плохом качестве связи</li>
+    <li>добавлена сборка релизов читалки: <a href="https://github.com/bookpauk/liberama/releases" target="_blank">https://github.com/bookpauk/liberama/releases</a></li>
+</ul>
+
+`
+},
+
 {
 {
     version: '0.12.2',
     version: '0.12.2',
     releaseDate: '2022-09-04',
     releaseDate: '2022-09-04',

+ 0 - 19
client/components/Settings/Settings.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Settings в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../vueComponent.js';
-
-class Settings {
-    created() {
-    }
-
-}
-
-export default vueComponent(Settings);
-//-----------------------------------------------------------------------------
-</script>

+ 0 - 19
client/components/Sources/Sources.vue

@@ -1,19 +0,0 @@
-<template>
-    <div>
-        Раздел Sources в разработке
-    </div>
-</template>
-
-<script>
-//-----------------------------------------------------------------------------
-import vueComponent from '../vueComponent.js';
-
-class Sources {
-    created() {
-    }
-
-}
-
-export default vueComponent(Sources);
-//-----------------------------------------------------------------------------
-</script>

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

@@ -27,7 +27,6 @@ class Notify {
             icon,
             icon,
             actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
             actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
             html: true,
             html: true,
-            classes: 'notify-margin',
 
 
             message: 
             message: 
                 `<div style="max-width: 350px">
                 `<div style="max-width: 350px">

+ 94 - 24
client/components/share/NumInput.vue

@@ -6,15 +6,26 @@
         class="no-mp"
         class="no-mp"
         :class="(error ? 'error' : '')"
         :class="(error ? 'error' : '')"
         :disable="disable"
         :disable="disable"
+        :mask="mask"
     >
     >
         <slot></slot>
         <slot></slot>
         <template #prepend>
         <template #prepend>
+            <q-icon
+                v-show="mmButtons"
+                v-ripple="modelValue != min" 
+                style="font-size: 100%"
+                :class="(modelValue != min ? '' : 'disable')" 
+                name="la la-angle-double-left" 
+                class="button" 
+                @click="toMin"
+            />
+
             <q-icon
             <q-icon
                 v-ripple="validate(modelValue - step)" 
                 v-ripple="validate(modelValue - step)" 
                 :class="(validate(modelValue - step) ? '' : 'disable')" 
                 :class="(validate(modelValue - step) ? '' : 'disable')" 
-                name="la la-minus-circle" 
+                :name="minusIcon" 
                 class="button" 
                 class="button" 
-                @click="minus"
+                @click="onClick('minus')"
                 @mousedown.prevent.stop="onMouseDown($event, 'minus')"
                 @mousedown.prevent.stop="onMouseDown($event, 'minus')"
                 @mouseup.prevent.stop="onMouseUp"
                 @mouseup.prevent.stop="onMouseUp"
                 @mouseout.prevent.stop="onMouseUp"
                 @mouseout.prevent.stop="onMouseUp"
@@ -27,9 +38,9 @@
             <q-icon
             <q-icon
                 v-ripple="validate(modelValue + step)"
                 v-ripple="validate(modelValue + step)"
                 :class="(validate(modelValue + step) ? '' : 'disable')"
                 :class="(validate(modelValue + step) ? '' : 'disable')"
-                name="la la-plus-circle"
+                :name="plusIcon"
                 class="button"
                 class="button"
-                @click="plus"
+                @click="onClick('plus')"
                 @mousedown.prevent.stop="onMouseDown($event, 'plus')"
                 @mousedown.prevent.stop="onMouseDown($event, 'plus')"
                 @mouseup.prevent.stop="onMouseUp"
                 @mouseup.prevent.stop="onMouseUp"
                 @mouseout.prevent.stop="onMouseUp"
                 @mouseout.prevent.stop="onMouseUp"
@@ -37,6 +48,16 @@
                 @touchend.stop="onTouchEnd"
                 @touchend.stop="onTouchEnd"
                 @touchcancel.prevent.stop="onTouchEnd"
                 @touchcancel.prevent.stop="onTouchEnd"
             />
             />
+
+            <q-icon
+                v-show="mmButtons"
+                v-ripple="modelValue != max" 
+                style="font-size: 100%"
+                :class="(modelValue != max ? '' : 'disable')" 
+                name="la la-angle-double-right" 
+                class="button" 
+                @click="toMax"
+            />
         </template>
         </template>
     </q-input>
     </q-input>
 </template>
 </template>
@@ -49,17 +70,18 @@ import * as utils from '../../share/utils';
 
 
 const componentOptions = {
 const componentOptions = {
     watch: {
     watch: {
-        filteredValue: function(newValue) {
-            if (this.validate(newValue)) {
-                this.error = false;
-                this.$emit('update:modelValue', this.string2number(newValue));
-            } else {
-                this.error = true;
-            }
+        filteredValue() {
+            this.checkErrorAndEmit(true);
         },
         },
-        modelValue: function(newValue) {
+        modelValue(newValue) {
             this.filteredValue = newValue;
             this.filteredValue = newValue;
         },
         },
+        min() {
+            this.checkErrorAndEmit();
+        },
+        max() {
+            this.checkErrorAndEmit();
+        }
     }
     }
 };
 };
 class NumInput {
 class NumInput {
@@ -70,7 +92,11 @@ class NumInput {
         max: { type: Number, default: Number.MAX_VALUE },
         max: { type: Number, default: Number.MAX_VALUE },
         step: { type: Number, default: 1 },
         step: { type: Number, default: 1 },
         digits: { type: Number, default: 0 },
         digits: { type: Number, default: 0 },
-        disable: Boolean
+        disable: Boolean,
+        minusIcon: {type: String, default: 'la la-minus-circle'},
+        plusIcon: {type: String, default: 'la la-plus-circle'},
+        mmButtons: Boolean,
+        mask: String,
     };
     };
 
 
     filteredValue = 0;
     filteredValue = 0;
@@ -95,6 +121,16 @@ class NumInput {
         return true;
         return true;
     }
     }
 
 
+    checkErrorAndEmit(emit = false) {
+        if (this.validate(this.filteredValue)) {
+            this.error = false;
+            if (emit)
+                this.$emit('update:modelValue', this.string2number(this.filteredValue));
+        } else {
+            this.error = true;
+        }
+    }
+
     plus() {
     plus() {
         const newValue = this.modelValue + this.step;
         const newValue = this.modelValue + this.step;
         if (this.validate(newValue))
         if (this.validate(newValue))
@@ -107,23 +143,42 @@ class NumInput {
             this.filteredValue = newValue;
             this.filteredValue = newValue;
     }
     }
 
 
+    onClick(way) {
+        if (this.clickRepeat)
+            return;
+
+        if (way == 'plus') {
+            this.plus();
+        } else {
+            this.minus();
+        }
+    }
+
     onMouseDown(event, way) {
     onMouseDown(event, way) {
         this.startClickRepeat = true;
         this.startClickRepeat = true;
         this.clickRepeat = false;
         this.clickRepeat = false;
 
 
         if (event.button == 0) {
         if (event.button == 0) {
             (async() => {
             (async() => {
-                await utils.sleep(300);
-                if (this.startClickRepeat) {
-                    this.clickRepeat = true;
-                    while (this.clickRepeat) {
-                        if (way == 'plus') {
-                            this.plus();
-                        } else {
-                            this.minus();
+                if (this.inRepeatFunc)
+                    return;
+
+                this.inRepeatFunc = true;
+                try {
+                    await utils.sleep(300);
+                    if (this.startClickRepeat) {
+                        this.clickRepeat = true;
+                        while (this.clickRepeat) {
+                            if (way == 'plus') {
+                                this.plus();
+                            } else {
+                                this.minus();
+                            }
+                            await utils.sleep(100);
                         }
                         }
-                        await utils.sleep(50);
                     }
                     }
+                } finally {
+                    this.inRepeatFunc = false;
                 }
                 }
             })();
             })();
         }
         }
@@ -133,7 +188,12 @@ class NumInput {
         if (this.inTouch)
         if (this.inTouch)
             return;
             return;
         this.startClickRepeat = false;
         this.startClickRepeat = false;
-        this.clickRepeat = false;
+        if (this.clickRepeat) {
+            (async() => {
+                await utils.sleep(50);
+                this.clickRepeat = false;
+            })();
+        }
     }
     }
 
 
     onTouchStart(event, way) {
     onTouchStart(event, way) {
@@ -151,6 +211,14 @@ class NumInput {
         this.inTouch = false;
         this.inTouch = false;
         this.onMouseUp();
         this.onMouseUp();
     }
     }
+
+    toMin() {
+        this.filteredValue = this.min;
+    }
+
+    toMax() {
+        this.filteredValue = this.max;
+    }
 }
 }
 
 
 export default vueComponent(NumInput);
 export default vueComponent(NumInput);
@@ -165,7 +233,9 @@ export default vueComponent(NumInput);
 
 
 .button {
 .button {
     font-size: 130%;
     font-size: 130%;
-    border-radius: 20px;
+    border-radius: 15px;
+    width: 30px;
+    height: 30px;
     color: #bbb;
     color: #bbb;
     cursor: pointer;
     cursor: pointer;
 }
 }

+ 3 - 1
client/components/share/Window.vue

@@ -10,7 +10,9 @@
                     @touchend.stop="onTouchEnd"
                     @touchend.stop="onTouchEnd"
                     @touchmove.stop="onTouchMove"
                     @touchmove.stop="onTouchMove"
                 >
                 >
-                    <span class="header-text col"><slot name="header"></slot></span>
+                    <div class="header-text col" style="width: 0">
+                        <slot name="header"></slot>
+                    </div>
                     <slot name="buttons"></slot>
                     <slot name="buttons"></slot>
                     <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
                     <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
                 </div>
                 </div>

+ 24 - 15
client/components/vueComponent.js

@@ -17,7 +17,7 @@ export default function(componentClass) {
                     }
                     }
                 }
                 }
             } else if (prop === '_props') {
             } else if (prop === '_props') {
-                comp['props'] = obj[prop];
+                comp.props = obj[prop];
             }
             }
         } else {//usual prop
         } else {//usual prop
             data[prop] = obj[prop];
             data[prop] = obj[prop];
@@ -26,23 +26,32 @@ export default function(componentClass) {
     comp.data = () => _.cloneDeep(data);
     comp.data = () => _.cloneDeep(data);
     
     
     //methods
     //methods
-    const classProto = Object.getPrototypeOf(obj);
-    const classMethods = Object.getOwnPropertyNames(classProto);
     const methods = {};
     const methods = {};
     const computed = {};
     const computed = {};
-    for (const method of classMethods) {
-        const desc = Object.getOwnPropertyDescriptor(classProto, method);
-        if (desc.get) {//has getter, computed
-            computed[method] = {get: desc.get};
-            if (desc.set)
-                computed[method].set = desc.set;
-        } else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
-                    'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
-                    'setup'].includes(method) ) {
-            comp[method] = obj[method];
-        } else if (method !== 'constructor') {//usual
-            methods[method] = obj[method];
+
+    let classProto = Object.getPrototypeOf(obj);
+    while (classProto) {
+        const classMethods = Object.getOwnPropertyNames(classProto);
+        for (const method of classMethods) {
+            const desc = Object.getOwnPropertyDescriptor(classProto, method);
+            if (desc.get) {//has getter, computed
+                if (!computed[method]) {
+                    computed[method] = {get: desc.get};
+                    if (desc.set)
+                        computed[method].set = desc.set;
+                }
+            } else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',
+                        'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',
+                        'setup'].includes(method) ) {//life cycle hooks
+                if (!comp[method])
+                    comp[method] = obj[method];
+            } else if (method !== 'constructor') {//usual
+                if (!methods[method])
+                    methods[method] = obj[method];
+            }
         }
         }
+
+        classProto = Object.getPrototypeOf(classProto);
     }
     }
     comp.methods = methods;
     comp.methods = methods;
     comp.computed = computed;
     comp.computed = computed;

+ 2 - 27
client/router.js

@@ -1,41 +1,16 @@
 import { createRouter, createWebHashHistory } from 'vue-router';
 import { createRouter, createWebHashHistory } from 'vue-router';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
-const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
-const Search = () => import('./components/CardIndex/Search/Search.vue');
-const Card = () => import('./components/CardIndex/Card/Card.vue');
-const Book = () => import('./components/CardIndex/Book/Book.vue');
-const History = () => import('./components/CardIndex/History/History.vue');
-
 //немедленная загрузка
 //немедленная загрузка
 //import Reader from './components/Reader/Reader.vue';
 //import Reader from './components/Reader/Reader.vue';
 const Reader = () => import('./components/Reader/Reader.vue');
 const Reader = () => import('./components/Reader/Reader.vue');
 const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
 const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
 
 
-const Income = () => import('./components/Income/Income.vue');
-const Sources = () => import('./components/Sources/Sources.vue');
-const Settings = () => import('./components/Settings/Settings.vue');
-const Help = () => import('./components/Help/Help.vue');
-const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
-
 const myRoutes = [
 const myRoutes = [
-    ['/', null, null, '/cardindex'],
-    ['/cardindex', CardIndex],
-    ['/cardindex~search', Search],
-    ['/cardindex~card', Card],
-    ['/cardindex~card/:authorId', Card],
-    ['/cardindex~book', Book],
-    ['/cardindex~book/:bookId', Book],
-    ['/cardindex~history', History],
-
+    ['/', null, null, '/reader'],
     ['/reader', Reader],
     ['/reader', Reader],
     ['/external-libs', ExternalLibs],
     ['/external-libs', ExternalLibs],
-    ['/income', Income],
-    ['/sources', Sources],
-    ['/settings', Settings],
-    ['/help', Help],
-    ['/404', NotFound404],
-    ['/:pathMatch(.*)*', null, null, '/cardindex'],
+    ['/:pathMatch(.*)*', null, null, '/reader'],
 ];
 ];
 
 
 let routes = {};
 let routes = {};

+ 5 - 18
client/share/utils.js

@@ -1,4 +1,5 @@
 import _ from 'lodash';
 import _ from 'lodash';
+import dayjs from 'dayjs';
 import baseX from 'base-x';
 import baseX from 'base-x';
 import PAKO from 'pako';
 import PAKO from 'pako';
 import {Buffer} from 'safe-buffer';
 import {Buffer} from 'safe-buffer';
@@ -35,24 +36,6 @@ export function randomHexString(len) {
     return Buffer.from(randomArray(len)).toString('hex');
     return Buffer.from(randomArray(len)).toString('hex');
 }
 }
 
 
-export function formatDate(d, format) {
-    if (!format)
-        format = 'normal';
-
-    switch (format) {
-        case 'normal':
-            return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` + 
-                `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
-        case 'coDate':
-            return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
-        case 'coMonth':
-            return `${(d.getMonth() + 1).toString().padStart(2, '0')}`;
-        case 'noDate':
-            return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
-    }
-    
-}
-
 export function fallbackCopyTextToClipboard(text) {
 export function fallbackCopyTextToClipboard(text) {
     let textArea = document.createElement('textarea');
     let textArea = document.createElement('textarea');
     textArea.value = text;
     textArea.value = text;
@@ -416,3 +399,7 @@ export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
 export function makeDonation() {
 export function makeDonation() {
     window.open('https://donatty.com/liberama', '_blank');
     window.open('https://donatty.com/liberama', '_blank');
 }
 }
+
+export function dateFormat(date, format = 'DD.MM.YYYY') {
+    return dayjs(date).format(format);
+}

+ 64 - 37
client/store/modules/reader.js

@@ -1,6 +1,10 @@
 import * as utils from '../../share/utils';
 import * as utils from '../../share/utils';
 import googleFonts from './fonts/fonts.json';
 import googleFonts from './fonts/fonts.json';
 
 
+const minuteMs = 60*1000;//количество ms в минуте
+const hourMs = 60*minuteMs;//количество ms в часе
+const dayMs = 24*hourMs;//количество ms в сутках
+
 const readerActions = {
 const readerActions = {
     'loader': 'На страницу загрузки',
     'loader': 'На страницу загрузки',
     'loadFile': 'Загрузить файл с диска',
     'loadFile': 'Загрузить файл с диска',
@@ -44,17 +48,17 @@ const toolButtons = [
     {name: 'undoAction',  show: true},
     {name: 'undoAction',  show: true},
     {name: 'redoAction',  show: true},
     {name: 'redoAction',  show: true},
     {name: 'fullScreen',  show: true},
     {name: 'fullScreen',  show: true},
-    {name: 'scrolling',   show: false},
+    {name: 'scrolling',   show: true},
     {name: 'setPosition', show: true},
     {name: 'setPosition', show: true},
     {name: 'search',      show: true},
     {name: 'search',      show: true},
-    {name: 'copyText',    show: false},
+    {name: 'copyText',    show: true},
     {name: 'convOptions', show: true},
     {name: 'convOptions', show: true},
     {name: 'refresh',     show: true},
     {name: 'refresh',     show: true},
     {name: 'contents',    show: true},
     {name: 'contents',    show: true},
     {name: 'libs',        show: true},
     {name: 'libs',        show: true},
     {name: 'recentBooks', show: true},
     {name: 'recentBooks', show: true},
-    {name: 'clickControl', show: false},
-    {name: 'offlineMode', show: false},
+    {name: 'clickControl', show: true},
+    {name: 'offlineMode', show: true},
 ];
 ];
 
 
 //readerActions[name]
 //readerActions[name]
@@ -186,6 +190,7 @@ const settingDefaults = {
     fontShifts: {},
     fontShifts: {},
     showToolButton: {},
     showToolButton: {},
     toolBarHideOnScroll: false,
     toolBarHideOnScroll: false,
+    toolBarMultiLine: true,
     userHotKeys: {},
     userHotKeys: {},
     userWallpapers: [],
     userWallpapers: [],
 
 
@@ -198,10 +203,6 @@ const settingDefaults = {
     bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
     bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
     bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
     bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
     bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
     bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
-
-    //для SettingsPage
-    needUpdateSettingsView: 0,
-
 };
 };
 
 
 for (const font of fonts)
 for (const font of fonts)
@@ -227,30 +228,52 @@ function addDefaultsToSettings(settings) {
     return false;
     return false;
 }
 }
 
 
-const libsDefaults = {
-    startLink: 'http://flibusta.is',
-    comment: 'Флибуста | Книжное братство',
-    closeAfterSubmit: false,
-    openInFrameOnEnter: false,
-    openInFrameOnAdd: false,
-    groups: [
-        {r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
-            {l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
-        ]},
-        {r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
-            {l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
-        ]},
-        {r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
-            {l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
-        ]},
-        {r: 'http://lib.ru', s: 'http://lib.ru', list: [
-            {l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
-        ]},
-        {r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
-            {l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
-        ]},
-    ]
-};
+function getLibsDefaults(mode = 'reader') {
+    const result = {
+        startLink: '',
+        comment: '',
+        closeAfterSubmit: false,
+        openInFrameOnEnter: false,
+        openInFrameOnAdd: false,
+        helpShowed: false,
+        mode,
+        groups: [
+            {r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
+                {l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
+            ]},
+            {r: 'http://lib.ru', s: 'http://lib.ru', list: [
+                {l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
+            ]},
+            {r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
+                {l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
+            ]},
+        ],
+    };
+
+    if (mode === 'liberama') {
+        result.groups.unshift(
+            {r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
+                {l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
+            ]}
+        );
+        result.groups.unshift(
+            {r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
+                {l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
+            ]}
+        );
+    } else if (mode === 'omnireader') {
+        result.groups.unshift(
+            {r: 'https://lib.omnireader.ru', s: 'https://lib.omnireader.ru', list: [
+                {l: 'https://lib.omnireader.ru', c: 'Общественное достояние'},
+            ]}
+        );
+    }
+
+    result.startLink = result.groups[0].r;
+    result.comment = result.groups[0].c;
+
+    return result;
+}
 
 
 // initial state
 // initial state
 const state = {
 const state = {
@@ -262,11 +285,11 @@ const state = {
     profilesRev: 0,
     profilesRev: 0,
     allowProfilesSave: false,//подстраховка для разработки
     allowProfilesSave: false,//подстраховка для разработки
     whatsNewContentHash: '',
     whatsNewContentHash: '',
-    donationRemindDate: '',
+    donationNextPopup: Date.now() + dayMs*30,
     currentProfile: '',
     currentProfile: '',
     settings: Object.assign({}, settingDefaults),
     settings: Object.assign({}, settingDefaults),
     settingsRev: {},
     settingsRev: {},
-    libs: Object.assign({}, libsDefaults),
+    libs: false,
     libsRev: 0,
     libsRev: 0,
 };
 };
 
 
@@ -302,8 +325,8 @@ const mutations = {
     setWhatsNewContentHash(state, value) {
     setWhatsNewContentHash(state, value) {
         state.whatsNewContentHash = value;
         state.whatsNewContentHash = value;
     },
     },
-    setDonationRemindDate(state, value) {
-        state.donationRemindDate = value;
+    setDonationNextPopup(state, value) {
+        state.donationNextPopup = value;
     },
     },
     setCurrentProfile(state, value) {
     setCurrentProfile(state, value) {
         state.currentProfile = value;
         state.currentProfile = value;
@@ -329,6 +352,10 @@ const mutations = {
 };
 };
 
 
 export default {
 export default {
+    minuteMs,
+    hourMs,
+    dayMs,
+
     readerActions,
     readerActions,
     toolButtons,
     toolButtons,
     hotKeys,
     hotKeys,
@@ -336,7 +363,7 @@ export default {
     webFonts,
     webFonts,
     settingDefaults,
     settingDefaults,
     addDefaultsToSettings,
     addDefaultsToSettings,
-    libsDefaults,
+    getLibsDefaults,
 
 
     namespaced: true,
     namespaced: true,
     state,
     state,

+ 14 - 10
docs/beta/beta.liberama

@@ -87,18 +87,22 @@ server {
     proxy_read_timeout 600s;
     proxy_read_timeout 600s;
   }
   }
 
 
-  location / {
-    root /home/beta.liberama/public;
+  location /tmp {
+    root /home/beta.liberama/.liberama/public-files;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
-    }
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+  location /upload {
+    root /home/beta.liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/beta.liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;

+ 66 - 10
docs/beta/beta.omnireader

@@ -32,18 +32,22 @@ server {
     proxy_read_timeout 600s;
     proxy_read_timeout 600s;
   }
   }
 
 
-  location / {
-    root /home/beta.liberama/public;
+  location /tmp {
+    root /home/beta.liberama/.liberama/public-files;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
-    }
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+  location /upload {
+    root /home/beta.liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/beta.liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;
@@ -57,3 +61,55 @@ server {
 
 
   return 301 https://$host$request_uri;
   return 301 https://$host$request_uri;
 }
 }
+
+server {
+  listen 80;
+  server_name b.beta.omnireader.ru;
+  set $liberama http://127.0.0.1:34081;
+
+  client_max_body_size 50m;
+  proxy_read_timeout 1h;
+
+  gzip on;
+  gzip_min_length 1024;
+  gzip_proxied expired no-cache no-store private auth;
+  gzip_types *;
+
+  location @liberama {
+    proxy_pass $liberama;
+  }
+
+  location /api {
+    proxy_pass $liberama;
+  }
+
+  location /ws {
+    proxy_pass $liberama;
+    proxy_http_version 1.1;
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+    proxy_read_timeout 600s;
+  }
+
+  location /tmp {
+    root /home/beta.liberama/.liberama/public-files;
+
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
+
+  location /upload {
+    root /home/beta.liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/beta.liberama/.liberama/public;
+
+    location ~* \.(?:manifest|appcache|html)$ {
+      expires -1;
+    }
+  }
+}

+ 15 - 11
docs/beta/beta.omnireader_http

@@ -1,6 +1,6 @@
 server {
 server {
   listen 80;
   listen 80;
-  server_name beta.omnireader.ru;
+  server_name beta.omnireader.ru b.beta.omnireader.ru;
   set $liberama http://127.0.0.1:34081;
   set $liberama http://127.0.0.1:34081;
 
 
   client_max_body_size 50m;
   client_max_body_size 50m;
@@ -27,18 +27,22 @@ server {
     proxy_read_timeout 600s;
     proxy_read_timeout 600s;
   }
   }
 
 
-  location / {
-    root /home/beta.liberama/public;
+  location /tmp {
+    root /home/beta.liberama/.liberama/public-files;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
-    }
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+  location /upload {
+    root /home/beta.liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/beta.liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;

+ 28 - 20
docs/liberama.top/liberama

@@ -43,18 +43,22 @@ server {
     proxy_read_timeout 600s;
     proxy_read_timeout 600s;
   }
   }
 
 
-  location / {
-    root /home/liberama/public;
+  location /tmp {
+    root /home/liberama/.liberama/public-files;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
-    }
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+  location /upload {
+    root /home/liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;
@@ -98,18 +102,22 @@ server {
     proxy_read_timeout 600s;
     proxy_read_timeout 600s;
   }
   }
 
 
-  location / {
-    root /home/liberama/public;
+  location /tmp {
+    root /home/liberama/.liberama/public-files;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
-    }
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+  location /upload {
+    root /home/liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;

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

@@ -3,7 +3,7 @@
 ### git, clone
 ### git, clone
 ```
 ```
 cd ~
 cd ~
-sudo apt install ssh git
+sudo apt install ssh git zip
 git clone https://github.com/bookpauk/liberama
 git clone https://github.com/bookpauk/liberama
 ```
 ```
 
 
@@ -18,6 +18,7 @@ sudo apt install -y nodejs
 ```
 ```
 cd liberama
 cd liberama
 npm i
 npm i
+cd docs/omnireader.ru
 ```
 ```
 
 
 ### create public dir
 ### create public dir
@@ -30,8 +31,8 @@ sudo chown www-data.www-data /home/liberama
 #### download from https://download.calibre-ebook.com/
 #### download from https://download.calibre-ebook.com/
 ```
 ```
 wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz"
 wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz"
-sudo -u www-data mkdir -p /home/liberama/data/calibre
-sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/data/calibre
+sudo -u www-data mkdir -p /home/liberama/.liberama/calibre
+sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/.liberama/calibre
 ```
 ```
 
 
 ### external converters
 ### external converters
@@ -44,7 +45,7 @@ sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graph
 Сначала настроим для HTTP:
 Сначала настроим для HTTP:
 ```
 ```
 sudo apt install nginx
 sudo apt install nginx
-sudo cp docs/omnireader.ru/omnireader_http /etc/nginx/sites-available/omnireader
+sudo cp ./omnireader_http /etc/nginx/sites-available/omnireader
 sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
 sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
 sudo rm /etc/nginx/sites-enabled/default
 sudo rm /etc/nginx/sites-enabled/default
 sudo service nginx reload
 sudo service nginx reload
@@ -55,7 +56,7 @@ sudo chown -R www-data.www-data /var/www
 #### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20
 #### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20
 После установки сертификата, можно использовать конфиг для nginx c ssl:
 После установки сертификата, можно использовать конфиг для nginx c ssl:
 ```
 ```
-sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
+sudo cp ./omnireader /etc/nginx/sites-available/omnireader
 sudo service nginx reload
 sudo service nginx reload
 
 
 ```
 ```
@@ -68,7 +69,7 @@ sudo service php7.4-fpm restart
 
 
 sudo mkdir /home/oldreader
 sudo mkdir /home/oldreader
 sudo chown www-data.www-data /home/oldreader
 sudo chown www-data.www-data /home/oldreader
-sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
+sudo -u www-data cp -r ./old/* /home/oldreader
 ```
 ```
 
 
 ## Запуск по крону
 ## Запуск по крону
@@ -78,7 +79,6 @@ sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
 
 
 ## Деплой и запуск
 ## Деплой и запуск
 ```
 ```
-cd docs/omnireader.ru
 ./stop_server.sh
 ./stop_server.sh
 ./deploy.sh
 ./deploy.sh
 ./start_server.sh
 ./start_server.sh

+ 63 - 8
docs/omnireader.ru/omnireader

@@ -32,18 +32,73 @@ server {
     proxy_read_timeout 600s;
     proxy_read_timeout 600s;
   }
   }
 
 
+  location /tmp {
+    root /home/liberama/.liberama/public-files;
+
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
+
+  location /upload {
+    root /home/liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
   location / {
   location / {
-    root /home/liberama/public;
+    root /home/liberama/.liberama/public;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
+    location ~* \.(?:manifest|appcache|html)$ {
+      expires -1;
     }
     }
+  }
+}
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+server {
+  listen 80;
+  server_name b.omnireader.ru;
+  set $liberama http://127.0.0.1:44081;
+
+  client_max_body_size 50m;
+  proxy_read_timeout 1h;
+
+  gzip on;
+  gzip_min_length 1024;
+  gzip_proxied expired no-cache no-store private auth;
+  gzip_types *;
+
+  location @liberama {
+    proxy_pass $liberama;
+  }
+
+  location /api {
+    proxy_pass $liberama;
+  }
+
+  location /ws {
+    proxy_pass $liberama;
+    proxy_http_version 1.1;
+    proxy_set_header Upgrade $http_upgrade;
+    proxy_set_header Connection "upgrade";
+  }
+
+  location /tmp {
+    root /home/liberama/.liberama/public-files;
+
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
+
+  location /upload {
+    root /home/liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;

+ 15 - 11
docs/omnireader.ru/omnireader_http

@@ -1,6 +1,6 @@
 server {
 server {
   listen 80;
   listen 80;
-  server_name omnireader.ru;
+  server_name omnireader.ru b.omnireader.ru;
   set $liberama http://127.0.0.1:44081;
   set $liberama http://127.0.0.1:44081;
 
 
   client_max_body_size 50m;
   client_max_body_size 50m;
@@ -26,18 +26,22 @@ server {
     proxy_set_header Connection "upgrade";
     proxy_set_header Connection "upgrade";
   }
   }
 
 
-  location / {
-    root /home/liberama/public;
+  location /tmp {
+    root /home/liberama/.liberama/public-files;
 
 
-    location /tmp {
-      types { } default_type "application/xml; charset=utf-8";
-      add_header Content-Encoding gzip;
-      try_files $uri @liberama;
-    }
+    types { } default_type "application/xml; charset=utf-8";
+    add_header Content-Encoding gzip;
+    try_files $uri @liberama;
+  }
 
 
-    location /upload {
-      try_files $uri @liberama;
-    }
+  location /upload {
+    root /home/liberama/.liberama/public-files;
+
+    try_files $uri @liberama;
+  }
+
+  location / {
+    root /home/liberama/.liberama/public;
 
 
     location ~* \.(?:manifest|appcache|html)$ {
     location ~* \.(?:manifest|appcache|html)$ {
       expires -1;
       expires -1;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 259 - 269
package-lock.json


+ 43 - 37
package.json

@@ -1,6 +1,6 @@
 {
 {
-  "name": "Liberama",
-  "version": "0.12.2",
+  "name": "liberama",
+  "version": "1.0.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",
@@ -8,77 +8,83 @@
     "node": ">=16.16.0"
     "node": ">=16.16.0"
   },
   },
   "scripts": {
   "scripts": {
-    "dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
+    "dev": "nodemon --inspect --ignore server/.liberama --ignore client --exec 'node server'",
     "build:client": "webpack --config build/webpack.prod.config.js",
     "build:client": "webpack --config build/webpack.prod.config.js",
-    "build:linux": "npm run build:client && node build/linux && pkg -t node16-linux-x64 -C GZip -o dist/linux/liberama .",
-    "build:win": "npm run build:client && node build/win && pkg -t node16-win-x64 -C GZip -o dist/win/liberama .",
+    "build:linux": "npm run build:client && node build/prepkg.js linux && pkg -t node16-linux-x64 -C GZip -o dist/linux/liberama .",
+    "build:linux-arm64": "npm run build:client && node build/prepkg.js linux-arm64 && pkg -t node16-linuxstatic-arm64 -C GZip -o dist/linux-arm64/liberama .",
+    "build:win": "npm run build:client && node build/prepkg.js win && pkg -t node16-win-x64 -C GZip -o dist/win/liberama .",
+    "build:macos": "npm run build:client && node build/prepkg.js macos && pkg -t node16-macos-x64 -C GZip -o dist/macos/liberama .",
     "lint": "eslint --ext=.js,.vue client server",
     "lint": "eslint --ext=.js,.vue client server",
     "build:client-dev": "webpack --config build/webpack.dev.config.js",
     "build:client-dev": "webpack --config build/webpack.dev.config.js",
-    "postinstall": "npm run build:client-dev && node build/linux"
+    "build:all": "npm run build:linux && npm run build:win && npm run build:macos && npm run build:linux-arm64",
+    "release": "npm run build:all && node build/release.js",
+    "postinstall": "npm run build:client-dev"
   },
   },
   "bin": "server/index.js",
   "bin": "server/index.js",
   "pkg": {
   "pkg": {
     "scripts": "server/config/*.js"
     "scripts": "server/config/*.js"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@babel/core": "^7.18.13",
-    "@babel/eslint-parser": "^7.18.9",
-    "@babel/eslint-plugin": "^7.18.10",
-    "@babel/plugin-proposal-decorators": "^7.18.10",
-    "@babel/preset-env": "^7.18.10",
+    "@babel/core": "^7.20.5",
+    "@babel/eslint-parser": "^7.19.1",
+    "@babel/eslint-plugin": "^7.19.1",
+    "@babel/plugin-proposal-decorators": "^7.20.5",
+    "@babel/preset-env": "^7.20.2",
     "@vue/compiler-sfc": "^3.2.22",
     "@vue/compiler-sfc": "^3.2.22",
-    "babel-loader": "^8.2.5",
+    "babel-loader": "^9.1.0",
     "copy-webpack-plugin": "^11.0.0",
     "copy-webpack-plugin": "^11.0.0",
-    "css-loader": "^6.7.1",
-    "css-minimizer-webpack-plugin": "^4.0.0",
-    "eslint": "^8.23.0",
-    "eslint-plugin-vue": "^9.4.0",
+    "css-loader": "^6.7.3",
+    "css-minimizer-webpack-plugin": "^4.2.2",
+    "eslint": "^8.29.0",
+    "eslint-plugin-vue": "^9.8.0",
     "html-webpack-plugin": "^5.5.0",
     "html-webpack-plugin": "^5.5.0",
-    "mini-css-extract-plugin": "^2.6.1",
+    "mini-css-extract-plugin": "^2.7.2",
     "pkg": "^5.8.0",
     "pkg": "^5.8.0",
+    "showdown": "^2.1.0",
     "terser-webpack-plugin": "^5.3.6",
     "terser-webpack-plugin": "^5.3.6",
-    "vue-eslint-parser": "^9.0.3",
-    "vue-loader": "^17.0.0",
+    "vue-eslint-parser": "^9.1.0",
+    "vue-loader": "^17.0.1",
     "vue-style-loader": "^4.1.3",
     "vue-style-loader": "^4.1.3",
-    "webpack": "^5.74.0",
-    "webpack-cli": "^4.10.0",
-    "webpack-dev-middleware": "^5.3.3",
-    "webpack-hot-middleware": "^2.25.2",
+    "webpack": "^5.75.0",
+    "webpack-cli": "^5.0.1",
+    "webpack-dev-middleware": "^6.0.1",
+    "webpack-hot-middleware": "^2.25.3",
     "webpack-merge": "^5.8.0",
     "webpack-merge": "^5.8.0",
     "workbox-webpack-plugin": "^6.5.4"
     "workbox-webpack-plugin": "^6.5.4"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@quasar/extras": "^1.15.2",
-    "@vue/compat": "^3.2.38",
+    "@quasar/extras": "^1.15.8",
+    "@vue/compat": "^3.2.45",
     "axios": "^0.27.2",
     "axios": "^0.27.2",
     "base-x": "^4.0.0",
     "base-x": "^4.0.0",
-    "chardet": "^1.4.0",
+    "chardet": "^1.5.0",
     "compression": "^1.7.4",
     "compression": "^1.7.4",
-    "express": "^4.18.1",
+    "dayjs": "^1.11.7",
+    "express": "^4.18.2",
     "fg-loadcss": "^3.1.0",
     "fg-loadcss": "^3.1.0",
     "fs-extra": "^10.1.0",
     "fs-extra": "^10.1.0",
     "he": "^1.2.0",
     "he": "^1.2.0",
     "iconv-lite": "^0.6.3",
     "iconv-lite": "^0.6.3",
-    "jembadb": "^4.2.0",
+    "jembadb": "^5.1.5",
     "localforage": "^1.10.0",
     "localforage": "^1.10.0",
     "lodash": "^4.17.21",
     "lodash": "^4.17.21",
-    "minimist": "^1.2.6",
+    "minimist": "^1.2.7",
     "multer": "^1.4.5-lts.1",
     "multer": "^1.4.5-lts.1",
-    "pako": "^2.0.4",
+    "pako": "^2.1.0",
     "path-browserify": "^1.0.1",
     "path-browserify": "^1.0.1",
-    "pidusage": "^3.0.0",
-    "quasar": "^2.7.7",
+    "pidusage": "^3.0.2",
+    "quasar": "^2.10.2",
     "safe-buffer": "^5.2.1",
     "safe-buffer": "^5.2.1",
-    "sanitize-html": "^2.7.1",
+    "sanitize-html": "^2.8.0",
     "sjcl": "^1.0.8",
     "sjcl": "^1.0.8",
     "tar-fs": "^2.1.1",
     "tar-fs": "^2.1.1",
     "unbzip2-stream": "^1.4.3",
     "unbzip2-stream": "^1.4.3",
     "vue": "^3.2.37",
     "vue": "^3.2.37",
-    "vue-router": "^4.1.5",
-    "vuex": "^4.0.2",
+    "vue-router": "^4.1.6",
+    "vuex": "^4.1.0",
     "vuex-persist": "^3.1.3",
     "vuex-persist": "^3.1.3",
-    "webdav": "^4.11.0",
-    "ws": "^8.8.1",
+    "webdav": "^4.11.2",
+    "ws": "^8.11.0",
     "zip-stream": "^4.1.0"
     "zip-stream": "^4.1.0"
   }
   }
 }
 }

+ 6 - 11
server/config/base.js

@@ -2,19 +2,14 @@ const path = require('path');
 const pckg = require('../../package.json');
 const pckg = require('../../package.json');
 
 
 const execDir = path.resolve(__dirname, '..');
 const execDir = path.resolve(__dirname, '..');
-const dataDir = `${execDir}/data`;
 
 
 module.exports = {
 module.exports = {
     branch: 'unknown',
     branch: 'unknown',
     version: pckg.version,
     version: pckg.version,
     name: pckg.name,
     name: pckg.name,
 
 
-    dataDir: dataDir,
-    tempDir: `${dataDir}/tmp`,
-    logDir: `${dataDir}/log`,
-    publicDir: `${execDir}/public`,
-    uploadDir: `${execDir}/public/upload`,
-    sharedDir: `${execDir}/public/shared`,
+    execDir,
+
     loggingEnabled: true,
     loggingEnabled: true,
 
 
     maxUploadFileSize: 50*1024*1024,//50Мб
     maxUploadFileSize: 50*1024*1024,//50Мб
@@ -27,13 +22,13 @@ module.exports = {
 
 
     jembaDb: [
     jembaDb: [
         {
         {
-            serverMode: ['reader', 'omnireader', 'liberama.top'],
+            serverMode: ['reader', 'omnireader', 'liberama'],
             dbName: 'app',
             dbName: 'app',
             thread: true,
             thread: true,
             openAll: true,
             openAll: true,
         },
         },
         {
         {
-            serverMode: ['reader', 'omnireader', 'liberama.top'],
+            serverMode: ['reader', 'omnireader', 'liberama'],
             dbName: 'reader-storage',
             dbName: 'reader-storage',
             thread: true,
             thread: true,
             openAll: true,
             openAll: true,
@@ -49,13 +44,13 @@ module.exports = {
     servers: [
     servers: [
         {
         {
             serverName: '1',
             serverName: '1',
-            mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
+            mode: 'reader', //'reader', 'omnireader', 'liberama', 'book_update_checker'
             ip: '0.0.0.0',
             ip: '0.0.0.0',
             port: '33080',
             port: '33080',
         },
         },
         /*{
         /*{
             serverName: '2',
             serverName: '2',
-            mode: 'book_update_checker', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
+            mode: 'book_update_checker',
             isHttps: true,
             isHttps: true,
             keysFile: 'server',
             keysFile: 'server',
             ip: '0.0.0.0',
             ip: '0.0.0.0',

+ 33 - 12
server/config/index.js

@@ -1,4 +1,5 @@
 const _ = require('lodash');
 const _ = require('lodash');
+const path = require('path');
 const fs = require('fs-extra');
 const fs = require('fs-extra');
 
 
 const branchFilename = __dirname + '/application_env';
 const branchFilename = __dirname + '/application_env';
@@ -29,7 +30,7 @@ class ConfigManager {
         return instance;
         return instance;
     }
     }
 
 
-    async init() {
+    async init(dataDir) {
         if (this.inited)
         if (this.inited)
             throw new Error('already inited');
             throw new Error('already inited');
 
 
@@ -44,10 +45,17 @@ class ConfigManager {
         process.env.NODE_ENV = this.branch;
         process.env.NODE_ENV = this.branch;
 
 
         this.branchConfigFile = __dirname + `/${this.branch}.js`;
         this.branchConfigFile = __dirname + `/${this.branch}.js`;
-        this._config = require(this.branchConfigFile);
+        const config = require(this.branchConfigFile);
 
 
-        await fs.ensureDir(this._config.dataDir);
-        this._userConfigFile = `${this._config.dataDir}/config.json`;
+        if (dataDir) {
+            config.dataDir = path.resolve(dataDir);
+        } else {
+            config.dataDir = `${config.execDir}/.${config.name}`;
+        }
+
+        await fs.ensureDir(config.dataDir);
+        this._userConfigFile = `${config.dataDir}/config.json`;
+        this._config = config;
 
 
         this.inited = true;
         this.inited = true;
     }
     }
@@ -72,15 +80,28 @@ class ConfigManager {
     }
     }
 
 
     async load() {
     async load() {
-        if (!this.inited)
-            throw new Error('not inited');
-        if (!await fs.pathExists(this.userConfigFile)) {
-            await this.save();
-            return;
+        try {
+            if (!this.inited)
+                throw new Error('not inited');
+
+            if (await fs.pathExists(this.userConfigFile)) {
+                const data = JSON.parse(await fs.readFile(this.userConfigFile, 'utf8'));
+                const config = _.pick(data, propsToSave);
+
+                this.config = config;
+
+                //сохраним конфиг, если не все атрибуты присутствуют в файле конфига
+                for (const prop of propsToSave)
+                    if (!Object.prototype.hasOwnProperty.call(config, prop)) {
+                        await this.save();
+                        break;
+                    }
+            } else {
+                await this.save();
+            }
+        } catch(e) {
+            throw new Error(`Error while loading "${this.userConfigFile}": ${e.message}`);
         }
         }
-
-        const data = await fs.readFile(this.userConfigFile, 'utf8');
-        this.config = JSON.parse(data);
     }
     }
 
 
     async save() {
     async save() {

+ 3 - 8
server/config/production.js

@@ -2,21 +2,16 @@ const path = require('path');
 const base = require('./base');
 const base = require('./base');
 
 
 const execDir = path.dirname(process.execPath);
 const execDir = path.dirname(process.execPath);
-const dataDir = `${execDir}/data`;
 
 
 module.exports = Object.assign({}, base, {
 module.exports = Object.assign({}, base, {
     branch: 'production',
     branch: 'production',
-    dataDir: dataDir,
-    tempDir: `${dataDir}/tmp`,
-    logDir: `${dataDir}/log`,
-    publicDir: `${execDir}/public`,
-    uploadDir: `${execDir}/public/upload`,
-    sharedDir: `${execDir}/public/shared`,
+
+    execDir,
 
 
     servers: [
     servers: [
         {
         {
             serverName: '1',
             serverName: '1',
-            mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader'
+            mode: 'reader',
             ip: '0.0.0.0',
             ip: '0.0.0.0',
             port: '44080',
             port: '44080',
         },
         },

+ 18 - 0
server/controllers/WebSocketController.js

@@ -71,6 +71,8 @@ class WebSocketController {
                     await this.test(req, ws); break;
                     await this.test(req, ws); break;
                 case 'get-config':
                 case 'get-config':
                     await this.getConfig(req, ws); break;
                     await this.getConfig(req, ws); break;
+                case 'load-book':
+                    await this.loadBook(req, ws); break;
                 case 'worker-get-state':
                 case 'worker-get-state':
                     await this.workerGetState(req, ws); break;
                     await this.workerGetState(req, ws); break;
                 case 'worker-get-state-finish':
                 case 'worker-get-state-finish':
@@ -124,6 +126,22 @@ class WebSocketController {
         }
         }
     }
     }
 
 
+    async loadBook(req, ws) {
+        const workerId = this.readerWorker.loadBookUrl({
+            url: req.url, 
+            enableSitesFilter: (_.has(req, 'enableSitesFilter') ? req.enableSitesFilter : true),
+            skipHtmlCheck: (_.has(req, 'skipHtmlCheck') ? req.skipHtmlCheck : false),
+            isText: (_.has(req, 'isText') ? req.isText : false),
+            uploadFileName: (_.has(req, 'uploadFileName') ? req.uploadFileName : false),
+            djvuQuality: (_.has(req, 'djvuQuality') ? req.djvuQuality : false),
+            pdfAsText: (_.has(req, 'pdfAsText') ? req.pdfAsText : false),
+            pdfQuality: (_.has(req, 'pdfQuality') ? req.pdfQuality : false),
+        });
+        const state = this.workerState.getState(workerId);
+
+        this.send((state ? state : {}), req, ws);
+    }
+
     async workerGetState(req, ws) {
     async workerGetState(req, ws) {
         if (!req.workerId)
         if (!req.workerId)
             throw new Error(`key 'workerId' is wrong`);
             throw new Error(`key 'workerId' is wrong`);

+ 4 - 0
server/core/AppLogger.js

@@ -37,6 +37,10 @@ class AppLogger {
                 {log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
                 {log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
                 {log: 'FileLog', fileName: this.fatalLogFileName, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
                 {log: 'FileLog', fileName: this.fatalLogFileName, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
             ];
             ];
+        } else {
+            loggerParams = [
+                {log: 'ConsoleLog'},
+            ];
         }
         }
 
 
         this._logger = new Logger(loggerParams);
         this._logger = new Logger(loggerParams);

+ 6 - 2
server/core/AsyncExit.js

@@ -1,9 +1,9 @@
-let instance = null;
-
 const defaultTimeout = 15*1000;//15 sec
 const defaultTimeout = 15*1000;//15 sec
 const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException'];
 const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException'];
 
 
 //singleton
 //singleton
+let instance = null;
+
 class AsyncExit {
 class AsyncExit {
     constructor(signals = exitSignals, codeOnSignal = 2) {
     constructor(signals = exitSignals, codeOnSignal = 2) {
         if (!instance) {
         if (!instance) {
@@ -22,6 +22,10 @@ class AsyncExit {
 
 
     _init(signals, codeOnSignal) {
     _init(signals, codeOnSignal) {
         const runSingalCallbacks = async(signal, err, origin) => {
         const runSingalCallbacks = async(signal, err, origin) => {
+            if (!this.onSignalCallbacks.size) {
+                console.error(`Uncaught signal "${signal}" received, error: "${(err.stack ? err.stack : err)}"`);
+            }
+
             for (const signalCallback of this.onSignalCallbacks.keys()) {
             for (const signalCallback of this.onSignalCallbacks.keys()) {
                 try {
                 try {
                     await signalCallback(signal, err, origin);
                     await signalCallback(signal, err, origin);

+ 5 - 2
server/core/FileDownloader.js

@@ -9,11 +9,12 @@ class FileDownloader {
         this.limitDownloadSize = limitDownloadSize;
         this.limitDownloadSize = limitDownloadSize;
     }
     }
 
 
-    async load(url, callback, abort) {
+    async load(url, opts, callback, abort) {
         let errMes = '';
         let errMes = '';
 
 
-        const options = {
+        let options = {
             headers: {
             headers: {
+                'accept-encoding': 'gzip, compress, deflate',
                 'user-agent': userAgent,
                 'user-agent': userAgent,
                 timeout: 300*1000,
                 timeout: 300*1000,
             },
             },
@@ -22,6 +23,8 @@ class FileDownloader {
             }),
             }),
             responseType: 'stream',
             responseType: 'stream',
         };
         };
+        if (opts)
+            options = Object.assign({}, opts, options);
 
 
         try {
         try {
             const res = await axios.get(url, options);
             const res = await axios.get(url, options);

+ 24 - 10
server/core/Logger.js

@@ -48,8 +48,12 @@ class BaseLog {
         this.outputBufferLength = 0;
         this.outputBufferLength = 0;
         this.outputBuffer = [];
         this.outputBuffer = [];
 
 
-        await this.flushImpl(this.data)
-            .catch(e => { console.error(`Logger error: ${e}`); ayncExit.exit(1); } );
+        try {
+            await this.flushImpl(this.data);
+        } catch (e) {
+            console.error(`Logger error: ${e}`);
+            ayncExit.exit(1);
+        }
         this.flushing = false;
         this.flushing = false;
     }
     }
 
 
@@ -112,10 +116,14 @@ class FileLog extends BaseLog {
         if (this.closed)
         if (this.closed)
             return;
             return;
         await super.close();
         await super.close();
+
         if (this.fd) {
         if (this.fd) {
+            while (this.flushing)
+                await sleep(1);
             await fs.close(this.fd);
             await fs.close(this.fd);
             this.fd = null;
             this.fd = null;
         }
         }
+
         if (this.rcid)
         if (this.rcid)
             clearTimeout(this.rcid);
             clearTimeout(this.rcid);
     }
     }
@@ -151,15 +159,21 @@ class FileLog extends BaseLog {
         if (this.closed)
         if (this.closed)
             return;
             return;
 
 
-        if (!this.rcid) {
-            await this.doFileRotationIfNeeded();
-            this.rcid = setTimeout(() => {
-                this.rcid = 0;
-            }, LOG_ROTATE_FILE_CHECK_INTERVAL);
-        }
+        this.flushing = true;
+        try {
+            if (!this.rcid) {
+                await this.doFileRotationIfNeeded();
+                this.rcid = setTimeout(() => {
+                    this.rcid = 0;
+                }, LOG_ROTATE_FILE_CHECK_INTERVAL);
+            }
 
 
-        if (this.fd)
-            await fs.write(this.fd, Buffer.from(data.join('')));
+            if (this.fd) {
+                await fs.write(this.fd, Buffer.from(data.join('')));
+            }
+        } finally {
+            this.flushing = false;
+        }
     }
     }
 }
 }
 
 

+ 55 - 14
server/core/Reader/JembaReaderStorage.js

@@ -12,6 +12,8 @@ class JembaReaderStorage {
         if (!instance) {
         if (!instance) {
             this.connManager = new JembaConnManager();
             this.connManager = new JembaConnManager();
             this.db = this.connManager.db['reader-storage'];
             this.db = this.connManager.db['reader-storage'];
+
+            this.cacheMap = new Map();
             this.periodicCleanCache(3*3600*1000);//1 раз в 3 часа
             this.periodicCleanCache(3*3600*1000);//1 раз в 3 часа
 
 
             instance = this;
             instance = this;
@@ -20,6 +22,21 @@ class JembaReaderStorage {
         return instance;
         return instance;
     }
     }
 
 
+    getCache(id) {
+        const obj = this.cacheMap.get(id);
+        if (obj)
+            obj.time = Date.now();
+        return obj;
+    }
+
+    setCache(id, newObj) {
+        let obj = this.cacheMap.get(id);
+        if (!obj)
+            obj = {};
+        Object.assign(obj, newObj, {time: Date.now()});
+        this.cacheMap.set(id, obj);
+    }
+
     async doAction(act) {
     async doAction(act) {
         try {
         try {
             if (!_.isObject(act.items))
             if (!_.isObject(act.items))
@@ -34,7 +51,7 @@ class JembaReaderStorage {
                     result = await this.getItems(act.items);
                     result = await this.getItems(act.items);
                     break;
                     break;
                 case 'set':
                 case 'set':
-                    result = await this.setItems(act.items, act.force);
+                    result = await this.setItems(act.items, act.identity, act.force);
                     break;
                     break;
                 default:
                 default:
                     throw new Error('Unknown action');
                     throw new Error('Unknown action');
@@ -53,8 +70,9 @@ class JembaReaderStorage {
         const db = this.db;
         const db = this.db;
 
 
         for (const id of Object.keys(items)) {
         for (const id of Object.keys(items)) {
-            if (this.cache[id]) {
-                result.items[id] = this.cache[id];
+            const obj = this.getCache(id);
+            if (obj && obj.items) {
+                result.items[id] = obj.items;
             } else {
             } else {
                 const rows = await db.select({//SQL`SELECT rev FROM storage WHERE id = ${id}`
                 const rows = await db.select({//SQL`SELECT rev FROM storage WHERE id = ${id}`
                     table: 'storage',
                     table: 'storage',
@@ -63,7 +81,8 @@ class JembaReaderStorage {
                 });
                 });
                 const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
                 const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
                 result.items[id] = {rev};
                 result.items[id] = {rev};
-                this.cache[id] = result.items[id];
+
+                this.setCache(id, {items: result.items[id]});
             }
             }
         }
         }
 
 
@@ -88,7 +107,7 @@ class JembaReaderStorage {
         return result;
         return result;
     }
     }
 
 
-    async setItems(items, force) {
+    async setItems(items, identity, force) {
         let check = await this.checkItems(items);
         let check = await this.checkItems(items);
 
 
         //сначала проверим совпадение ревизий
         //сначала проверим совпадение ревизий
@@ -96,32 +115,54 @@ class JembaReaderStorage {
             if (!_.isString(items[id].data))
             if (!_.isString(items[id].data))
                 throw new Error('items.data is not a string');
                 throw new Error('items.data is not a string');
 
 
-            if (!force && check.items[id].rev + 1 !== items[id].rev)
+            //identity необходимо для работы при нестабильной связи,
+            //одному и тому же клиенту разрешается перезаписывать данные при расхождении на 0 или 1 ревизию
+            const obj = this.getCache(id) || {};
+            const sameClient = (identity && obj.identity === identity);
+            if (identity && obj.identity !== identity) {
+                obj.identity = identity;
+                this.setCache(id, obj);
+            }
+
+            const revDiff = items[id].rev - check.items[id].rev;
+            const allowUpdate = force || revDiff === 1 || (sameClient && (revDiff === 0 || revDiff === 1));
+            if (!allowUpdate)
                 return {state: 'reject', items: check.items};
                 return {state: 'reject', items: check.items};
         }
         }
 
 
         const db = this.db;
         const db = this.db;
-        const newRev = {};
         for (const id of Object.keys(items)) {
         for (const id of Object.keys(items)) {
             await db.insert({//SQL`INSERT OR REPLACE INTO storage (id, rev, time, data) VALUES (${id}, ${items[id].rev}, strftime('%s','now'), ${items[id].data})`);
             await db.insert({//SQL`INSERT OR REPLACE INTO storage (id, rev, time, data) VALUES (${id}, ${items[id].rev}, strftime('%s','now'), ${items[id].data})`);
                 table: 'storage',
                 table: 'storage',
                 replace: true,
                 replace: true,
                 rows: [{id, rev: items[id].rev, time: utils.toUnixTime(Date.now()), data: items[id].data}],
                 rows: [{id, rev: items[id].rev, time: utils.toUnixTime(Date.now()), data: items[id].data}],
             });
             });
-            newRev[id] = {rev: items[id].rev};
+            this.setCache(id, {items: {rev: items[id].rev}});
         }
         }
         
         
-        Object.assign(this.cache, newRev);
-
         return {state: 'success'};
         return {state: 'success'};
     }
     }
 
 
     periodicCleanCache(timeout) {
     periodicCleanCache(timeout) {
-        this.cache = {};
+        try {
+            const sorted = [];
+            for (const [id, obj] of this.cacheMap)
+                sorted.push({id, time: obj.time});
 
 
-        setTimeout(() => {
-            this.periodicCleanCache(timeout);
-        }, timeout);
+            sorted.sort((a, b) => b.time - a.time);
+
+            for (const obj of sorted) {
+                //оставляем только 1000 недавних
+                if (this.cacheMap.size <= 1000)
+                    break;
+
+                this.cacheMap.delete(obj.id);
+            }
+        } finally {
+            setTimeout(() => {
+                this.periodicCleanCache(timeout);
+            }, timeout);
+        }
     }
     }
 }
 }
 
 

+ 6 - 9
server/core/Reader/ReaderWorker.js

@@ -29,9 +29,6 @@ class ReaderWorker {
             this.config.tempDownloadDir = `${config.tempDir}/download`;
             this.config.tempDownloadDir = `${config.tempDir}/download`;
             fs.ensureDirSync(this.config.tempDownloadDir);
             fs.ensureDirSync(this.config.tempDownloadDir);
 
 
-            this.config.tempPublicDir = `${config.publicDir}/tmp`;
-            fs.ensureDirSync(this.config.tempPublicDir);
-
             this.workerState = new WorkerState();
             this.workerState = new WorkerState();
             this.down = new FileDownloader(config.maxUploadFileSize);
             this.down = new FileDownloader(config.maxUploadFileSize);
             this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
             this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
@@ -55,7 +52,7 @@ class ReaderWorker {
                     moveToRemote: true,
                     moveToRemote: true,
                 },
                 },
                 {
                 {
-                    dir: this.config.uploadDir,
+                    dir: this.config.uploadPublicDir,
                     remoteDir: '/upload',
                     remoteDir: '/upload',
                     maxSize: this.config.maxUploadPublicDirSize,
                     maxSize: this.config.maxUploadPublicDirSize,
                     moveToRemote: true,
                     moveToRemote: true,
@@ -109,7 +106,7 @@ class ReaderWorker {
             let downloadSize = -1;
             let downloadSize = -1;
             //download or use uploaded
             //download or use uploaded
             if (url.indexOf('disk://') != 0) {//download
             if (url.indexOf('disk://') != 0) {//download
-                const downdata = await this.down.load(url, (progress) => {
+                const downdata = await this.down.load(url, {}, (progress) => {
                     wState.set({progress});
                     wState.set({progress});
                 }, q.abort);
                 }, q.abort);
 
 
@@ -119,7 +116,7 @@ class ReaderWorker {
                 await fs.writeFile(downloadedFilename, downdata);
                 await fs.writeFile(downloadedFilename, downdata);
             } else {//uploaded file
             } else {//uploaded file
                 const fileHash = url.substr(7);
                 const fileHash = url.substr(7);
-                downloadedFilename = `${this.config.uploadDir}/${fileHash}`;
+                downloadedFilename = `${this.config.uploadPublicDir}/${fileHash}`;
                 if (!await fs.pathExists(downloadedFilename)) {
                 if (!await fs.pathExists(downloadedFilename)) {
                     //если удалено из upload, попробуем восстановить из удаленного хранилища
                     //если удалено из upload, попробуем восстановить из удаленного хранилища
                     try {
                     try {
@@ -227,7 +224,7 @@ class ReaderWorker {
 
 
     async saveFile(file) {
     async saveFile(file) {
         const hash = await utils.getFileHash(file.path, 'sha256', 'hex');
         const hash = await utils.getFileHash(file.path, 'sha256', 'hex');
-        const outFilename = `${this.config.uploadDir}/${hash}`;
+        const outFilename = `${this.config.uploadPublicDir}/${hash}`;
 
 
         if (!await fs.pathExists(outFilename)) {
         if (!await fs.pathExists(outFilename)) {
             await fs.move(file.path, outFilename);
             await fs.move(file.path, outFilename);
@@ -242,7 +239,7 @@ class ReaderWorker {
 
 
     async saveFileBuf(buf) {
     async saveFileBuf(buf) {
         const hash = await utils.getBufHash(buf, 'sha256', 'hex');
         const hash = await utils.getBufHash(buf, 'sha256', 'hex');
-        const outFilename = `${this.config.uploadDir}/${hash}`;
+        const outFilename = `${this.config.uploadPublicDir}/${hash}`;
 
 
         if (!await fs.pathExists(outFilename)) {
         if (!await fs.pathExists(outFilename)) {
             await fs.writeFile(outFilename, buf);
             await fs.writeFile(outFilename, buf);
@@ -255,7 +252,7 @@ class ReaderWorker {
     }
     }
 
 
     async uploadFileTouch(url) {
     async uploadFileTouch(url) {
-        const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
+        const outFilename = `${this.config.uploadPublicDir}/${url.replace('disk://', '')}`;
 
 
         await utils.touchFile(outFilename);
         await utils.touchFile(outFilename);
 
 

+ 59 - 0
server/core/Zip/ZipReader.js

@@ -0,0 +1,59 @@
+const StreamUnzip = require('./node_stream_zip_changed');
+//const StreamUnzip = require('node-stream-zip');
+
+class ZipReader {
+    constructor() {
+        this.zip = null;
+    }
+
+    checkState() {
+        if (!this.zip)
+            throw new Error('Zip closed');
+    }
+
+    async open(zipFile, zipEntries = true) {
+        if (this.zip)
+            throw new Error('Zip file is already open');
+
+         const zip = new StreamUnzip.async({file: zipFile, skipEntryNameValidation: true});
+         
+        if (zipEntries)
+            this.zipEntries = await zip.entries();
+
+         this.zip = zip;
+    }
+
+    get entries() {
+        this.checkState();
+
+        return this.zipEntries;
+    }
+
+    async extractToBuf(entryFilePath) {
+        this.checkState();
+
+        return await this.zip.entryData(entryFilePath);
+    }
+
+    async extractToFile(entryFilePath, outputFile) {
+        this.checkState();
+
+        await this.zip.extract(entryFilePath, outputFile);
+    }
+
+    async extractAllToDir(outputDir) {
+        this.checkState();
+
+        await this.zip.extract(null, outputDir);
+    }
+
+    async close() {
+        if (this.zip) {
+            await this.zip.close();
+            this.zip = null;
+            this.zipEntries = undefined;
+        }
+    }
+}
+
+module.exports = ZipReader;

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

@@ -2,7 +2,7 @@
 const path = require('path');
 const path = require('path');
 
 
 const zipStream = require('zip-stream');*/
 const zipStream = require('zip-stream');*/
-const unzipStream = require('./node_stream_zip');
+const StreamUnzip = require('./node_stream_zip_changed');
 
 
 class ZipStreamer {
 class ZipStreamer {
     constructor() {
     constructor() {
@@ -63,7 +63,7 @@ class ZipStreamer {
                 decodeEntryNameCallback = false,
                 decodeEntryNameCallback = false,
             } = options;
             } = options;
 
 
-            const unzip = new unzipStream({file: zipFile});
+            const unzip = new StreamUnzip({file: zipFile, skipEntryNameValidation: true});
 
 
             unzip.on('error', reject);
             unzip.on('error', reject);
 
 

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.