Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98a44def2 | ||
|
|
c6e972b165 | ||
|
|
7b7146b502 | ||
|
|
f00700cb41 | ||
|
|
c3e099f095 | ||
|
|
6393c24575 | ||
|
|
17378f3686 | ||
|
|
d7453302f7 | ||
|
|
07f5146534 | ||
|
|
d04851af72 | ||
|
|
6aff0eb4e6 | ||
|
|
2f5409b485 | ||
|
|
3aa7dc32d3 | ||
|
|
f5cd6ebdbc | ||
|
|
a7289cda74 | ||
|
|
ada3a3b4fd | ||
|
|
a21e216eb9 | ||
|
|
b85fe7f219 | ||
|
|
4efb3031de | ||
|
|
6b66acb2cf | ||
|
|
481e1e840e | ||
|
|
e296b49821 | ||
|
|
254118f845 | ||
|
|
88f5a98c55 | ||
|
|
572a5dd200 | ||
|
|
8dce00db44 | ||
|
|
0ab73deffd | ||
|
|
9863dc6dd0 | ||
|
|
797f93d467 | ||
|
|
c602f3d531 | ||
|
|
dfd45a58bd | ||
|
|
70a832530e | ||
|
|
4fc32eafd7 | ||
|
|
6579d34b90 | ||
|
|
a5bf8f88cd | ||
|
|
55264314b8 | ||
|
|
23a9e9154b | ||
|
|
0ee373c1f3 | ||
|
|
29b40bc91d | ||
|
|
10b7363b06 | ||
|
|
e37f15975d | ||
|
|
ce0f61c543 | ||
|
|
ea62abfc9a | ||
|
|
15a2b6ba7e | ||
|
|
10773526e4 | ||
|
|
facd7f1414 | ||
|
|
29bf80108d | ||
|
|
00bbb56ec6 | ||
|
|
2e057f5c96 | ||
|
|
936fa6a172 | ||
|
|
5d5ad40f4e | ||
|
|
55ee303fc5 | ||
|
|
f30f11ce2d | ||
|
|
f5e57b3319 | ||
|
|
d5fe4f8eb4 | ||
|
|
4f4f226d8c | ||
|
|
5b7712c274 | ||
|
|
8da71a98da | ||
|
|
f9fc59718a | ||
|
|
9bc4c3201c | ||
|
|
eb4ea0cc9c | ||
|
|
4b2e63bb5b | ||
|
|
817f018d4d | ||
|
|
9160b4ef90 | ||
|
|
e8d1817566 | ||
|
|
419b203fcf | ||
|
|
528b32ccf7 | ||
|
|
bc0c9932c8 | ||
|
|
5827d7a246 | ||
|
|
5dd08c43a6 | ||
|
|
13c5fc244a | ||
|
|
b8b52fe662 | ||
|
|
f4c0a48868 | ||
|
|
78b98e77c6 | ||
|
|
8cbaf60755 | ||
|
|
62ac60887e | ||
|
|
fe6243e889 | ||
|
|
8abd8ecaab | ||
|
|
c860422a5a | ||
|
|
083151460a | ||
|
|
c8f97ef386 | ||
|
|
c9a22a5eaf | ||
|
|
f926732070 | ||
|
|
3fbe6e9d9b | ||
|
|
225230381f | ||
|
|
b58d3a1b8b | ||
|
|
ffedce4351 | ||
|
|
a4fdb67913 | ||
|
|
6ba46421b9 | ||
|
|
d201961046 | ||
|
|
614a7f9da7 | ||
|
|
113ab3e596 | ||
|
|
c95870bfe5 | ||
|
|
e69e9335f9 | ||
|
|
fd21cd77dd | ||
|
|
d1880acaf9 | ||
|
|
428b507257 | ||
|
|
043dab0731 | ||
|
|
a7b4d9c0d8 | ||
|
|
6f9c95e351 | ||
|
|
7a53063ea8 | ||
|
|
ec4d5cac4f | ||
|
|
f8557cba88 | ||
|
|
5dead039f5 | ||
|
|
ea38392df4 | ||
|
|
0cc9d90a94 | ||
|
|
8c7b86c458 | ||
|
|
0e29546fc5 | ||
|
|
c9fa90d07c | ||
|
|
7d8e0525b1 | ||
|
|
ddf69876a6 | ||
|
|
1d78e75e38 | ||
|
|
7ed58fe3c6 | ||
|
|
058c79570b | ||
|
|
ec8fbcdf38 | ||
|
|
76673295bf | ||
|
|
084401b9c3 | ||
|
|
49038b10f7 | ||
|
|
45ea26810a | ||
|
|
18c8b2d803 | ||
|
|
f4a7482b3b | ||
|
|
32dff128f4 | ||
|
|
a00b2d6574 | ||
|
|
10c6e7d522 | ||
|
|
df6a256d51 | ||
|
|
fbdb74ee68 | ||
|
|
9ad7250da0 | ||
|
|
8c86984ea1 | ||
|
|
834b3f6210 | ||
|
|
105b8d5042 | ||
|
|
7ca8fd9ca1 | ||
|
|
0067c2800a | ||
|
|
688c8796f4 | ||
|
|
56af65742b | ||
|
|
629ad26d40 | ||
|
|
4b0e499c10 | ||
|
|
4697b46cba | ||
|
|
7f17e7daed | ||
|
|
a1fcb7597b | ||
|
|
35e46d0685 | ||
|
|
e2c0f3658b | ||
|
|
a3541ec16a | ||
|
|
08d0d3e7f3 | ||
|
|
2c47b2bee3 | ||
|
|
e6008b5ec4 | ||
|
|
e214ddf8d5 | ||
|
|
52927c6188 | ||
|
|
92ca9dd983 | ||
|
|
ed8be34c12 | ||
|
|
93bddfd05e | ||
|
|
8c99101bb3 | ||
|
|
d874f9ded4 | ||
|
|
d7be4d3d94 | ||
|
|
a2fa312839 | ||
|
|
f7e1e09928 | ||
|
|
f0832b07cb | ||
|
|
7c253df291 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/server/data
|
/server/.liberama*
|
||||||
/server/public
|
|
||||||
/server/ipfs
|
|
||||||
/dist
|
/dist
|
||||||
|
dev*.sh
|
||||||
|
|
||||||
|
|||||||
159
README.md
159
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)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
При запуске приложения, по умолчанию веб-сервер доступен по адресу [http://127.0.0.1:44080](http://127.0.0.1:44080)
|
||||||
|
|
||||||
|
Для указания местоположения рабочей директории, воспользуйтесь [параметрами командной строки](#cli).
|
||||||
|
Дополнительные параметры сервера настраиваются в [конфигурационном файле](#config).
|
||||||
|
|
||||||
|
[Отблагодарить автора проекта](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
|
||||||
|
|
||||||
|
По умолчанию веб-интерфейс будет доступен по адресу [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 Починить БД приложения при запуске, если она повреждена
|
||||||
|
```
|
||||||
|
|
||||||
|
<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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
При необходимости, можно настроить нужный параметр в этом файле вручную.
|
||||||
|
|
||||||
|
<a id="vps" />
|
||||||
|
|
||||||
## VPS
|
## VPS
|
||||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||||
|
|
||||||
## Сборка проекта
|
<a id="build" />
|
||||||
Необходима версия node.js не ниже 14.
|
|
||||||
|
|
||||||
```
|
### Сборка проекта
|
||||||
$ git clone https://github.com/bookpauk/liberama
|
Сборка только в среде Linux.
|
||||||
$ cd liberama
|
Необходима версия node.js не ниже 16.
|
||||||
$ npm i
|
|
||||||
|
Для сборки linux-arm64 необходимо предварительно установить [QEMU](https://wiki.debian.org/QemuUserEmulation).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/bookpauk/liberama
|
||||||
|
cd liberama
|
||||||
|
npm i
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows
|
#### Релизы
|
||||||
```
|
```sh
|
||||||
$ npm run build:win
|
npm run release
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linux
|
Результат сборки будет доступен в каталоге `dist/release`
|
||||||
```
|
|
||||||
$ npm run build:linux
|
|
||||||
```
|
|
||||||
|
|
||||||
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
|
<a id="development" />
|
||||||
|
|
||||||
### Разработка
|
### Разработка
|
||||||
```
|
```sh
|
||||||
$ npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Помочь проекту
|
Связаться с автором проекта: [bookpauk@gmail.com](mailto:bookpauk@gmail.com)
|
||||||
|
|
||||||
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85
|
|
||||||
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
|
|
||||||
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz
|
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -1,69 +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);
|
|
||||||
|
|
||||||
//sqlite3
|
|
||||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
|
|
||||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
|
|
||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
|
||||||
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
|
||||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
|
||||||
|
|
||||||
//распаковываем
|
|
||||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
|
||||||
console.log('files decompressed');
|
|
||||||
}
|
|
||||||
// копируем в дистрибутив
|
|
||||||
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
|
|
||||||
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
|
|
||||||
|
|
||||||
//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
build/prepkg.js
Normal file
51
build/prepkg.js
Normal file
@@ -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
build/release.js
Normal file
33
build/release.js
Normal file
@@ -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();
|
||||||
@@ -14,6 +14,7 @@ module.exports = {
|
|||||||
entry: [`${clientDir}/main.js`],
|
entry: [`${clientDir}/main.js`],
|
||||||
output: {
|
output: {
|
||||||
publicPath: '/app/',
|
publicPath: '/app/',
|
||||||
|
clean: true
|
||||||
},
|
},
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
@@ -29,10 +30,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
},
|
},
|
||||||
{
|
|
||||||
resourceQuery: /^\?vue/,
|
|
||||||
use: path.resolve(__dirname, 'includer.js')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
|
|||||||
@@ -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,15 +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',
|
||||||
},
|
},
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
@@ -38,6 +39,6 @@ module.exports = merge(baseWpConfig, {
|
|||||||
template: `${clientDir}/index.html.template`,
|
template: `${clientDir}/index.html.template`,
|
||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
|
new CopyWebpackPlugin({patterns: [{context: `${clientDir}/assets`, from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,8 +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',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
@@ -54,7 +54,7 @@ module.exports = merge(baseWpConfig, {
|
|||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin({patterns:
|
new CopyWebpackPlugin({patterns:
|
||||||
[{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
|
[{context: `${clientDir}/assets`, from: `${clientDir}/assets/*`, to: `${publicDir}/` }]
|
||||||
}),
|
}),
|
||||||
new GenerateSW({
|
new GenerateSW({
|
||||||
cacheId: 'liberama',
|
cacheId: 'liberama',
|
||||||
|
|||||||
63
build/win.js
63
build/win.js
@@ -1,63 +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);
|
|
||||||
|
|
||||||
//sqlite3
|
|
||||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
|
|
||||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
|
|
||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
|
||||||
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
|
||||||
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
|
||||||
|
|
||||||
//распаковываем
|
|
||||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
|
||||||
console.log('files decompressed');
|
|
||||||
}
|
|
||||||
// копируем в дистрибутив
|
|
||||||
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
|
|
||||||
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
|
|
||||||
|
|
||||||
//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();
|
|
||||||
@@ -1,29 +1,17 @@
|
|||||||
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() {
|
||||||
|
|
||||||
const query = {params: [
|
const query = {params: [
|
||||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch',
|
||||||
]};
|
]};
|
||||||
|
|
||||||
try {
|
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||||
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
|
if (config.error)
|
||||||
if (config.error)
|
throw new Error(config.error);
|
||||||
throw new Error(config.error);
|
|
||||||
return config;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
//если с WebSocket проблема, работаем по http
|
return config;
|
||||||
const response = await api.post('/config', query);
|
|
||||||
return response.data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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});
|
||||||
const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
|
|
||||||
|
|
||||||
let prevResponse = false;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
//если с WebSocket проблема, работаем по http
|
|
||||||
const refreshPause = 500;
|
|
||||||
let i = 0;
|
|
||||||
response = {};
|
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
const prevProgress = response.progress || 0;
|
response = await wsc.message(requestId);
|
||||||
const prevState = response.state || 0;
|
|
||||||
response = await workerApi.post('/get-state', {workerId});
|
|
||||||
response = response.data;
|
|
||||||
callback(response);
|
|
||||||
|
|
||||||
if (!response.state)
|
if (!response.state && prevResponse !== false) {//экономия траффика
|
||||||
throw new Error('Неверный ответ api');
|
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);
|
let response = await wsc.message(await wsc.send(Object.assign({action: 'load-book'}, opts)));
|
||||||
|
const workerId = response.workerId;
|
||||||
const workerId = response.data.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;
|
const response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
|
||||||
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 state = response.state;
|
if (response.error)
|
||||||
if (!state)
|
|
||||||
throw new Error('Неверный ответ api');
|
|
||||||
if (state == 'error') {
|
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
|
||||||
|
if (!response.state)
|
||||||
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -229,6 +185,17 @@ class Reader {
|
|||||||
return (await axios.get(url)).data;
|
return (await axios.get(url)).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkBuc(bookUrls) {
|
||||||
|
const response = await wsc.message(await wsc.send({action: 'check-buc', bookUrls}));
|
||||||
|
|
||||||
|
if (response.error)
|
||||||
|
throw new Error(response.error);
|
||||||
|
|
||||||
|
if (!response.data)
|
||||||
|
throw new Error(`response.data is empty`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Reader();
|
export default new Reader();
|
||||||
@@ -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>" и редирект при необходимости
|
||||||
@@ -238,7 +221,7 @@ class App {
|
|||||||
const url = s[1] || '';
|
const url = s[1] || '';
|
||||||
const q = utils.parseQuery(s[0] || '');
|
const q = utils.parseQuery(s[0] || '');
|
||||||
if (url) {
|
if (url) {
|
||||||
q.url = decodeURIComponent(url);
|
q.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.history.replaceState({}, '', '/');
|
window.history.replaceState({}, '', '/');
|
||||||
@@ -271,6 +254,10 @@ body, html, #app {
|
|||||||
font: normal 12pt ReaderDefault;
|
font: normal 12pt ReaderDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.q-notifications__list--top {
|
||||||
|
top: 55px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dborder {
|
.dborder {
|
||||||
border: 2px solid magenta !important;
|
border: 2px solid magenta !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Book в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../../vueComponent.js';
|
|
||||||
|
|
||||||
class Book {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Book);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Card в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../../vueComponent.js';
|
|
||||||
|
|
||||||
class Card {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Card);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел History в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../../vueComponent.js';
|
|
||||||
|
|
||||||
class History {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(History);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Search в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../../vueComponent.js';
|
|
||||||
|
|
||||||
class Search {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Search);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.libsDefaults = rstore.getLibsDefaults(this.mode);
|
||||||
this.$router.replace('/404');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
submitUrl(url) {
|
||||||
if (this.bookUrl) {
|
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.bookmarkLink = this.libsDefaults.groups[index].s;
|
||||||
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
|
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'});
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Help в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../vueComponent.js';
|
|
||||||
|
|
||||||
class Help {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Help);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Income в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../vueComponent.js';
|
|
||||||
|
|
||||||
class Income {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Income);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Страница не найдена
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../vueComponent.js';
|
|
||||||
|
|
||||||
class NotFound404 {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(NotFound404);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,70 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="box">
|
<div class="column items-center" style="width: 500px">
|
||||||
<p class="p">
|
<p class="p">
|
||||||
Вы можете пожертвовать на развитие проекта любую сумму:
|
Здесь вы можете пожертвовать на развитие проекта:
|
||||||
</p>
|
</p>
|
||||||
<div class="address">
|
|
||||||
<img class="logo" src="./assets/yoomoney.png">
|
|
||||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
|
|
||||||
Пожертвовать
|
|
||||||
</q-btn><br>
|
|
||||||
<div class="para">
|
|
||||||
{{ yooAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--div class="address">
|
<q-btn no-caps class="q-my-lg" color="green-8" size="14px" style="width: 200px" @click="makeDonation">
|
||||||
<img class="logo" src="./assets/paypal.png">
|
<q-icon class="q-mr-xs" name="la la-donate" size="24px" />
|
||||||
<div class="para">
|
Поддержать проект
|
||||||
{{ paypalAddress }}
|
</q-btn>
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div-->
|
|
||||||
|
|
||||||
<div class="address">
|
<div style="font-size: 60%">
|
||||||
<img class="logo" src="./assets/bitcoin.png">
|
* Ваш донат является подарком автору проекта
|
||||||
<div class="para">
|
|
||||||
{{ bitcoinAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address">
|
|
||||||
<img class="logo" src="./assets/litecoin.png">
|
|
||||||
<div class="para">
|
|
||||||
{{ litecoinAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="address">
|
|
||||||
<img class="logo" src="./assets/monero.png">
|
|
||||||
<div class="para">
|
|
||||||
{{ moneroAddress }}
|
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
|
||||||
Скопировать
|
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,28 +21,14 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import vueComponent from '../../../vueComponent.js';
|
import vueComponent from '../../../vueComponent.js';
|
||||||
|
|
||||||
import {copyTextToClipboard} from '../../../../share/utils';
|
import * as utils from '../../../../share/utils';
|
||||||
|
|
||||||
class DonateHelpPage {
|
class DonateHelpPage {
|
||||||
yooAddress = '410018702323056';
|
|
||||||
paypalAddress = 'bookpauk@gmail.com';
|
|
||||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
|
||||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
|
||||||
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
donateYooMoney() {
|
makeDonation() {
|
||||||
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
|
utils.makeDonation();
|
||||||
}
|
|
||||||
|
|
||||||
async copyAddress(address, prefix) {
|
|
||||||
const result = await copyTextToClipboard(address);
|
|
||||||
if (result)
|
|
||||||
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
|
||||||
else
|
|
||||||
this.$root.notify.error('Копирование не удалось');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,31 +49,4 @@ export default vueComponent(DonateHelpPage);
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
text-indent: 20px;
|
text-indent: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
|
||||||
max-width: 550px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address {
|
|
||||||
padding-top: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.para {
|
|
||||||
margin: 10px 10px 10px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 130px;
|
|
||||||
position: relative;
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
margin-left: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 120%;
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Window @close="close">
|
<Window @close="close" style="z-index: 200">
|
||||||
<template #header>
|
<template #header>
|
||||||
Справка
|
Справка
|
||||||
</template>
|
</template>
|
||||||
@@ -36,14 +36,14 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
|||||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||||
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||||
|
|
||||||
const pages = {
|
const pages = {
|
||||||
'CommonHelpPage': CommonHelpPage,
|
'CommonHelpPage': CommonHelpPage,
|
||||||
'HotkeysHelpPage': HotkeysHelpPage,
|
'HotkeysHelpPage': HotkeysHelpPage,
|
||||||
'MouseHelpPage': MouseHelpPage,
|
'MouseHelpPage': MouseHelpPage,
|
||||||
'VersionHistoryPage': VersionHistoryPage,
|
'VersionHistoryPage': VersionHistoryPage,
|
||||||
//'DonateHelpPage': DonateHelpPage,
|
'DonateHelpPage': DonateHelpPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -51,7 +51,7 @@ const tabs = [
|
|||||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||||
['HotkeysHelpPage', 'Клавиатура'],
|
['HotkeysHelpPage', 'Клавиатура'],
|
||||||
['VersionHistoryPage', 'История версий'],
|
['VersionHistoryPage', 'История версий'],
|
||||||
//['DonateHelpPage', 'Помочь проекту'],
|
['DonateHelpPage', 'Помочь проекту'],
|
||||||
];
|
];
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
@@ -80,7 +80,7 @@ class HelpPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activateDonateHelpPage() {
|
activateDonateHelpPage() {
|
||||||
//this.selectedTab = 'DonateHelpPage';
|
this.selectedTab = 'DonateHelpPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
activateVersionHistoryHelpPage() {
|
activateVersionHistoryHelpPage() {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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">
|
||||||
от центра вниз: плавный скроллинг
|
от центра вниз: плавный скроллинг
|
||||||
|
|||||||
@@ -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() {
|
async init() {
|
||||||
if (this.mode != 'liberama.top')
|
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}`;
|
||||||
|
|||||||
@@ -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,27 +55,14 @@
|
|||||||
</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>
|
||||||
|
|
||||||
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||||
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||||
</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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -1,133 +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 ref="buttons" class="row" :class="{'no-wrap': !toolBarMultiLine}">
|
||||||
<div class="row no-wrap">
|
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
||||||
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
<q-icon name="la la-arrow-left" size="32px" />
|
||||||
<q-icon name="la la-arrow-left" size="32px" />
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
{{ rstore.readerActions['loader'] }}
|
||||||
{{ rstore.readerActions['loader'] }}
|
</q-tooltip>
|
||||||
</q-tooltip>
|
</button>
|
||||||
</button>
|
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
|
||||||
<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-icon name="la la-caret-square-up" size="32px" />
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
{{ rstore.readerActions['loadFile'] }}
|
||||||
{{ rstore.readerActions['loadFile'] }}
|
</q-tooltip>
|
||||||
</q-tooltip>
|
</button>
|
||||||
</button>
|
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
|
||||||
<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-icon name="la la-comment" size="32px" />
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
{{ rstore.readerActions['loadBuffer'] }}
|
||||||
{{ rstore.readerActions['loadBuffer'] }}
|
</q-tooltip>
|
||||||
</q-tooltip>
|
</button>
|
||||||
</button>
|
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
|
||||||
<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-icon name="la la-question" size="32px" />
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
{{ rstore.readerActions['help'] }}
|
||||||
{{ rstore.readerActions['help'] }}
|
</q-tooltip>
|
||||||
</q-tooltip>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row no-wrap">
|
<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 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')">
|
|
||||||
<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">
|
<div class="space"></div>
|
||||||
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
|
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
||||||
<q-icon name="la la-mouse" size="32px" />
|
<q-icon name="la la-angle-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
{{ rstore.readerActions['clickControl'] }}
|
{{ rstore.readerActions['undoAction'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
<button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
|
||||||
<q-icon name="la la-unlink" size="32px" />
|
<q-icon name="la la-angle-right" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
{{ rstore.readerActions['offlineMode'] }}
|
{{ rstore.readerActions['redoAction'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
|
<div class="space"></div>
|
||||||
<q-icon name="la la-cog" size="32px" />
|
<button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
|
||||||
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">
|
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
|
||||||
{{ rstore.readerActions['settings'] }}
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
</q-tooltip>
|
{{ rstore.readerActions['fullScreen'] }}
|
||||||
</button>
|
</q-tooltip>
|
||||||
</div>
|
</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 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>
|
||||||
|
|
||||||
@@ -156,7 +161,7 @@
|
|||||||
></SearchPage>
|
></SearchPage>
|
||||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
||||||
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
||||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose" @update-count-changed="updateCountChanged"></RecentBooksPage>
|
||||||
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
||||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||||
@@ -286,7 +291,6 @@ class Reader {
|
|||||||
libsActive = false;
|
libsActive = false;
|
||||||
recentBooksActive = false;
|
recentBooksActive = false;
|
||||||
clickControlActive = false;
|
clickControlActive = false;
|
||||||
offlineModeActive = false;
|
|
||||||
settingsActive = false;
|
settingsActive = false;
|
||||||
|
|
||||||
clickMapActive = false;
|
clickMapActive = false;
|
||||||
@@ -299,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;
|
||||||
@@ -309,6 +315,10 @@ class Reader {
|
|||||||
donationVisible = false;
|
donationVisible = false;
|
||||||
dualPageMode = false;
|
dualPageMode = false;
|
||||||
|
|
||||||
|
bucEnabled = false;
|
||||||
|
bucSetOnNew = false;
|
||||||
|
needBookUpdateCount = 0;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.rstore = rstore;
|
this.rstore = rstore;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -357,6 +367,32 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
this.debouncedRecentBooksPageUpdate = _.debounce(async() => {
|
||||||
|
if (this.recentBooksActive) {
|
||||||
|
await this.$refs.recentBooksPage.updateTableData();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
this.recentItemKeys = [];
|
||||||
|
this.debouncedSaveRecent = _.debounce(async() => {
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
if (!this.offlineModeActive)
|
||||||
|
this.$root.notify.error('Таймаут соединения');
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const itemKeys = this.recentItemKeys;
|
||||||
|
this.recentItemKeys = [];
|
||||||
|
//сохранение в удаленном хранилище
|
||||||
|
await this.$refs.serverStorage.saveRecent(itemKeys);
|
||||||
|
} catch (e) {
|
||||||
|
if (!this.offlineModeActive)
|
||||||
|
this.$root.notify.error(e.message);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, 500, {maxWait: 1000});
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
});
|
});
|
||||||
@@ -394,16 +430,30 @@ class Reader {
|
|||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
|
|
||||||
await this.$refs.dialogs.init();
|
await this.$refs.dialogs.init();
|
||||||
|
|
||||||
|
this.$refs.recentBooksPage.init();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
//проверки обновлений читалки
|
||||||
(async() => {
|
(async() => {
|
||||||
this.isFirstNeedUpdateNotify = true;
|
this.isFirstNeedUpdateNotify = true;
|
||||||
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
|
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
|
||||||
while (true) {// eslint-disable-line no-constant-condition
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
await this.checkNewVersionAvailable();
|
await this.checkNewVersionAvailable();
|
||||||
await utils.sleep(3600*1000); //каждый час
|
await utils.sleep(60*60*1000); //каждый час
|
||||||
}
|
}
|
||||||
//дальше кода нет
|
//дальше хода нет
|
||||||
|
})();
|
||||||
|
|
||||||
|
//проверки обновлений книг
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(15*1000); //подождем неск. секунд перед первым запросом
|
||||||
|
//вечный цикл, запрашиваем периодически обновления
|
||||||
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
|
await this.checkBuc();
|
||||||
|
await utils.sleep(70*60*1000); //каждые 70 минут
|
||||||
|
}
|
||||||
|
//дальше хода нет
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,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;
|
||||||
@@ -425,6 +476,11 @@ class Reader {
|
|||||||
this.pdfQuality = settings.pdfQuality;
|
this.pdfQuality = settings.pdfQuality;
|
||||||
this.dualPageMode = settings.dualPageMode;
|
this.dualPageMode = settings.dualPageMode;
|
||||||
this.userWallpapers = settings.userWallpapers;
|
this.userWallpapers = settings.userWallpapers;
|
||||||
|
this.bucEnabled = settings.bucEnabled;
|
||||||
|
this.bucSizeDiff = settings.bucSizeDiff;
|
||||||
|
this.bucSetOnNew = settings.bucSetOnNew;
|
||||||
|
this.bucCancelEnabled = settings.bucCancelEnabled;
|
||||||
|
this.bucCancelDays = settings.bucCancelDays;
|
||||||
|
|
||||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||||
this.$root.readerActionByKeyEvent = (event) => {
|
this.$root.readerActionByKeyEvent = (event) => {
|
||||||
@@ -489,9 +545,7 @@ class Reader {
|
|||||||
|
|
||||||
//обновим settings, если загружали обои из /upload/
|
//обновим settings, если загружали обои из /upload/
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const newSettings = _.cloneDeep(this.settings);
|
this.commit('reader/setSettings', {});
|
||||||
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
|
|
||||||
this.commit('reader/setSettings', newSettings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicCss.replace('wallpapers', newCss);
|
dynamicCss.replace('wallpapers', newCss);
|
||||||
@@ -522,6 +576,92 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkBuc() {
|
||||||
|
if (!this.bothBucEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sorted = bookManager.getSortedRecent();
|
||||||
|
|
||||||
|
//выберем все кандидиаты на обновление
|
||||||
|
const updateUrls = new Set();
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (!book.deleted && book.checkBuc && book.url && book.url.indexOf('disk://') !== 0)
|
||||||
|
updateUrls.add(book.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
//теперь по кусочкам запросим сервер
|
||||||
|
const arr = Array.from(updateUrls);
|
||||||
|
const bucSize = {};
|
||||||
|
const chunkSize = 100;
|
||||||
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
|
const chunk = arr.slice(i, i + chunkSize);
|
||||||
|
|
||||||
|
const data = await readerApi.checkBuc(chunk);
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
bucSize[item.id] = item.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(1000);//чтобы не ддосить сервер
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSetTime = {};
|
||||||
|
//проставим новые размеры у книг
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (book.deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//размер 0 считаем отсутствующим
|
||||||
|
if (book.url && bucSize[book.url] && bucSize[book.url] !== book.bucSize) {
|
||||||
|
book.bucSize = bucSize[book.url];
|
||||||
|
await bookManager.recentSetItem(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
//подготовка к следующему шагу, ищем книгу по url с максимальной датой установки checkBucTime/loadTime
|
||||||
|
//от этой даты будем потом отсчитывать bucCancelDays
|
||||||
|
if (updateUrls.has(book.url)) {
|
||||||
|
let rec = checkSetTime[book.url] || {time: 0, loadTime: 0};
|
||||||
|
|
||||||
|
const time = (book.checkBucTime ? book.checkBucTime : (rec.loadTime || 0));
|
||||||
|
if (time > rec.time || (time == rec.time && (book.loadTime > rec.loadTime)))
|
||||||
|
rec = {time, loadTime: book.loadTime, key: book.key};
|
||||||
|
|
||||||
|
checkSetTime[book.url] = rec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//bucCancelEnabled и bucCancelDays
|
||||||
|
//снимем флаг checkBuc у необновлявшихся bucCancelDays
|
||||||
|
if (this.bucCancelEnabled) {
|
||||||
|
for (const rec of Object.values(checkSetTime)) {
|
||||||
|
if (rec.time && Date.now() - rec.time > this.bucCancelDays*24*3600*1000) {
|
||||||
|
const book = await bookManager.getRecentBook({key: rec.key});
|
||||||
|
const needBookUpdate =
|
||||||
|
book.checkBuc
|
||||||
|
&& book.bucSize
|
||||||
|
&& utils.hasProp(book, 'downloadSize')
|
||||||
|
&& book.bucSize !== book.downloadSize
|
||||||
|
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff)
|
||||||
|
;
|
||||||
|
|
||||||
|
if (book && !needBookUpdate) {
|
||||||
|
await bookManager.setCheckBuc(book, undefined);//!!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.$refs.recentBooksPage.updateTableData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCountChanged(event) {
|
||||||
|
this.needBookUpdateCount = event.needBookUpdateCount;
|
||||||
|
}
|
||||||
|
|
||||||
checkSetStorageAccessKey() {
|
checkSetStorageAccessKey() {
|
||||||
const q = this.$route.query;
|
const q = this.$route.query;
|
||||||
|
|
||||||
@@ -580,7 +720,7 @@ class Reader {
|
|||||||
return;
|
return;
|
||||||
const recent = this.mostRecentBook();
|
const recent = this.mostRecentBook();
|
||||||
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
||||||
const url = (recent ? `url=${recent.url}` : '');
|
const url = (recent ? `url=${encodeURIComponent(recent.url)}` : '');
|
||||||
if (isNewRoute)
|
if (isNewRoute)
|
||||||
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
|
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
|
||||||
else
|
else
|
||||||
@@ -600,6 +740,10 @@ class Reader {
|
|||||||
return versionHistory[0].version;
|
return versionHistory[0].version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get bothBucEnabled() {
|
||||||
|
return this.$store.state.config.bucEnabled && this.bucEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
let result = '';
|
let result = '';
|
||||||
const path = this.$route.fullPath;
|
const path = this.$route.fullPath;
|
||||||
@@ -648,27 +792,12 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventName == 'recent-changed') {
|
if (eventName == 'recent-changed') {
|
||||||
if (this.recentBooksActive) {
|
this.debouncedRecentBooksPageUpdate();
|
||||||
await this.$refs.recentBooksPage.updateTableData();
|
|
||||||
}
|
|
||||||
|
|
||||||
//сохранение в serverStorage
|
//сохранение в serverStorage
|
||||||
if (value) {
|
if (value && this.recentItemKeys.indexOf(value) < 0) {
|
||||||
await utils.sleep(500);
|
this.recentItemKeys.push(value);
|
||||||
|
this.debouncedSaveRecent();
|
||||||
let timer = setTimeout(() => {
|
|
||||||
if (!this.offlineModeActive)
|
|
||||||
this.$root.notify.error('Таймаут соединения');
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$refs.serverStorage.saveRecent(value);
|
|
||||||
} catch (e) {
|
|
||||||
if (!this.offlineModeActive)
|
|
||||||
this.$root.notify.error(e.message);
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -677,6 +806,10 @@ class Reader {
|
|||||||
return this.reader.toolBarActive;
|
return this.reader.toolBarActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get offlineModeActive() {
|
||||||
|
return this.reader.offlineModeActive;
|
||||||
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
const result = bookManager.mostRecentBook();
|
const result = bookManager.mostRecentBook();
|
||||||
this.mostRecentBookReactive = result;
|
this.mostRecentBookReactive = result;
|
||||||
@@ -707,8 +840,7 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fullScreenToggle() {
|
fullScreenToggle() {
|
||||||
this.fullScreenActive = !this.fullScreenActive;
|
if (!this.$q.fullscreen.isActive) {
|
||||||
if (this.fullScreenActive) {
|
|
||||||
this.$q.fullscreen.request();
|
this.$q.fullscreen.request();
|
||||||
} else {
|
} else {
|
||||||
this.$q.fullscreen.exit();
|
this.$q.fullscreen.exit();
|
||||||
@@ -876,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();
|
||||||
}
|
}
|
||||||
@@ -889,8 +1021,7 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
offlineModeToggle() {
|
offlineModeToggle() {
|
||||||
this.offlineModeActive = !this.offlineModeActive;
|
this.commit('reader/setOfflineModeActive', !this.offlineModeActive);
|
||||||
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsToggle() {
|
settingsToggle() {
|
||||||
@@ -1161,6 +1292,7 @@ class Reader {
|
|||||||
|
|
||||||
this.checkBookPosPercent();
|
this.checkBookPosPercent();
|
||||||
this.activateClickMapPage();//no await
|
this.activateClickMapPage();//no await
|
||||||
|
this.$refs.recentBooksPage.updateTableData();//no await
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,9 +1369,13 @@ class Reader {
|
|||||||
delete wasOpened.loadTime;
|
delete wasOpened.loadTime;
|
||||||
|
|
||||||
// добавляем в историю
|
// добавляем в историю
|
||||||
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
|
const recentBook = await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
|
||||||
|
if (this.bucSetOnNew) {
|
||||||
|
await bookManager.setCheckBuc(recentBook, true);
|
||||||
|
}
|
||||||
|
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(wasOpened.bookPos);
|
this.addAction(recentBook.bookPos);
|
||||||
this.updateRoute(true);
|
this.updateRoute(true);
|
||||||
|
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
@@ -1251,6 +1387,7 @@ class Reader {
|
|||||||
|
|
||||||
this.checkBookPosPercent();
|
this.checkBookPosPercent();
|
||||||
this.activateClickMapPage();//no await
|
this.activateClickMapPage();//no await
|
||||||
|
this.$refs.recentBooksPage.updateTableData();//no await
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
@@ -1513,33 +1650,27 @@ export default vueComponent(Reader);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
height: 50px;
|
padding: 5px 5px 0px 5px;
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 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;
|
background-color: #1B695F;
|
||||||
border-radius: 4px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header::-webkit-scrollbar-thumb {
|
.header::-webkit-scrollbar-thumb {
|
||||||
background-color: #c49a60;
|
background-color: #c4aa60;
|
||||||
border-radius: 4px;
|
border-radius: 1px;
|
||||||
border: 2px solid #e4e4e4;
|
border: 1px solid #1B695F;
|
||||||
}
|
|
||||||
|
|
||||||
.header::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #b48a50;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
@@ -1548,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;
|
||||||
@@ -1601,4 +1733,16 @@ export default vueComponent(Reader);
|
|||||||
.clear {
|
.clear {
|
||||||
color: rgba(0,0,0,0);
|
color: rgba(0,0,0,0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.need-book-update-count {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 6px 2px 6px;
|
||||||
|
left: 27px;
|
||||||
|
top: 22px;
|
||||||
|
background-color: blue;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: white;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,56 +18,63 @@
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog ref="dialog2" v-model="donationVisible">
|
<q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
|
||||||
<template #header>
|
<div class="column bg-white no-wrap q-pa-md">
|
||||||
Здравствуйте, уважаемые читатели!
|
<div class="row justify-center q-mb-md">
|
||||||
</template>
|
Здравствуйте, дорогие читатели!
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="word-break: normal">
|
<div class="q-mx-md column" style="font-size: 90%; word-break: normal">
|
||||||
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
<div>
|
||||||
|
Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
|
||||||
|
|
||||||
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
|
Напоминаем вам, что проект является некоммерческим и обладает такими
|
||||||
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
|
достоинствами, как:
|
||||||
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
|
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>непрерывно улучшаемой</li>
|
<li>все функции читалки открыты и доступны совершенно бесплатно</li>
|
||||||
<li>без рекламы</li>
|
<li>в проекте отсутствует какая-либо реклама или баннеры</li>
|
||||||
<li>без регистрации</li>
|
<li>нет никакой регистрации и монетизации</li>
|
||||||
<li>Open Source</li>
|
<li>нет сбора персональных данных</li>
|
||||||
</ul>
|
<li>открытый исходный код</li>
|
||||||
|
<li>проект постепенно улучшается, по мере возможности</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
Автор также обращается с просьбой о помощи в распространении
|
Однако на оплату хостинга читалки и сервера обновлений автор тратит свои
|
||||||
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
собственные средства, а также тратит свое время и силы на улучшение проекта.
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
<br><br>
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
Давайте поддержим наш ресурс, чтобы и дальше спокойно существовать и развиваться:
|
||||||
Скопировать
|
</div>
|
||||||
</q-tooltip>
|
|
||||||
</q-icon>
|
|
||||||
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
|
||||||
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
|
||||||
|
|
||||||
<br><br>
|
<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" />
|
||||||
<br><br>
|
Поддержать проект
|
||||||
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
</q-btn>
|
||||||
|
|
||||||
<br><br>
|
<div class="row justify-center q-mt-sm">
|
||||||
<div class="row justify-center">
|
Напомнить снова через:
|
||||||
<!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
|
</div>
|
||||||
Помочь проекту
|
|
||||||
</q-btn-->
|
<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 q-mt-md">
|
||||||
|
<div class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
|
||||||
|
Помочь проекту можно в любое время
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</q-dialog>
|
||||||
<template #footer>
|
|
||||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
|
||||||
<br>
|
|
||||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">
|
|
||||||
Напомнить позже
|
|
||||||
</q-btn>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -76,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" />
|
||||||
Из буфера обмена
|
Из буфера обмена
|
||||||
@@ -99,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: {
|
||||||
@@ -134,13 +137,13 @@ class ReaderDialogs {
|
|||||||
loadSettings() {
|
loadSettings() {
|
||||||
const settings = this.settings;
|
const settings = this.settings;
|
||||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
this.showDonationDialog = settings.showDonationDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -149,9 +152,7 @@ class ReaderDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showDonation() {
|
async showDonation() {
|
||||||
const today = utils.formatDate(new Date(), 'coDate');
|
if ((this.mode == 'omnireader' || this.mode == 'liberama') && this.showDonationDialog && this.donationNextPopup <= Date.now()) {
|
||||||
|
|
||||||
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
|
|
||||||
await utils.sleep(3000);
|
await utils.sleep(3000);
|
||||||
this.donationVisible = true;
|
this.donationVisible = true;
|
||||||
}
|
}
|
||||||
@@ -166,20 +167,18 @@ class ReaderDialogs {
|
|||||||
this.urlHelpVisible = false;
|
this.urlHelpVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
donationDialogDisable() {
|
donationDialogRemindLater(remindAfter = 30) {
|
||||||
this.donationVisible = false;
|
this.donationVisible = false;
|
||||||
if (this.showDonationDialog2020) {
|
|
||||||
this.commit('reader/setSettings', { showDonationDialog2020: false });
|
this.commit('reader/setDonationNextPopup', Date.now() + rstore.dayMs*remindAfter);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
donationDialogRemind() {
|
makeDonation() {
|
||||||
this.donationVisible = false;
|
utils.makeDonation();
|
||||||
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
this.donationDialogRemindLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
openDonate() {
|
openDonate() {
|
||||||
this.donationVisible = false;
|
|
||||||
this.$emit('donate-toggle');
|
this.$emit('donate-toggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +216,8 @@ class ReaderDialogs {
|
|||||||
return this.$store.state.reader.whatsNewContentHash;
|
return this.$store.state.reader.whatsNewContentHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
get donationRemindDate() {
|
get donationNextPopup() {
|
||||||
return this.$store.state.reader.donationRemindDate;
|
return this.$store.state.reader.donationNextPopup;
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook() {
|
keyHook() {
|
||||||
|
|||||||
@@ -9,14 +9,26 @@
|
|||||||
|
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
<div
|
<div
|
||||||
|
v-show="needBookUpdateCount > 0"
|
||||||
class="row justify-center items-center"
|
class="row justify-center items-center"
|
||||||
:class="{'header-button': !archive, 'header-button-pressed': archive}"
|
:class="{'header-button-update': !showNeedBookUpdateOnly, 'header-button-update-pressed': showNeedBookUpdateOnly}"
|
||||||
@mousedown.stop @click="archiveToggle"
|
@mousedown.stop @click="showNeedBookUpdateOnlyToggle"
|
||||||
|
>
|
||||||
|
<span style="font-size: 90%">{{ needBookUpdateCount }} обновлен{{ wordEnding(needBookUpdateCount, 3) }}</span>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ (needBookUpdateCount ? 'Скрыть обновления' : 'Показать обновления') }}
|
||||||
|
</q-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="row justify-center items-center"
|
||||||
|
:class="{'header-button': !showArchive, 'header-button-pressed': showArchive}"
|
||||||
|
@mousedown.stop @click="showArchiveToggle"
|
||||||
>
|
>
|
||||||
<q-icon class="q-mr-xs" name="la la-archive" size="20px" />
|
<q-icon class="q-mr-xs" name="la la-archive" size="20px" />
|
||||||
<span style="font-size: 90%">Архив</span>
|
<span style="font-size: 90%">Архив</span>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
{{ (archive ? 'Скрыть архивные' : 'Показать архивные') }}
|
{{ (showArchive ? 'Скрыть архивные' : 'Показать архивные') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,9 +117,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
<div class="row-part column justify-center items-stretch" style="width: 80px">
|
||||||
<div class="col row justify-center items-center clickable" style="padding: 0 2px 0 2px" @click="loadBook(item)">
|
<div class="col row justify-center items-center clickable" style="padding: 0 2px 0 2px" @click="loadBook(item, bothBucEnabled && item.needBookUpdate)">
|
||||||
<div v-show="isLoadedCover(item.coverPageUrl)" style="height: 80px" v-html="getCoverHtml(item.coverPageUrl)" />
|
<div v-show="isLoadedCover(item.coverPageUrl)" style="height: 80px" v-html="getCoverHtml(item.coverPageUrl)" />
|
||||||
<q-icon v-show="!isLoadedCover(item.coverPageUrl)" name="la la-book" size="40px" style="color: #dddddd" />
|
<q-icon v-show="!isLoadedCover(item.coverPageUrl)" name="la la-book" size="40px" style="color: #dddddd" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="bothBucEnabled && item.needBookUpdate"
|
||||||
|
class="column justify-center"
|
||||||
|
style="position: absolute; background-color: rgba(255, 255, 255, 0.5); border-radius: 40px;"
|
||||||
|
>
|
||||||
|
<q-icon name="la la-sync" size="60px" style="color: blue" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
|
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
|
||||||
@@ -126,6 +146,10 @@
|
|||||||
<div style="font-size: 75%">
|
<div style="font-size: 75%">
|
||||||
{{ item.desc.title }}
|
{{ item.desc.title }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="bothBucEnabled && item.needBookUpdate" style="font-size: 75%; color: blue;">
|
||||||
|
Размер: {{ item.bucSize - item.downloadSize > 0 ? '+' : '' }}{{ item.bucSize - item.downloadSize }}
|
||||||
|
({{ item.downloadSize }} → {{ item.bucSize }})
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" style="font-size: 10px">
|
<div class="row" style="font-size: 10px">
|
||||||
@@ -169,7 +193,7 @@
|
|||||||
class="col column justify-center"
|
class="col column justify-center"
|
||||||
style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
|
style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
|
||||||
>
|
>
|
||||||
<div :style="`margin-top: ${(archive ? 20 : 0)}px`">
|
<div style="margin: 25px 0 0 5px">
|
||||||
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
|
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
|
||||||
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
|
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,12 +205,12 @@
|
|||||||
>
|
>
|
||||||
<q-icon class="la la-times" size="12px" />
|
<q-icon class="la la-times" size="12px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
{{ (archive ? 'Удалить окончательно' : 'Перенести в архив') }}
|
{{ (showArchive ? 'Удалить окончательно' : 'Перенести в архив') }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="archive"
|
v-show="showArchive"
|
||||||
class="restore-button self-start row justify-center items-center clickable"
|
class="restore-button self-start row justify-center items-center clickable"
|
||||||
@click="handleRestore(item.key)"
|
@click="handleRestore(item.key)"
|
||||||
>
|
>
|
||||||
@@ -195,6 +219,27 @@
|
|||||||
Восстановить из архива
|
Восстановить из архива
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="bothBucEnabled && item.showCheckBuc"
|
||||||
|
class="buc-checkbox self-start"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="item.checkBuc"
|
||||||
|
size="xs"
|
||||||
|
style="position: relative; top: -3px; left: -3px;"
|
||||||
|
@update:model-value="checkBucChange(item)"
|
||||||
|
>
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
<div v-if="item.checkBuc === undefined">
|
||||||
|
Проверка обновлений отключена автоматически<br>т.к. книга не обновлялась {{ bucCancelDays }} дней
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ (item.checkBuc ? 'Проверка обновлений книги включена' : 'Проверка обновлений книги отключена') }}
|
||||||
|
</div>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-virtual-scroll>
|
</q-virtual-scroll>
|
||||||
@@ -230,6 +275,12 @@ const componentOptions = {
|
|||||||
settings() {
|
settings() {
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
},
|
},
|
||||||
|
needBookUpdateCount() {
|
||||||
|
if (this.needBookUpdateCount == 0)
|
||||||
|
this.showNeedBookUpdateOnly = false;
|
||||||
|
|
||||||
|
this.$emit('update-count-changed', {needBookUpdateCount: this.needBookUpdateCount});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
class RecentBooksPage {
|
class RecentBooksPage {
|
||||||
@@ -240,7 +291,14 @@ class RecentBooksPage {
|
|||||||
tableData = [];
|
tableData = [];
|
||||||
sortMethod = '';
|
sortMethod = '';
|
||||||
showSameBook = false;
|
showSameBook = false;
|
||||||
archive = false;
|
bucEnabled = false;
|
||||||
|
bucSizeDiff = 0;
|
||||||
|
bucSetOnNew = false;
|
||||||
|
bucCancelDays = 0;
|
||||||
|
needBookUpdateCount = 0;
|
||||||
|
|
||||||
|
showArchive = false;
|
||||||
|
showNeedBookUpdateOnly = false;
|
||||||
|
|
||||||
covers = {};
|
covers = {};
|
||||||
coversLoadFunc = {};
|
coversLoadFunc = {};
|
||||||
@@ -277,12 +335,20 @@ class RecentBooksPage {
|
|||||||
const settings = this.settings;
|
const settings = this.settings;
|
||||||
this.showSameBook = settings.recentShowSameBook;
|
this.showSameBook = settings.recentShowSameBook;
|
||||||
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
|
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
|
||||||
|
this.bucEnabled = settings.bucEnabled;
|
||||||
|
this.bucSizeDiff = settings.bucSizeDiff;
|
||||||
|
this.bucSetOnNew = settings.bucSetOnNew;
|
||||||
|
this.bucCancelDays = settings.bucCancelDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
return this.$store.state.reader.settings;
|
return this.$store.state.reader.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get bothBucEnabled() {
|
||||||
|
return this.$store.state.config.bucEnabled && this.bucEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
async updateTableData() {
|
async updateTableData() {
|
||||||
if (!this.inited)
|
if (!this.inited)
|
||||||
return;
|
return;
|
||||||
@@ -296,15 +362,15 @@ class RecentBooksPage {
|
|||||||
|
|
||||||
//подготовка полей
|
//подготовка полей
|
||||||
for (const book of sorted) {
|
for (const book of sorted) {
|
||||||
if ((!this.archive && book.deleted) || (this.archive && book.deleted != 1))
|
if ((!this.showArchive && book.deleted) || (this.showArchive && book.deleted != 1))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
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 = '';
|
||||||
@@ -320,7 +386,7 @@ class RecentBooksPage {
|
|||||||
|
|
||||||
let title = bt.bookTitle;
|
let title = bt.bookTitle;
|
||||||
title = (title ? `"${title}"`: '');
|
title = (title ? `"${title}"`: '');
|
||||||
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
|
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url))) || '';
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
key: book.key,
|
key: book.key,
|
||||||
@@ -344,6 +410,19 @@ class RecentBooksPage {
|
|||||||
inGroup: false,
|
inGroup: false,
|
||||||
coverPageUrl: book.coverPageUrl,
|
coverPageUrl: book.coverPageUrl,
|
||||||
|
|
||||||
|
showCheckBuc: !this.showArchive && utils.hasProp(book, 'downloadSize') && book.url.indexOf('disk://') !== 0,
|
||||||
|
checkBuc: book.checkBuc,
|
||||||
|
needBookUpdate: (
|
||||||
|
!this.showArchive
|
||||||
|
&& book.checkBuc
|
||||||
|
&& book.bucSize
|
||||||
|
&& utils.hasProp(book, 'downloadSize')
|
||||||
|
&& book.bucSize !== book.downloadSize
|
||||||
|
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff)
|
||||||
|
),
|
||||||
|
bucSize: book.bucSize,
|
||||||
|
downloadSize: book.downloadSize,
|
||||||
|
|
||||||
//для сортировки
|
//для сортировки
|
||||||
loadTimeRaw,
|
loadTimeRaw,
|
||||||
touchTimeRaw: book.touchTime,
|
touchTimeRaw: book.touchTime,
|
||||||
@@ -361,12 +440,15 @@ class RecentBooksPage {
|
|||||||
//фильтрация
|
//фильтрация
|
||||||
const search = this.search;
|
const search = this.search;
|
||||||
if (search) {
|
if (search) {
|
||||||
|
const lowerSearch = search.toLowerCase();
|
||||||
|
|
||||||
result = result.filter(item => {
|
result = result.filter(item => {
|
||||||
return !search ||
|
return !search
|
||||||
item.touchTime.includes(search) ||
|
|| item.touchTime.includes(search)
|
||||||
item.loadTime.includes(search) ||
|
|| item.loadTime.includes(search)
|
||||||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
|| item.desc.title.toLowerCase().includes(lowerSearch)
|
||||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
|| item.desc.author.toLowerCase().includes(lowerSearch)
|
||||||
|
;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +481,7 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//группировка
|
//группировка
|
||||||
|
let nbuCount = 0;
|
||||||
const groups = {};
|
const groups = {};
|
||||||
const parents = {};
|
const parents = {};
|
||||||
let newResult = [];
|
let newResult = [];
|
||||||
@@ -415,13 +498,20 @@ class RecentBooksPage {
|
|||||||
if (book.active)
|
if (book.active)
|
||||||
parents[book.sameBookKey].activeParent = true;
|
parents[book.sameBookKey].activeParent = true;
|
||||||
|
|
||||||
|
book.showCheckBuc = false;
|
||||||
|
book.needBookUpdate = false;
|
||||||
|
|
||||||
groups[book.sameBookKey].push(book);
|
groups[book.sameBookKey].push(book);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newResult.push(book);
|
newResult.push(book);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (book.needBookUpdate)
|
||||||
|
nbuCount++;
|
||||||
}
|
}
|
||||||
result = newResult;
|
result = newResult;
|
||||||
|
this.needBookUpdateCount = nbuCount;
|
||||||
|
|
||||||
//showSameBook
|
//showSameBook
|
||||||
if (this.showSameBook) {
|
if (this.showSameBook) {
|
||||||
@@ -438,6 +528,11 @@ class RecentBooksPage {
|
|||||||
result = newResult;
|
result = newResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//showNeedBookUpdateOnly
|
||||||
|
if (this.showNeedBookUpdateOnly) {
|
||||||
|
result = result.filter(item => item.needBookUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
//другие стадии
|
//другие стадии
|
||||||
//.....
|
//.....
|
||||||
|
|
||||||
@@ -456,7 +551,8 @@ class RecentBooksPage {
|
|||||||
const endings = [
|
const endings = [
|
||||||
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
|
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
|
||||||
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],
|
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],
|
||||||
['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о']
|
['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],
|
||||||
|
['ий', 'ие', 'ия', 'ия', 'ия', 'ий', 'ий', 'ий', 'ий', 'ий']
|
||||||
];
|
];
|
||||||
const deci = num % 100;
|
const deci = num % 100;
|
||||||
if (deci > 10 && deci < 20) {
|
if (deci > 10 && deci < 20) {
|
||||||
@@ -468,7 +564,7 @@ class RecentBooksPage {
|
|||||||
|
|
||||||
get header() {
|
get header() {
|
||||||
const len = (this.tableData ? this.tableData.length : 0);
|
const len = (this.tableData ? this.tableData.length : 0);
|
||||||
return `${(this.search ? `Найден${this.wordEnding(len, 2)}` : 'Всего')} ${len} файл${this.wordEnding(len)}${this.archive ? ' в архиве' : ''}`;
|
return `${(this.search || this.showNeedBookUpdateOnly ? `Найден${this.wordEnding(len, 2)}` : 'Всего')} ${len} файл${this.wordEnding(len)}${this.showArchive ? ' в архиве' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBook(fb2path, fullTitle) {
|
async downloadBook(fb2path, fullTitle) {
|
||||||
@@ -494,7 +590,7 @@ class RecentBooksPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDel(key) {
|
async handleDel(key) {
|
||||||
if (!this.archive) {
|
if (!this.showArchive) {
|
||||||
await bookManager.delRecentBook({key});
|
await bookManager.delRecentBook({key});
|
||||||
this.$root.notify.info('Перенесено в архив');
|
this.$root.notify.info('Перенесено в архив');
|
||||||
} else {
|
} else {
|
||||||
@@ -510,14 +606,11 @@ class RecentBooksPage {
|
|||||||
this.$root.notify.info('Восстановлено из архива');
|
this.$root.notify.info('Восстановлено из архива');
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBook(item) {
|
async loadBook(item, force = false) {
|
||||||
//чтобы не обновлять лишний раз updateTableData
|
|
||||||
this.inited = false;
|
|
||||||
|
|
||||||
if (item.deleted)
|
if (item.deleted)
|
||||||
await this.handleRestore(item.key);
|
await this.handleRestore(item.key);
|
||||||
|
|
||||||
this.$emit('load-book', {url: item.url, path: item.path});
|
this.$emit('load-book', {url: item.url, path: item.path, force});
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,8 +738,10 @@ class RecentBooksPage {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveToggle() {
|
showArchiveToggle() {
|
||||||
this.archive = !this.archive;
|
this.showArchive = !this.showArchive;
|
||||||
|
this.showNeedBookUpdateOnly = false;
|
||||||
|
|
||||||
this.updateTableData();
|
this.updateTableData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,6 +808,27 @@ class RecentBooksPage {
|
|||||||
else
|
else
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkBucChange(item) {
|
||||||
|
const book = await bookManager.getRecentBook(item);
|
||||||
|
if (book) {
|
||||||
|
await bookManager.setCheckBuc(book, item.checkBuc);
|
||||||
|
|
||||||
|
this.$root.notify.info(item.checkBuc
|
||||||
|
? 'Проверка обновлений книги включена'
|
||||||
|
: 'Проверка обновлений книги отключена'
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNeedBookUpdateOnlyToggle() {
|
||||||
|
this.showNeedBookUpdateOnly = !this.showNeedBookUpdateOnly;
|
||||||
|
this.showArchive = false;
|
||||||
|
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vueComponent(RecentBooksPage);
|
export default vueComponent(RecentBooksPage);
|
||||||
@@ -842,17 +958,24 @@ export default vueComponent(RecentBooksPage);
|
|||||||
color: #555555;
|
color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-button:hover {
|
.header-button-update, .header-button-update-pressed {
|
||||||
|
width: 120px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button:hover, .header-button-update:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #39902F;
|
background-color: #39902F;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-button-pressed {
|
.header-button-pressed, .header-button-update-pressed {
|
||||||
color: black;
|
color: black;
|
||||||
background-color: yellow;
|
background-color: yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-button-pressed:hover {
|
.buc-checkbox {
|
||||||
color: black;
|
position: absolute;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import bookManager from '../share/bookManager';
|
|||||||
import readerApi from '../../../api/reader';
|
import readerApi from '../../../api/reader';
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||||
|
import LockQueue from '../../../share/LockQueue';
|
||||||
|
|
||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
const ssCacheStore = localForage.createInstance({
|
const ssCacheStore = localForage.createInstance({
|
||||||
@@ -48,6 +49,9 @@ 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.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
|
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
|
||||||
|
|
||||||
this.debouncedSaveSettings = _.debounce(() => {
|
this.debouncedSaveSettings = _.debounce(() => {
|
||||||
@@ -201,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', '');
|
||||||
@@ -542,14 +550,16 @@ class ServerStorage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRecent(itemKey, recurse) {
|
async saveRecent(itemKeys, recurse) {
|
||||||
while (!this.inited || this.savingRecent)
|
while (!this.inited)
|
||||||
await utils.sleep(100);
|
await utils.sleep(100);
|
||||||
|
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.savingRecent = true;
|
let needRecurseCall = false;
|
||||||
|
|
||||||
|
await this.lock.get();
|
||||||
try {
|
try {
|
||||||
const bm = bookManager;
|
const bm = bookManager;
|
||||||
|
|
||||||
@@ -559,22 +569,29 @@ class ServerStorage {
|
|||||||
|
|
||||||
//newRecentMod
|
//newRecentMod
|
||||||
let newRecentMod = {};
|
let newRecentMod = {};
|
||||||
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
|
let oneItemKey = null;
|
||||||
|
if (itemKeys && itemKeys.length == 1)
|
||||||
|
oneItemKey = itemKeys[0];
|
||||||
|
|
||||||
|
if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
|
||||||
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||||
newRecentMod.rev++;
|
newRecentMod.rev++;
|
||||||
|
|
||||||
newRecentMod.data.key = itemKey;
|
newRecentMod.data.key = oneItemKey;
|
||||||
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
|
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
|
||||||
needSaveRecentMod = true;
|
needSaveRecentMod = true;
|
||||||
}
|
}
|
||||||
this.prevItemKey = itemKey;
|
this.prevItemKey = oneItemKey;
|
||||||
|
|
||||||
//newRecentPatch
|
//newRecentPatch
|
||||||
let newRecentPatch = {};
|
let newRecentPatch = {};
|
||||||
if (itemKey && !needSaveRecentMod) {
|
if (itemKeys && !needSaveRecentMod) {
|
||||||
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||||
newRecentPatch.rev++;
|
newRecentPatch.rev++;
|
||||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
|
||||||
|
for (const key of itemKeys) {
|
||||||
|
newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
|
||||||
|
}
|
||||||
|
|
||||||
const applyMod = this.cachedRecentMod.data;
|
const applyMod = this.cachedRecentMod.data;
|
||||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||||
@@ -587,11 +604,7 @@ class ServerStorage {
|
|||||||
|
|
||||||
//newRecent
|
//newRecent
|
||||||
let newRecent = {};
|
let newRecent = {};
|
||||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
//ждем весь bm.recent
|
|
||||||
/*while (!bookManager.loaded)
|
|
||||||
await utils.sleep(100);*/
|
|
||||||
|
|
||||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
@@ -625,10 +638,8 @@ class ServerStorage {
|
|||||||
|
|
||||||
if (res)
|
if (res)
|
||||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
if (!recurse && itemKey) {
|
if (!recurse && itemKeys) {
|
||||||
this.savingRecent = false;
|
needRecurseCall = true;
|
||||||
await this.saveRecent(itemKey, true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else if (result.state == 'success') {
|
} else if (result.state == 'success') {
|
||||||
if (needSaveRecent && newRecent.rev)
|
if (needSaveRecent && newRecent.rev)
|
||||||
@@ -637,10 +648,15 @@ 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.savingRecent = false;
|
this.lock.ret();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needRecurseCall)
|
||||||
|
await this.saveRecent(itemKeys, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async storageCheck(items) {
|
async storageCheck(items) {
|
||||||
@@ -656,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);
|
||||||
|
|||||||
@@ -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
client/components/Reader/SettingsPage/ConvertTab/ConvertTab.vue
Normal file
145
client/components/Reader/SettingsPage/ConvertTab/ConvertTab.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/KeysTab/KeysTab.vue
Normal file
78
client/components/Reader/SettingsPage/KeysTab/KeysTab.vue
Normal file
@@ -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>
|
||||||
@@ -73,10 +73,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import vueComponent from '../../../vueComponent.js';
|
import vueComponent from '../../../../vueComponent.js';
|
||||||
|
|
||||||
import rstore from '../../../../store/modules/reader';
|
import rstore from '../../../../../store/modules/reader';
|
||||||
//import * as utils from '../../share/utils';
|
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -1,102 +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="showNeedUpdateNotify">
|
|
||||||
Показывать уведомление о новой версии
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
|
||||||
Напоминать о необходимости обновления страницы<br>
|
|
||||||
при появлении новой версии читалки
|
|
||||||
</q-tooltip>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--div class="item row">
|
|
||||||
<div class="label-6">Уведомление</div>
|
|
||||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
|
||||||
Показывать "Оплатим хостинг вместе"
|
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
|
||||||
Показывать уведомление "Оплатим хостинг вместе"
|
|
||||||
</q-tooltip>
|
|
||||||
</q-checkbox>
|
|
||||||
</div-->
|
|
||||||
|
|
||||||
<!---------------------------------------------->
|
|
||||||
<div class="part-header">Другое</div>
|
|
||||||
|
|
||||||
<div class="item row">
|
|
||||||
<div class="label-6">Обработка</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
client/components/Reader/SettingsPage/OthersTab/OthersTab.vue
Normal file
148
client/components/Reader/SettingsPage/OthersTab/OthersTab.vue
Normal file
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
ключ доступа
|
|
||||||
</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>
|
|
||||||
@@ -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>
|
||||||
|
ключ доступа
|
||||||
|
</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>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="item row">
|
|
||||||
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
|
||||||
</div>
|
|
||||||
41
client/components/Reader/SettingsPage/ResetTab/ResetTab.vue
Normal file
41
client/components/Reader/SettingsPage/ResetTab/ResetTab.vue
Normal file
@@ -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>
|
||||||
@@ -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,90 +22,34 @@
|
|||||||
stretch
|
stretch
|
||||||
inline-label
|
inline-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-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
|
<q-icon :name="item.icon" :color="selectedTab == item.name ? 'yellow' : 'teal-7'" size="24px" />
|
||||||
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
|
<div class="q-ml-xs" style="font-size: 90%">
|
||||||
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
|
{{ item.label }}
|
||||||
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
</div>
|
||||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
</q-tab>
|
||||||
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
|
||||||
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
|
||||||
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
|
||||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col fit">
|
<div class="col fit">
|
||||||
<!-- Профили --------------------------------------------------------------------->
|
<!-- Профили --------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'profiles'" class="fit tab-panel">
|
<ProfilesTab v-if="selectedTab == 'profiles'" :form="form" />
|
||||||
@@include('./ProfilesTab.inc');
|
|
||||||
</div>
|
|
||||||
<!-- Вид ------------------------------------------------------------------------->
|
<!-- Вид ------------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'view'" class="fit column">
|
<ViewTab v-if="selectedTab == 'view'" :form="form" />
|
||||||
<q-tabs
|
|
||||||
v-model="selectedViewTab"
|
|
||||||
active-color="black"
|
|
||||||
active-bg-color="white"
|
|
||||||
indicator-color="white"
|
|
||||||
dense
|
|
||||||
no-caps
|
|
||||||
class="no-mp bg-grey-4 text-grey-7"
|
|
||||||
>
|
|
||||||
<q-tab name="mode" label="Режим" />
|
|
||||||
<q-tab name="color" label="Цвет" />
|
|
||||||
<q-tab name="font" label="Шрифт" />
|
|
||||||
<q-tab name="text" label="Текст" />
|
|
||||||
<q-tab name="status" label="Строка статуса" />
|
|
||||||
</q-tabs>
|
|
||||||
|
|
||||||
<div class="q-mb-sm" />
|
|
||||||
|
|
||||||
<div class="col tab-panel">
|
|
||||||
<div v-if="selectedViewTab == 'mode'">
|
|
||||||
@@include('./ViewTab/Mode.inc');
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedViewTab == 'color'">
|
|
||||||
@@include('./ViewTab/Color.inc');
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedViewTab == 'font'">
|
|
||||||
@@include('./ViewTab/Font.inc');
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedViewTab == 'text'">
|
|
||||||
@@include('./ViewTab/Text.inc');
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedViewTab == 'status'">
|
|
||||||
@@include('./ViewTab/Status.inc');
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Кнопки ---------------------------------------------------------------------->
|
<!-- Кнопки ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
|
<ToolBarTab v-if="selectedTab == 'toolbar'" :form="form" />
|
||||||
@@include('./ToolBarTab.inc');
|
|
||||||
</div>
|
|
||||||
<!-- Управление ------------------------------------------------------------------>
|
<!-- Управление ------------------------------------------------------------------>
|
||||||
<div v-if="selectedTab == 'keys'" class="fit column">
|
<KeysTab v-if="selectedTab == 'keys'" :form="form" />
|
||||||
@@include('./KeysTab.inc');
|
|
||||||
</div>
|
|
||||||
<!-- Листание -------------------------------------------------------------------->
|
<!-- Листание -------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
<PageMoveTab v-if="selectedTab == 'pagemove'" :form="form" />
|
||||||
@@include('./PageMoveTab.inc');
|
|
||||||
</div>
|
|
||||||
<!-- Конвертирование ------------------------------------------------------------->
|
<!-- Конвертирование ------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
<ConvertTab v-if="selectedTab == 'convert'" :form="form" />
|
||||||
@@include('./ConvertTab.inc');
|
<!-- Обновление ------------------------------------------------------------------>
|
||||||
</div>
|
<UpdateTab v-if="selectedTab == 'update'" :form="form" />
|
||||||
<!-- Прочее ---------------------------------------------------------------------->
|
<!-- Прочее ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
<OthersTab v-if="selectedTab == 'others'" :form="form" />
|
||||||
@@include('./OthersTab.inc');
|
|
||||||
</div>
|
|
||||||
<!-- Сброс ----------------------------------------------------------------------->
|
<!-- Сброс ----------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
|
<ResetTab v-if="selectedTab == 'reset'" :form="form" @tab-event="tabEvent" />
|
||||||
@@include('./ResetTab.inc');
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
@@ -114,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';
|
//stuff
|
||||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
|
||||||
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,
|
//pages
|
||||||
UserHotKeys,
|
ProfilesTab,
|
||||||
},
|
ViewTab,
|
||||||
data: function() {
|
ToolBarTab,
|
||||||
return Object.assign({}, rstore.settingDefaults);
|
KeysTab,
|
||||||
|
PageMoveTab,
|
||||||
|
ConvertTab,
|
||||||
|
UpdateTab,
|
||||||
|
OthersTab,
|
||||||
|
ResetTab,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
settings: function() {
|
settings: function() {
|
||||||
this.settingsChanged();
|
this.settingsChanged();//no await
|
||||||
},
|
},
|
||||||
form: function(newValue) {
|
form: {
|
||||||
if (this.inited) {
|
handler() {
|
||||||
this.commit('reader/setSettings', _.cloneDeep(newValue));
|
if (this.inited && !this.isSetsChanged) {
|
||||||
}
|
this.debouncedCommitSettings();
|
||||||
},
|
}
|
||||||
fontBold: function(newValue) {
|
},
|
||||||
this.fontWeight = (newValue ? 'bold' : '');
|
deep: true,
|
||||||
},
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
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 = [];
|
isSetsChanged = false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.reader = this.$store.state.reader;
|
|
||||||
|
|
||||||
this.form = {};
|
this.debouncedCommitSettings = _.debounce(() => {
|
||||||
this.rstore = rstore;
|
this.commit('reader/setSettings', _.cloneDeep(this.form));
|
||||||
this.toolButtons = rstore.toolButtons;
|
}, 50);
|
||||||
this.settingsChanged();
|
|
||||||
|
this.settingsChanged();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$watch(
|
|
||||||
'$refs.tabs.scrollable',
|
|
||||||
(newValue) => {
|
|
||||||
this.tabsScrollable = newValue && !this.$root.isMobileDevice;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -267,190 +144,20 @@ class SettingsPage {
|
|||||||
this.inited = true;
|
this.inited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsChanged() {
|
async settingsChanged() {
|
||||||
if (_.isEqual(this.form, this.settings))
|
this.isSetsChanged = true;
|
||||||
return;
|
try {
|
||||||
|
this.form = reactive(_.cloneDeep(this.settings));
|
||||||
this.form = Object.assign({}, this.settings);
|
} finally {
|
||||||
for (const prop in rstore.settingDefaults) {
|
await this.$nextTick();
|
||||||
this[prop] = _.cloneDeep(this.form[prop]);
|
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 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'});
|
||||||
}
|
}
|
||||||
@@ -458,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);
|
this.form = _.cloneDeep(rstore.settingDefaults);
|
||||||
for (let prop in rstore.settingDefaults) {
|
|
||||||
this[prop] = this.form[prop];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addProfile() {
|
tabEvent(event) {
|
||||||
try {
|
if (!event || !event.action)
|
||||||
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;
|
return;
|
||||||
|
|
||||||
try {
|
switch (event.action) {
|
||||||
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
|
case 'set-defaults': this.setDefaults(); break;
|
||||||
`<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 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'});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,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%;
|
||||||
@@ -729,25 +215,7 @@ export default vueComponent(SettingsPage);
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.sets-label {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -756,33 +224,14 @@ export default vueComponent(SettingsPage);
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.sets-item {
|
||||||
font-size: 90%;
|
width: 100%;
|
||||||
line-height: 130%;
|
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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
122
client/components/Reader/SettingsPage/UpdateTab/UpdateTab.vue
Normal file
122
client/components/Reader/SettingsPage/UpdateTab/UpdateTab.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/ViewTab/Color/Color.vue
Normal file
329
client/components/Reader/SettingsPage/ViewTab/Color/Color.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/ViewTab/Font/Font.vue
Normal file
176
client/components/Reader/SettingsPage/ViewTab/Font/Font.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/ViewTab/Mode/Mode.vue
Normal file
229
client/components/Reader/SettingsPage/ViewTab/Mode/Mode.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/ViewTab/Status/Status.vue
Normal file
153
client/components/Reader/SettingsPage/ViewTab/Status/Status.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/ViewTab/Text/Text.vue
Normal file
210
client/components/Reader/SettingsPage/ViewTab/Text/Text.vue
Normal file
@@ -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
client/components/Reader/SettingsPage/ViewTab/ViewTab.vue
Normal file
75
client/components/Reader/SettingsPage/ViewTab/ViewTab.vue
Normal file
@@ -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>
|
||||||
@@ -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
client/components/Reader/SettingsPage/ViewTab/helper.js
Normal file
9
client/components/Reader/SettingsPage/ViewTab/helper.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)
|
if (this.pageChangeAnimation) {
|
||||||
this.toggleLayout = !this.toggleLayout;
|
|
||||||
else {
|
|
||||||
this.page2 = this.page1;
|
this.page2 = this.page1;
|
||||||
this.toggleLayout = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.toggleLayout)
|
this.page1 = this.drawHelper.drawPage(lines);
|
||||||
this.page1 = this.drawHelper.drawPage(lines);
|
|
||||||
else
|
|
||||||
this.page2 = 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
|
const lines = this.getLines(this.bookPos);
|
||||||
if (!this.pageChangeAnimation && this.pageChangeDirectionDown && this.pagePrepared && this.bookPos == this.bookPosPrepared) {
|
this.linesDown = lines.linesDown;
|
||||||
this.toggleLayout = !this.toggleLayout;
|
this.linesUp = lines.linesUp;
|
||||||
this.linesDown = this.linesDownNext;
|
this.debouncedUpdatePage(lines.linesDown);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,6 +234,10 @@ class BookManager {
|
|||||||
|
|
||||||
async addBook(newBook, callback) {
|
async addBook(newBook, callback) {
|
||||||
let meta = {url: newBook.url, path: newBook.path};
|
let meta = {url: newBook.url, path: newBook.path};
|
||||||
|
|
||||||
|
if (newBook.downloadSize !== undefined && newBook.downloadSize >= 0)
|
||||||
|
meta.downloadSize = newBook.downloadSize;
|
||||||
|
|
||||||
meta.key = this.keyFromPath(meta.path);
|
meta.key = this.keyFromPath(meta.path);
|
||||||
meta.addTime = Date.now();//время добавления в кеш
|
meta.addTime = Date.now();//время добавления в кеш
|
||||||
|
|
||||||
@@ -483,6 +487,31 @@ class BookManager {
|
|||||||
await this.recentSetItem(item);
|
await this.recentSetItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCheckBuc(value, checkBuc) {
|
||||||
|
const item = this.recent[value.key];
|
||||||
|
|
||||||
|
const updateItems = [];
|
||||||
|
if (item) {
|
||||||
|
if (item.sameBookKey !== undefined) {
|
||||||
|
const sorted = this.getSortedRecent();
|
||||||
|
for (const book of sorted) {
|
||||||
|
if (!book.deleted && book.sameBookKey === item.sameBookKey)
|
||||||
|
updateItems.push(book);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateItems.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
for (const book of updateItems) {
|
||||||
|
book.checkBuc = checkBuc;
|
||||||
|
if (checkBuc)
|
||||||
|
book.checkBucTime = now;
|
||||||
|
await this.recentSetItem(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
const sorted = this.getSortedRecent();
|
const sorted = this.getSortedRecent();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,66 @@
|
|||||||
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',
|
||||||
|
releaseDate: '2022-09-04',
|
||||||
|
showUntil: '2022-09-11',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц</li>
|
||||||
|
<li>автор приносит извинения за доставленные неудобства</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.12.1',
|
||||||
|
releaseDate: '2022-09-01',
|
||||||
|
showUntil: '2022-08-30',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена форма для доната</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.12.0',
|
||||||
|
releaseDate: '2022-07-27',
|
||||||
|
showUntil: '2022-08-03',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>запущен сервер проверки обновлений книг:</li>
|
||||||
|
<ul>
|
||||||
|
<li>проверка обновления той или иной книги настраивается в списке загруженных (чекбокс)</li>
|
||||||
|
<li>для того, чтобы чекбокс появился у ранее загруженной, необходимо принудительно обновить книгу</li>
|
||||||
|
<li>в настройках можно указать разницу размеров, при которой требуется делать уведомление</li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
version: '0.11.8',
|
version: '0.11.8',
|
||||||
releaseDate: '2022-07-14',
|
releaseDate: '2022-07-14',
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Settings в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../vueComponent.js';
|
|
||||||
|
|
||||||
class Settings {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Settings);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
Раздел Sources в разработке
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import vueComponent from '../vueComponent.js';
|
|
||||||
|
|
||||||
class Sources {
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default vueComponent(Sources);
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
@@ -29,7 +29,7 @@ class Notify {
|
|||||||
html: true,
|
html: true,
|
||||||
|
|
||||||
message:
|
message:
|
||||||
`<div style="max-width: 350px;">
|
`<div style="max-width: 350px">
|
||||||
${caption}
|
${caption}
|
||||||
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
|
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|||||||
@@ -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) {
|
filteredValue() {
|
||||||
if (this.validate(newValue)) {
|
this.checkErrorAndEmit(true);
|
||||||
this.error = false;
|
|
||||||
this.$emit('update:modelValue', this.string2number(newValue));
|
|
||||||
} else {
|
|
||||||
this.error = 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.inRepeatFunc)
|
||||||
if (this.startClickRepeat) {
|
return;
|
||||||
this.clickRepeat = true;
|
|
||||||
while (this.clickRepeat) {
|
this.inRepeatFunc = true;
|
||||||
if (way == 'plus') {
|
try {
|
||||||
this.plus();
|
await utils.sleep(300);
|
||||||
} else {
|
if (this.startClickRepeat) {
|
||||||
this.minus();
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
let classProto = Object.getPrototypeOf(obj);
|
||||||
if (desc.get) {//has getter, computed
|
while (classProto) {
|
||||||
computed[method] = {get: desc.get};
|
const classMethods = Object.getOwnPropertyNames(classProto);
|
||||||
if (desc.set)
|
for (const method of classMethods) {
|
||||||
computed[method].set = desc.set;
|
const desc = Object.getOwnPropertyDescriptor(classProto, method);
|
||||||
} else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
|
if (desc.get) {//has getter, computed
|
||||||
'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
|
if (!computed[method]) {
|
||||||
'setup'].includes(method) ) {
|
computed[method] = {get: desc.get};
|
||||||
comp[method] = obj[method];
|
if (desc.set)
|
||||||
} else if (method !== 'constructor') {//usual
|
computed[method].set = desc.set;
|
||||||
methods[method] = obj[method];
|
}
|
||||||
|
} 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;
|
||||||
|
|||||||
@@ -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'],
|
['/', null, null, '/reader'],
|
||||||
['/cardindex', CardIndex],
|
|
||||||
['/cardindex~search', Search],
|
|
||||||
['/cardindex~card', Card],
|
|
||||||
['/cardindex~card/:authorId', Card],
|
|
||||||
['/cardindex~book', Book],
|
|
||||||
['/cardindex~book/:bookId', Book],
|
|
||||||
['/cardindex~history', History],
|
|
||||||
|
|
||||||
['/reader', Reader],
|
['/reader', Reader],
|
||||||
['/external-libs', ExternalLibs],
|
['/external-libs', ExternalLibs],
|
||||||
['/income', Income],
|
['/:pathMatch(.*)*', null, null, '/reader'],
|
||||||
['/sources', Sources],
|
|
||||||
['/settings', Settings],
|
|
||||||
['/help', Help],
|
|
||||||
['/404', NotFound404],
|
|
||||||
['/:pathMatch(.*)*', null, null, '/cardindex'],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let routes = {};
|
let routes = {};
|
||||||
|
|||||||
@@ -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,22 +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 '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;
|
||||||
@@ -410,3 +395,11 @@ export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
|
|||||||
reject('Не удалось изменить размер');
|
reject('Не удалось изменить размер');
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeDonation() {
|
||||||
|
window.open('https://donatty.com/liberama', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateFormat(date, format = 'DD.MM.YYYY') {
|
||||||
|
return dayjs(date).format(format);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
import createPersistedState from 'vuex-persistedstate';
|
//import createPersistedState from 'vuex-persistedstate';
|
||||||
|
import VuexPersistence from 'vuex-persist';
|
||||||
|
|
||||||
import root from './root.js';
|
import root from './root.js';
|
||||||
import uistate from './modules/uistate';
|
import uistate from './modules/uistate';
|
||||||
@@ -8,6 +9,8 @@ import reader from './modules/reader';
|
|||||||
|
|
||||||
const debug = process.env.NODE_ENV !== 'production';
|
const debug = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
const vuexLocal = new VuexPersistence();
|
||||||
|
|
||||||
export default createStore(Object.assign({}, root, {
|
export default createStore(Object.assign({}, root, {
|
||||||
modules: {
|
modules: {
|
||||||
uistate,
|
uistate,
|
||||||
@@ -15,5 +18,5 @@ export default createStore(Object.assign({}, root, {
|
|||||||
reader,
|
reader,
|
||||||
},
|
},
|
||||||
strict: debug,
|
strict: debug,
|
||||||
plugins: [createPersistedState()]
|
plugins: [vuexLocal.plugin]
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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: 'clickControl', show: true},
|
||||||
{name: 'offlineMode', show: false},
|
{name: 'offlineMode', show: true},
|
||||||
];
|
];
|
||||||
|
|
||||||
//readerActions[name]
|
//readerActions[name]
|
||||||
@@ -180,19 +184,25 @@ const settingDefaults = {
|
|||||||
|
|
||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
showDonationDialog2020: true,
|
showDonationDialog: true,
|
||||||
showNeedUpdateNotify: true,
|
showNeedUpdateNotify: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
toolBarHideOnScroll: true,
|
toolBarHideOnScroll: false,
|
||||||
|
toolBarMultiLine: true,
|
||||||
userHotKeys: {},
|
userHotKeys: {},
|
||||||
userWallpapers: [],
|
userWallpapers: [],
|
||||||
|
|
||||||
recentShowSameBook: false,
|
recentShowSameBook: false,
|
||||||
recentSortMethod: '',
|
recentSortMethod: '',
|
||||||
|
|
||||||
needUpdateSettingsView: 0,
|
//Book Update Checker
|
||||||
|
bucEnabled: true, // общее включение/выключение проверки обновлений
|
||||||
|
bucSizeDiff: 1, // разница в размерах файла, при которой показывать наличие обновления
|
||||||
|
bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
|
||||||
|
bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
|
||||||
|
bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
@@ -218,45 +228,68 @@ function addDefaultsToSettings(settings) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const libsDefaults = {
|
function getLibsDefaults(mode = 'reader') {
|
||||||
startLink: 'http://flibusta.is',
|
const result = {
|
||||||
comment: 'Флибуста | Книжное братство',
|
startLink: '',
|
||||||
closeAfterSubmit: false,
|
comment: '',
|
||||||
openInFrameOnEnter: false,
|
closeAfterSubmit: false,
|
||||||
openInFrameOnAdd: false,
|
openInFrameOnEnter: false,
|
||||||
groups: [
|
openInFrameOnAdd: false,
|
||||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
helpShowed: false,
|
||||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
mode,
|
||||||
]},
|
groups: [
|
||||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
|
||||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
|
||||||
]},
|
]},
|
||||||
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
|
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
|
||||||
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
|
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
|
||||||
]},
|
]},
|
||||||
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
|
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
|
||||||
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
|
{l: 'https://aldebaran.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 = {
|
||||||
toolBarActive: true,
|
toolBarActive: true,
|
||||||
|
offlineModeActive: false,
|
||||||
serverSyncEnabled: false,
|
serverSyncEnabled: false,
|
||||||
serverStorageKey: '',
|
serverStorageKey: '',
|
||||||
profiles: {},
|
profiles: {},
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,6 +304,9 @@ const mutations = {
|
|||||||
setToolBarActive(state, value) {
|
setToolBarActive(state, value) {
|
||||||
state.toolBarActive = value;
|
state.toolBarActive = value;
|
||||||
},
|
},
|
||||||
|
setOfflineModeActive(state, value) {
|
||||||
|
state.offlineModeActive = value;
|
||||||
|
},
|
||||||
setServerSyncEnabled(state, value) {
|
setServerSyncEnabled(state, value) {
|
||||||
state.serverSyncEnabled = value;
|
state.serverSyncEnabled = value;
|
||||||
},
|
},
|
||||||
@@ -289,8 +325,8 @@ const mutations = {
|
|||||||
setWhatsNewContentHash(state, value) {
|
setWhatsNewContentHash(state, value) {
|
||||||
state.whatsNewContentHash = value;
|
state.whatsNewContentHash = value;
|
||||||
},
|
},
|
||||||
setDonationRemindDate(state, value) {
|
setDonationNextPopup(state, value) {
|
||||||
state.donationRemindDate = value;
|
state.donationNextPopup = value;
|
||||||
},
|
},
|
||||||
setCurrentProfile(state, value) {
|
setCurrentProfile(state, value) {
|
||||||
state.currentProfile = value;
|
state.currentProfile = value;
|
||||||
@@ -316,6 +352,10 @@ const mutations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
minuteMs,
|
||||||
|
hourMs,
|
||||||
|
dayMs,
|
||||||
|
|
||||||
readerActions,
|
readerActions,
|
||||||
toolButtons,
|
toolButtons,
|
||||||
hotKeys,
|
hotKeys,
|
||||||
@@ -323,7 +363,7 @@ export default {
|
|||||||
webFonts,
|
webFonts,
|
||||||
settingDefaults,
|
settingDefaults,
|
||||||
addDefaultsToSettings,
|
addDefaultsToSettings,
|
||||||
libsDefaults,
|
getLibsDefaults,
|
||||||
|
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -87,18 +87,22 @@ server {
|
|||||||
proxy_read_timeout 600s;
|
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 / {
|
location / {
|
||||||
root /home/beta.liberama/public;
|
root /home/beta.liberama/.liberama/public;
|
||||||
|
|
||||||
location /tmp {
|
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
|
||||||
add_header Content-Encoding gzip;
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /upload {
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
expires -1;
|
expires -1;
|
||||||
|
|||||||
@@ -32,18 +32,22 @@ server {
|
|||||||
proxy_read_timeout 600s;
|
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 / {
|
location / {
|
||||||
root /home/beta.liberama/public;
|
root /home/beta.liberama/.liberama/public;
|
||||||
|
|
||||||
location /tmp {
|
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
|
||||||
add_header Content-Encoding gzip;
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /upload {
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 /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 / {
|
location / {
|
||||||
root /home/beta.liberama/public;
|
root /home/beta.liberama/.liberama/public;
|
||||||
|
|
||||||
location /tmp {
|
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
|
||||||
add_header Content-Encoding gzip;
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /upload {
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
expires -1;
|
expires -1;
|
||||||
|
|||||||
@@ -43,18 +43,22 @@ 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 /upload {
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 /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 /upload {
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
expires -1;
|
expires -1;
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
### 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
|
||||||
```
|
```
|
||||||
|
|
||||||
### node.js
|
### node.js
|
||||||
```
|
```
|
||||||
sudo apt install -y curl
|
sudo apt install -y curl
|
||||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||||
sudo apt install -y nodejs
|
sudo apt install -y nodejs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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 mkdir -p /home/liberama/.liberama/calibre
|
||||||
sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/data/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
|
||||||
|
|||||||
@@ -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 {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
types { } default_type "application/xml; charset=utf-8";
|
expires -1;
|
||||||
add_header Content-Encoding gzip;
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
location /upload {
|
server {
|
||||||
try_files $uri @liberama;
|
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;
|
||||||
|
|||||||
@@ -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 /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 /upload {
|
|
||||||
try_files $uri @liberama;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:manifest|appcache|html)$ {
|
location ~* \.(?:manifest|appcache|html)$ {
|
||||||
expires -1;
|
expires -1;
|
||||||
|
|||||||
8029
package-lock.json
generated
8029
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
93
package.json
93
package.json
@@ -1,87 +1,90 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "liberama",
|
||||||
"version": "0.11.8",
|
"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",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.4.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 node14-linux-x64 -C GZip -o dist/linux/liberama .",
|
"build:linux": "npm run build:client && node build/prepkg.js linux && pkg -t node16-linux-x64 -C GZip -o dist/linux/liberama .",
|
||||||
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -C GZip -o dist/win/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.16.0",
|
"@babel/core": "^7.20.5",
|
||||||
"@babel/eslint-parser": "^7.16.3",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@babel/eslint-plugin": "^7.14.5",
|
"@babel/eslint-plugin": "^7.19.1",
|
||||||
"@babel/plugin-proposal-decorators": "^7.16.0",
|
"@babel/plugin-proposal-decorators": "^7.20.5",
|
||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.20.2",
|
||||||
"@vue/compiler-sfc": "^3.2.22",
|
"@vue/compiler-sfc": "^3.2.22",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^9.1.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.5.1",
|
"css-loader": "^6.7.3",
|
||||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
"css-minimizer-webpack-plugin": "^4.2.2",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.29.0",
|
||||||
"eslint-plugin-vue": "^9.2.0",
|
"eslint-plugin-vue": "^9.8.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"mini-css-extract-plugin": "^2.4.4",
|
"mini-css-extract-plugin": "^2.7.2",
|
||||||
"pkg": "^5.5.1",
|
"pkg": "^5.8.0",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"showdown": "^2.1.0",
|
||||||
"vue-eslint-parser": "^9.0.3",
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"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.64.1",
|
"webpack": "^5.75.0",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack-cli": "^5.0.1",
|
||||||
"webpack-dev-middleware": "^5.2.1",
|
"webpack-dev-middleware": "^6.0.1",
|
||||||
"webpack-hot-middleware": "^2.25.1",
|
"webpack-hot-middleware": "^2.25.3",
|
||||||
"webpack-merge": "^5.8.0",
|
"webpack-merge": "^5.8.0",
|
||||||
"workbox-webpack-plugin": "^6.4.1"
|
"workbox-webpack-plugin": "^6.5.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.12.0",
|
"@quasar/extras": "^1.15.8",
|
||||||
"@vue/compat": "^3.2.21",
|
"@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.17.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": "^3.0.8",
|
"jembadb": "^5.1.5",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"minimist": "^1.2.5",
|
"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",
|
"pidusage": "^3.0.2",
|
||||||
"quasar": "^2.7.5",
|
"quasar": "^2.10.2",
|
||||||
"safe-buffer": "^5.2.1",
|
"safe-buffer": "^5.2.1",
|
||||||
"sanitize-html": "^2.5.3",
|
"sanitize-html": "^2.8.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"sql-template-strings": "^2.2.2",
|
|
||||||
"sqlite": "^4.0.23",
|
|
||||||
"sqlite3": "^5.0.2",
|
|
||||||
"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.1",
|
"vue-router": "^4.1.6",
|
||||||
"vuex": "^4.0.2",
|
"vuex": "^4.1.0",
|
||||||
"vuex-persistedstate": "^4.1.0",
|
"vuex-persist": "^3.1.3",
|
||||||
"webdav": "^4.7.0",
|
"webdav": "^4.11.2",
|
||||||
"ws": "^8.2.3",
|
"ws": "^8.11.0",
|
||||||
"zip-stream": "^4.1.0"
|
"zip-stream": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
execDir,
|
||||||
tempDir: `${dataDir}/tmp`,
|
|
||||||
logDir: `${dataDir}/log`,
|
|
||||||
publicDir: `${execDir}/public`,
|
|
||||||
uploadDir: `${execDir}/public/upload`,
|
|
||||||
sharedDir: `${execDir}/public/shared`,
|
|
||||||
loggingEnabled: true,
|
loggingEnabled: true,
|
||||||
|
|
||||||
maxUploadFileSize: 50*1024*1024,//50Мб
|
maxUploadFileSize: 50*1024*1024,//50Мб
|
||||||
@@ -23,58 +18,61 @@ module.exports = {
|
|||||||
|
|
||||||
useExternalBookConverter: false,
|
useExternalBookConverter: false,
|
||||||
acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png',
|
acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png',
|
||||||
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch'],
|
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch'],
|
||||||
|
|
||||||
db: [
|
|
||||||
{
|
|
||||||
poolName: 'app',
|
|
||||||
connCount: 20,
|
|
||||||
fileName: 'app.sqlite',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
poolName: 'readerStorage',
|
|
||||||
connCount: 20,
|
|
||||||
fileName: 'reader-storage.sqlite',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
jembaDb: [
|
jembaDb: [
|
||||||
{
|
{
|
||||||
|
serverMode: ['reader', 'omnireader', 'liberama'],
|
||||||
dbName: 'app',
|
dbName: 'app',
|
||||||
thread: true,
|
thread: true,
|
||||||
openAll: true,
|
openAll: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
serverMode: ['reader', 'omnireader', 'liberama'],
|
||||||
dbName: 'reader-storage',
|
dbName: 'reader-storage',
|
||||||
thread: true,
|
thread: true,
|
||||||
openAll: true,
|
openAll: true,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
serverMode: 'book_update_checker',
|
||||||
|
dbName: 'book-update-server',
|
||||||
|
thread: true,
|
||||||
|
openAll: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
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',
|
||||||
|
mode: 'book_update_checker',
|
||||||
|
isHttps: true,
|
||||||
|
keysFile: 'server',
|
||||||
|
ip: '0.0.0.0',
|
||||||
|
port: '33443',
|
||||||
|
accessToken: '',
|
||||||
|
}*/
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
remoteWebDavStorage: false,
|
|
||||||
remoteWebDavStorage: {
|
|
||||||
url: '127.0.0.1:1900',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
remoteStorage: false,
|
remoteStorage: false,
|
||||||
/*
|
/*
|
||||||
remoteStorage: {
|
remoteStorage: {
|
||||||
url: 'https://127.0.0.1:11900',
|
url: 'wss://127.0.0.1:11900',
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
bucEnabled: false,
|
||||||
|
bucServer: false,
|
||||||
|
/*
|
||||||
|
bucServer: {
|
||||||
|
url: 'wss://127.0.0.1:33443',
|
||||||
|
accessToken: '',
|
||||||
|
}
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -10,7 +11,9 @@ const propsToSave = [
|
|||||||
'useExternalBookConverter',
|
'useExternalBookConverter',
|
||||||
|
|
||||||
'servers',
|
'servers',
|
||||||
'remoteWebDavStorage',
|
'remoteStorage',
|
||||||
|
'bucEnabled',
|
||||||
|
'bucServer',
|
||||||
];
|
];
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
@@ -27,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');
|
||||||
|
|
||||||
@@ -42,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);
|
if (dataDir) {
|
||||||
this._userConfigFile = `${this._config.dataDir}/config.json`;
|
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;
|
||||||
}
|
}
|
||||||
@@ -70,15 +80,28 @@ class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
if (!this.inited)
|
try {
|
||||||
throw new Error('not inited');
|
if (!this.inited)
|
||||||
if (!await fs.pathExists(this.userConfigFile)) {
|
throw new Error('not inited');
|
||||||
await this.save();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fs.readFile(this.userConfigFile, 'utf8');
|
if (await fs.pathExists(this.userConfigFile)) {
|
||||||
this.config = JSON.parse(data);
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
|
|||||||
@@ -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`,
|
execDir,
|
||||||
logDir: `${dataDir}/log`,
|
|
||||||
publicDir: `${execDir}/public`,
|
|
||||||
uploadDir: `${execDir}/public/upload`,
|
|
||||||
sharedDir: `${execDir}/public/shared`,
|
|
||||||
|
|
||||||
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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const WebSocket = require ('ws');
|
const WebSocket = require('ws');
|
||||||
//const _ = require('lodash');
|
//const _ = require('lodash');
|
||||||
|
|
||||||
|
const BUCServer = require('../core/BookUpdateChecker/BUCServer');
|
||||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
//const utils = require('../core/utils');
|
//const utils = require('../core/utils');
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ class BookUpdateCheckerController {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.isDevelopment = (config.branch == 'development');
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
//this.readerStorage = new JembaReaderStorage();
|
this.accessToken = config.accessToken;
|
||||||
|
this.bucServer = new BUCServer(config);
|
||||||
|
|
||||||
this.wss = wss;
|
this.wss = wss;
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ class BookUpdateCheckerController {
|
|||||||
let req = {};
|
let req = {};
|
||||||
try {
|
try {
|
||||||
if (this.isDevelopment) {
|
if (this.isDevelopment) {
|
||||||
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
log(`BUC-WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
req = JSON.parse(message);
|
req = JSON.parse(message);
|
||||||
@@ -56,9 +58,16 @@ class BookUpdateCheckerController {
|
|||||||
//pong for WebSocketConnection
|
//pong for WebSocketConnection
|
||||||
this.send({_rok: 1}, req, ws);
|
this.send({_rok: 1}, req, ws);
|
||||||
|
|
||||||
|
if (req.accessToken !== this.accessToken)
|
||||||
|
throw new Error('Access denied');
|
||||||
|
|
||||||
switch (req.action) {
|
switch (req.action) {
|
||||||
case 'test':
|
case 'test':
|
||||||
await this.test(req, ws); break;
|
await this.test(req, ws); break;
|
||||||
|
case 'get-buc':
|
||||||
|
await this.getBuc(req, ws); break;
|
||||||
|
case 'update-buc':
|
||||||
|
await this.updateBuc(req, ws); break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Action not found: ${req.action}`);
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
@@ -79,7 +88,7 @@ class BookUpdateCheckerController {
|
|||||||
ws.send(message);
|
ws.send(message);
|
||||||
|
|
||||||
if (this.isDevelopment) {
|
if (this.isDevelopment) {
|
||||||
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
log(`BUC-WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -90,6 +99,28 @@ class BookUpdateCheckerController {
|
|||||||
this.send({message: 'Liberama project is awesome'}, req, ws);
|
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBuc(req, ws) {
|
||||||
|
if (!req.fromCheckTime)
|
||||||
|
throw new Error(`key 'fromCheckTime' is empty`);
|
||||||
|
|
||||||
|
await this.bucServer.getBuc(req.fromCheckTime, (rows) => {
|
||||||
|
this.send({state: 'get', rows}, req, ws);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.send({state: 'finish'}, req, ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBuc(req, ws) {
|
||||||
|
if (!req.bookUrls)
|
||||||
|
throw new Error(`key 'bookUrls' is empty`);
|
||||||
|
|
||||||
|
if (!Array.isArray(req.bookUrls))
|
||||||
|
throw new Error(`key 'bookUrls' must be array`);
|
||||||
|
|
||||||
|
await this.bucServer.updateBuc(req.bookUrls);
|
||||||
|
|
||||||
|
this.send({state: 'success'}, req, ws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = BookUpdateCheckerController;
|
module.exports = BookUpdateCheckerController;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const _ = require('lodash');
|
|||||||
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
||||||
const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
|
const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
|
||||||
const WorkerState = require('../core/WorkerState');//singleton
|
const WorkerState = require('../core/WorkerState');//singleton
|
||||||
|
const BUCClient = require('../core/BookUpdateChecker/BUCClient');//singleton
|
||||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
const utils = require('../core/utils');
|
const utils = require('../core/utils');
|
||||||
|
|
||||||
@@ -19,6 +20,10 @@ class WebSocketController {
|
|||||||
this.readerWorker = new ReaderWorker(config);
|
this.readerWorker = new ReaderWorker(config);
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
|
|
||||||
|
if (config.bucEnabled) {
|
||||||
|
this.bucClient = new BUCClient(config);
|
||||||
|
}
|
||||||
|
|
||||||
this.wss = wss;
|
this.wss = wss;
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
@@ -66,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':
|
||||||
@@ -76,6 +83,8 @@ class WebSocketController {
|
|||||||
await this.uploadFileBuf(req, ws); break;
|
await this.uploadFileBuf(req, ws); break;
|
||||||
case 'upload-file-touch':
|
case 'upload-file-touch':
|
||||||
await this.uploadFileTouch(req, ws); break;
|
await this.uploadFileTouch(req, ws); break;
|
||||||
|
case 'check-buc':
|
||||||
|
await this.checkBuc(req, ws); break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Action not found: ${req.action}`);
|
throw new Error(`Action not found: ${req.action}`);
|
||||||
@@ -117,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`);
|
||||||
@@ -179,6 +204,21 @@ class WebSocketController {
|
|||||||
|
|
||||||
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
|
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkBuc(req, ws) {
|
||||||
|
if (!this.config.bucEnabled)
|
||||||
|
throw new Error('BookUpdateChecker disabled');
|
||||||
|
|
||||||
|
if (!req.bookUrls)
|
||||||
|
throw new Error(`key 'bookUrls' is empty`);
|
||||||
|
|
||||||
|
if (!Array.isArray(req.bookUrls))
|
||||||
|
throw new Error(`key 'bookUrls' must be array`);
|
||||||
|
|
||||||
|
const data = await this.bucClient.checkBuc(req.bookUrls);
|
||||||
|
|
||||||
|
this.send({state: 'success', data}, req, ws);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WebSocketController;
|
module.exports = WebSocketController;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user