Compare commits

..

165 Commits

Author SHA1 Message Date
Book Pauk
a9a3227433 Merge branch 'release/0.11.8-4' 2022-07-17 16:53:59 +07:00
Book Pauk
60cb3514b2 Тюнинг таймаутов 2022-07-17 16:53:12 +07:00
Book Pauk
4aeaa05f0b Merge tag '0.11.8-3' into develop
0.11.8-3
2022-07-17 15:58:34 +07:00
Book Pauk
9c06552278 Merge branch 'release/0.11.8-3' 2022-07-17 15:58:28 +07:00
Book Pauk
000f8dde82 Переход на RemoteStorage 2022-07-17 15:43:12 +07:00
Book Pauk
9ffc218002 Поправка 2022-07-16 21:36:50 +07:00
Book Pauk
68a188f099 Конфиг nginx 2022-07-16 21:10:33 +07:00
Book Pauk
8829bb3810 Конфиг nginx 2022-07-16 21:07:16 +07:00
Book Pauk
5164d2f536 Merge tag '0.11.8-2' into develop
0.11.8-2
2022-07-16 21:02:05 +07:00
Book Pauk
451538fcf7 Merge branch 'release/0.11.8-2' 2022-07-16 21:01:56 +07:00
Book Pauk
82a02ef339 Удаление более ненужной функциональности 2022-07-16 20:48:50 +07:00
Book Pauk
b834d4951f Обработка ошибок 2022-07-16 20:40:21 +07:00
Book Pauk
edc3b669be Добавлено восстановление файлов из webdav 2022-07-16 20:35:34 +07:00
Book Pauk
522826311d Переделка механизма чистки папок и отправки через RemoteWebDavStorage 2022-07-16 20:24:37 +07:00
Book Pauk
e69b9951d5 Отключил проверку валидности tls-сертификата 2022-07-16 18:43:09 +07:00
Book Pauk
c6300222ea Мелкий рефакторинг 2022-07-16 17:54:27 +07:00
Book Pauk
5aa6ee899c Изменение механизма работы с /tmp и /upload (начало) 2022-07-16 17:35:32 +07:00
Book Pauk
4b76f97d2b Поправки конфигов nginx 2022-07-16 15:45:52 +07:00
Book Pauk
5ccfe71c55 Начало работы над BookUpdateChecker 2022-07-16 13:16:57 +07:00
Book Pauk
97fc902cdb Поправлен баг 2022-07-15 23:53:54 +07:00
Book Pauk
7e935951d7 Поправка разметки 2022-07-15 23:17:30 +07:00
Book Pauk
810c6d68d2 Поправка разметки 2022-07-15 23:14:09 +07:00
Book Pauk
003dc70f4f Merge tag '0.11.8-1' into develop
0.11.8-1
2022-07-15 18:14:12 +07:00
Book Pauk
371ff64a95 Merge branch 'release/0.11.8-1' 2022-07-15 18:14:06 +07:00
Book Pauk
b0de5adbf3 Добавлена возможность скачивать обои 2022-07-15 18:11:24 +07:00
Book Pauk
d1d2b07c33 Поправки разметки 2022-07-15 17:42:19 +07:00
Book Pauk
d9b2444c1a Улучшен механизм загрузки обложек 2022-07-15 17:36:49 +07:00
Book Pauk
e7fae27031 Убрал отладку 2022-07-15 17:17:00 +07:00
Book Pauk
eb0c7b0a32 Отладка 2022-07-15 17:11:58 +07:00
Book Pauk
3d7ad0dd9a Небюольшие оптимизации загрузки обложек 2022-07-15 17:05:17 +07:00
Book Pauk
ae04feb311 Merge tag '0.11.8' into develop
0.11.8
2022-07-15 02:11:03 +07:00
Book Pauk
7b59f911ef Merge branch 'release/0.11.8' 2022-07-15 02:10:58 +07:00
Book Pauk
d3444da647 Поправки разметки 2022-07-15 01:58:42 +07:00
Book Pauk
66738d0c9c К предыдущему 2022-07-15 01:51:28 +07:00
Book Pauk
7e187acd68 Версия 0.11.8 2022-07-15 01:50:17 +07:00
Book Pauk
c751372a54 Добавлен resizeImage 2022-07-15 01:38:25 +07:00
Book Pauk
7fc98fc7da Добавление отображения обложки (coverpage) в окне загруженных файлов 2022-07-15 00:47:24 +07:00
Book Pauk
b56f45694e Добавлен coversStorage для хранения coverpage 2022-07-15 00:45:56 +07:00
Book Pauk
091ca521ef Новые upload-методы 2022-07-15 00:45:09 +07:00
Book Pauk
c7a17b0a76 Добавлена синхронизация файлов обоев 2022-07-14 20:14:40 +07:00
Book Pauk
26468b996a Мелкая поправка 2022-07-14 20:12:37 +07:00
Book Pauk
c4e240d87c Увеличил maxPayloadSize 2022-07-14 20:11:17 +07:00
Book Pauk
04713f47c8 Небольшие поправки 2022-07-14 16:14:25 +07:00
Book Pauk
37ab3493db Merge tag '0.11.7-6' into develop
0.11.7-6
2022-07-14 03:52:50 +07:00
Book Pauk
a4cb3c628e Merge branch 'release/0.11.7-6' 2022-07-14 03:52:44 +07:00
Book Pauk
8492da8a13 Небольшое улучшение 2022-07-14 03:51:59 +07:00
Book Pauk
98d7c64a56 Исправление багов 2022-07-14 03:34:55 +07:00
Book Pauk
25f121e5ed Merge tag '0.11.7-5' into develop
0.11.7-5
2022-07-14 01:57:36 +07:00
Book Pauk
4c8797c99c Merge branch 'release/0.11.7-5' 2022-07-14 01:57:30 +07:00
Book Pauk
1155aa285d Лишние пробелы 2022-07-14 01:57:03 +07:00
Book Pauk
239bbb8263 Добавлено восстановление из архива 2022-07-14 01:55:09 +07:00
Book Pauk
e6b9330108 Добавление работы с архивом 2022-07-14 01:17:09 +07:00
Book Pauk
935b767c2e Поправил поведение buttonActiveClass 2022-07-14 00:31:24 +07:00
Book Pauk
8acf3295b5 Поправил разметку 2022-07-14 00:31:09 +07:00
Book Pauk
48c3a12fa0 Улучшение парсинга плохих fb2 2022-07-14 00:30:27 +07:00
Book Pauk
a1dea514b7 Поправка разметки 2022-07-13 23:47:55 +07:00
Book Pauk
d4788439cb Merge tag '0.11.7-4' into develop
0.11.7-4
2022-07-13 16:38:10 +07:00
Book Pauk
0a60ad354c Merge branch 'release/0.11.7-4' 2022-07-13 16:38:04 +07:00
Book Pauk
c565a20344 Поправки разметки 2022-07-13 16:37:47 +07:00
Book Pauk
735ee88f0b Merge tag '0.11.7-3' into develop
0.11.7-3
2022-07-13 16:34:22 +07:00
Book Pauk
9405ce2cc0 Merge branch 'release/0.11.7-3' 2022-07-13 16:34:16 +07:00
Book Pauk
115277d88a Поправки разметки 2022-07-13 16:34:00 +07:00
Book Pauk
6925c11dbd Merge tag '0.11.7-2' into develop
0.11.7-2
2022-07-13 16:25:11 +07:00
Book Pauk
984d835892 Merge branch 'release/0.11.7-2' 2022-07-13 16:25:05 +07:00
Book Pauk
23353a4960 Улучшен парсинг fb2 2022-07-13 16:23:52 +07:00
Book Pauk
955bcda032 Поправки разметки 2022-07-13 15:01:35 +07:00
Book Pauk
81ad5d7a2c Поправки разметки 2022-07-13 14:47:24 +07:00
Book Pauk
dada7980ec Merge tag '0.11.7-1' into develop
0.11.7-1
2022-07-12 19:23:38 +07:00
Book Pauk
511a308646 Merge branch 'release/0.11.7-1' 2022-07-12 19:23:33 +07:00
Book Pauk
65c8f2cc81 Небольшие поправки на панели, изменена нумерация на обратную 2022-07-12 19:21:26 +07:00
Book Pauk
238c18bc48 Merge tag '0.11.7' into develop
0.11.7
2022-07-12 19:08:35 +07:00
Book Pauk
873a08fee1 Merge branch 'release/0.11.7' 2022-07-12 19:08:27 +07:00
Book Pauk
7e89228803 Версия 0.11.7 2022-07-12 19:07:39 +07:00
Book Pauk
fc630923a4 Настройка методов сортировки 2022-07-12 18:50:35 +07:00
Book Pauk
928f911d03 Добавлены подсказки к кнопкам 2022-07-12 17:53:14 +07:00
Book Pauk
7ffcd3fe1b Поправки поведения при скроллинге 2022-07-12 17:33:03 +07:00
Book Pauk
0efbaf643a Поправил сообщение об ошибке 2022-07-12 17:32:19 +07:00
Book Pauk
f1bf8e54ae Добавлен метод scrollToActiveBook 2022-07-12 17:10:50 +07:00
Book Pauk
b4aa6ab6c8 Поправки поиска 2022-07-12 16:58:34 +07:00
Book Pauk
72431f0202 Работа над группировкой 2022-07-12 16:51:32 +07:00
Book Pauk
04a326c0e4 Работа над группировкой 2022-07-12 15:51:43 +07:00
Book Pauk
931966f4f3 Поправки разметки 2022-07-12 15:05:17 +07:00
Book Pauk
8808cc4779 Работа над группировкой по файлам 2022-07-12 14:46:34 +07:00
Book Pauk
988c959eba Работа над группировкой файлов 2022-07-12 04:05:51 +07:00
Book Pauk
c0b658d9e6 К предыдущему 2022-07-12 01:41:18 +07:00
Book Pauk
3190246f34 Улучшена реакция на onResize 2022-07-12 01:35:19 +07:00
Book Pauk
d957b4a5f9 Добавлена возможность автосокрытия панели при прокрутке 2022-07-12 01:03:44 +07:00
Book Pauk
bef9e5705c Поправки текстовых строк 2022-07-11 23:53:54 +07:00
Book Pauk
eb2affa518 Приведение input к единому стилю 2022-07-11 23:50:51 +07:00
Book Pauk
07b9a3c033 Мелкие правки 2022-07-11 22:28:48 +07:00
Book Pauk
3ca14ae06a Работа над группировкой 2022-07-11 22:26:34 +07:00
Book Pauk
7caa0c2112 Начало добавления группировки в RecentBooksPage 2022-07-11 20:11:38 +07:00
Book Pauk
9c69f5bc01 Поправил размер иконки 2022-07-11 20:10:51 +07:00
Book Pauk
125a2e0f17 Исправление багов 2022-07-11 17:12:17 +07:00
Book Pauk
1b4360b897 Дополнение в convertRecent 2022-07-11 16:26:03 +07:00
Book Pauk
4775d6e47b Поправлен баг 2022-07-10 20:07:33 +07:00
Book Pauk
33fc553c55 Добавлен запрос на объединение позиций при
обнаружении похожего файла в загруженных
2022-07-10 19:54:00 +07:00
Book Pauk
25cad81c50 Улучшение отображения загруженных 2022-07-10 19:53:30 +07:00
Book Pauk
02a2099c1f Поправлен z-index 2022-07-10 19:52:58 +07:00
Book Pauk
1cda186b1a Добавлен диалог askYesNo 2022-07-10 19:52:29 +07:00
Book Pauk
f10291b6c6 Поправка названия действия 2022-07-10 19:51:31 +07:00
Book Pauk
26ab5d6765 Рефакторинг 2022-07-10 18:27:05 +07:00
Book Pauk
5edeed0747 Изменение механизма хранения книг 2022-07-10 17:31:21 +07:00
Book Pauk
c878ce432f Небольшое исправление опознававния кодировки 2022-07-10 17:20:47 +07:00
Book Pauk
81798897c8 Изменения в механизме хранения книг:
теперь ориентируемся на "ключ-filepath", а не "ключ-url"
2022-07-10 16:38:54 +07:00
Book Pauk
63840fadbc К предыдущему 2022-07-10 14:59:39 +07:00
Book Pauk
36aa057035 Поправка цвета 2022-07-09 21:00:09 +07:00
Book Pauk
30afd2421c Рефакторинг 2022-07-09 20:50:31 +07:00
Book Pauk
53a1d90bd8 Улучшение поведения при очереди загрузки книг 2022-07-09 02:01:14 +07:00
Book Pauk
2ecf6beef2 Небольшой багфикс 2022-07-09 01:56:42 +07:00
Book Pauk
85910a20e9 Улучшение ContentsPage 2022-07-08 20:50:55 +07:00
Book Pauk
66cf7790b3 Улучшения ContentsPage 2022-07-08 19:09:57 +07:00
Book Pauk
4a9eb7e4bb Удалил устаревшее 2022-07-08 14:30:44 +07:00
Book Pauk
07446696c1 Поправлен цвет заголовка 2022-07-08 13:52:45 +07:00
Book Pauk
a29f9d9a4b Унификация размеров окон 2022-07-08 13:43:59 +07:00
Book Pauk
d49c9baec3 Унификация интерфейса 2022-07-08 13:34:53 +07:00
Book Pauk
8c9d4a12ee Настройка цветов 2022-07-08 13:24:13 +07:00
Book Pauk
fce69e4657 Настройка цветов 2022-07-08 13:21:42 +07:00
Book Pauk
b387509f88 Добавил блокировку при загрузке книг, теперь загружаются последовательно 2022-07-08 12:26:47 +07:00
Book Pauk
8dc8bdc0d6 Merge tag '0.11.6-2' into develop
0.11.6-2
2022-07-07 19:43:47 +07:00
Book Pauk
00caae8363 Merge branch 'release/0.11.6-2' 2022-07-07 19:43:40 +07:00
Book Pauk
2ead8570a7 Небольшая поправка 2022-07-07 19:39:02 +07:00
Book Pauk
408315466b Частичный откат предыдущих изменений 2022-07-07 19:38:17 +07:00
Book Pauk
c651836554 Поправки скриптов запуска 2022-07-07 19:33:32 +07:00
Book Pauk
03a1e70fce Поправки, чтобы не падал в случае детача скрина 2022-07-07 19:05:54 +07:00
Book Pauk
ab5a11a24f Убрал сайт flibs.in из сетевых библиотек 2022-07-07 17:42:05 +07:00
Book Pauk
8cd6ed472c Изменил client_max_body_size 100m 2022-07-07 17:37:25 +07:00
Book Pauk
055181b744 Исправлен баг выпадающих списков в оглавлении 2022-07-07 17:34:03 +07:00
Book Pauk
e331a3920b Актуализация пакетов 2022-07-07 17:29:47 +07:00
Book Pauk
c62bccb470 Улучшил журналирование ошибок БД 2022-07-07 16:24:59 +07:00
Book Pauk
ea351ea293 Merge tag '0.11.6-1' into develop
0.11.6-1
2022-07-04 12:23:55 +07:00
Book Pauk
d806a07c60 Merge branch 'release/0.11.6-1' 2022-07-04 12:23:48 +07:00
Book Pauk
c0ea096f1f Обновил jembadb 2022-07-04 12:22:27 +07:00
Book Pauk
011d4a1672 Merge tag '0.11.6' into develop
0.11.6
2022-07-02 17:41:42 +07:00
Book Pauk
4836a737c6 Merge branch 'release/0.11.6' 2022-07-02 17:41:34 +07:00
Book Pauk
5712b2ee17 Версия 0.11.6 2022-07-02 17:40:28 +07:00
Book Pauk
32dd17694e Улучшено копирование текстов со страницы 2022-07-02 17:36:12 +07:00
Book Pauk
3ebc932a6a Поправил список расширений 2022-07-02 14:46:22 +07:00
Book Pauk
8f351d9bef Удалил неиспользуемый код 2022-07-02 14:18:16 +07:00
Book Pauk
5ae3ea94e4 Добавлены типы файлов в диалог загрузки 2022-07-02 13:57:44 +07:00
Book Pauk
f203d453a4 Актуализация пакетов 2022-07-02 13:21:30 +07:00
Book Pauk
0d5cba121b Мелкий рефакторинг 2022-07-02 13:02:22 +07:00
Book Pauk
0cd6a48a46 Актуализация пакетов 2022-07-02 12:59:07 +07:00
Book Pauk
4e07ce2b5c Актуализация пакетов 2022-07-02 12:55:39 +07:00
Book Pauk
85a525e301 Актуализация пакета base-x 2022-07-02 12:46:10 +07:00
Book Pauk
03e4a6d723 Мелкий рефакторинг 2022-07-02 12:36:59 +07:00
Book Pauk
ab28af1abe Актуализация пакетов 2022-07-02 12:16:52 +07:00
Book Pauk
7fceed5301 Переход на axios 2022-07-02 12:16:19 +07:00
Book Pauk
0077816afa Улучшена обработка и журналирование ошибок 2022-07-02 12:07:42 +07:00
Book Pauk
cb01423147 Поправил настройки прокси 2022-07-02 00:00:13 +07:00
Book Pauk
61b0712d36 Переход на axios 2022-07-01 21:38:32 +07:00
Book Pauk
12d7843377 Merge tag '0.11.5' into develop
0.11.5
2022-04-15 16:42:40 +07:00
Book Pauk
9293c0a0d4 Merge branch 'release/0.11.5' 2022-04-15 16:42:35 +07:00
Book Pauk
bb9522197a 0.11.5 2022-04-15 16:41:24 +07:00
Book Pauk
450a2e0664 Поправки css 2022-04-15 16:38:34 +07:00
Book Pauk
41e35f3ec8 Поправки css 2022-04-15 16:09:41 +07:00
Book Pauk
a9bc98abe3 Рефакторинг 2022-04-15 15:12:28 +07:00
Book Pauk
47bca03532 Поправки подсказок 2022-04-15 15:02:02 +07:00
Book Pauk
942021371c Merge tag '0.11.4-2' into develop
0.11.4-2
2022-04-14 19:54:20 +07:00
Book Pauk
ea2f178730 Merge branch 'release/0.11.4-2' 2022-04-14 19:54:14 +07:00
Book Pauk
4b5c8d9efe Добавил подсказку 2022-04-14 19:53:47 +07:00
Book Pauk
28ebf13c3a Merge tag '0.11.4-1' into develop
0.11.4-1
2022-04-14 19:19:21 +07:00
Book Pauk
5d52e63dd9 Merge branch 'release/0.11.4-1' 2022-04-14 19:19:14 +07:00
Book Pauk
1a0e024050 Поправил баг 2022-04-14 19:18:49 +07:00
Book Pauk
e627a0d970 Merge tag '0.11.4' into develop
0.11.4
2022-04-14 19:05:36 +07:00
66 changed files with 8048 additions and 6440 deletions

View File

@@ -4,7 +4,7 @@ const util = require('util');
const stream = require('stream'); const stream = require('stream');
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
const got = require('got'); const axios = require('axios');
const FileDecompressor = require('../server/core/FileDecompressor'); const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist'); const distDir = path.resolve(__dirname, '../dist');
@@ -29,7 +29,8 @@ async function main() {
if (!await fs.pathExists(sqliteDecompressedFilename)) { if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку // Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`)); 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(`done downloading ${sqliteRemoteUrl}`);
//распаковываем //распаковываем
@@ -46,7 +47,8 @@ async function main() {
// Скачиваем ipfs // Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz'; const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.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(`done downloading ${ipfsRemoteUrl}`);
//распаковываем //распаковываем

View File

@@ -4,7 +4,7 @@ const util = require('util');
const stream = require('stream'); const stream = require('stream');
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
const got = require('got'); const axios = require('axios');
const FileDecompressor = require('../server/core/FileDecompressor'); const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist'); const distDir = path.resolve(__dirname, '../dist');
@@ -29,7 +29,8 @@ async function main() {
if (!await fs.pathExists(sqliteDecompressedFilename)) { if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку // Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`)); 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(`done downloading ${sqliteRemoteUrl}`);
//распаковываем //распаковываем
@@ -46,7 +47,8 @@ async function main() {
// Скачиваем ipfs // Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip'; const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.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(`done downloading ${ipfsRemoteUrl}`);
//распаковываем //распаковываем

View File

@@ -9,7 +9,7 @@ class Misc {
async loadConfig() { async loadConfig() {
const query = {params: [ const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch', 'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
]}; ]};
try { try {

View File

@@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
import * as cryptoUtils from '../share/cryptoUtils';
import wsc from './webSocketConnection'; import wsc from './webSocketConnection';
const api = axios.create({ const api = axios.create({
@@ -119,32 +120,7 @@ class Reader {
estSize = response.headers['content-length']; estSize = response.headers['content-length'];
} }
} catch (e) { } catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака //
let response = null
try {
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
if (response.state == 'error') {
throw new Error(response.error);
}
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
} }
return estSize; return estSize;
@@ -174,11 +150,10 @@ class Reader {
return await axios.get(url, options); return await axios.get(url, options);
} }
async uploadFile(file, maxUploadFileSize, callback) { async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
if (!maxUploadFileSize)
maxUploadFileSize = 10*1024*1024;
if (file.size > maxUploadFileSize) if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`); throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData(); let formData = new FormData();
formData.append('file', file, file.name); formData.append('file', file, file.name);
@@ -225,6 +200,35 @@ class Reader {
return response; return response;
} }
makeUrlFromBuf(buf) {
const key = utils.toHex(cryptoUtils.sha256(buf));
return `disk://${key}`;
}
async uploadFileBuf(buf, url) {
if (!url)
url = this.makeUrlFromBuf(buf);
let response;
try {
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
} catch (e) {
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
}
if (response.error)
throw new Error(response.error);
return response;
}
async getUploadedFileBuf(url) {
url = url.replace('disk://', '/upload/');
return (await axios.get(url)).data;
}
} }
export default new Reader(); export default new Reader();

View File

@@ -11,7 +11,7 @@
Открыть выбранную закладку Открыть выбранную закладку
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-input ref="search" v-model="search" class="col" rounded outlined dense bg-color="white" placeholder="Найти"> <q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
<template #append> <template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" /> <q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
</template> </template>

View File

@@ -5,19 +5,19 @@
</template> </template>
<template #buttons> <template #buttons>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle"> <span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" /> <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)"> <span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<q-icon name="la la-plus" size="16px" /> <q-icon name="la la-plus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)"> <span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<q-icon name="la la-minus" size="16px" /> <q-icon name="la la-minus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp"> <span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
<q-icon name="la la-question-circle" size="16px" /> <q-icon name="la la-question-circle" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
</span> </span>
@@ -32,7 +32,7 @@
:options="rootLinkOptions" :options="rootLinkOptions"
style="width: 230px" style="width: 230px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize outlined dense emit-value map-options display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide" @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
> >
<template #prepend> <template #prepend>
@@ -61,7 +61,7 @@
:options="selectedLinkOptions" :options="selectedLinkOptions"
style="width: 50px" style="width: 50px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide" @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
> >
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -73,9 +73,9 @@
ref="input" ref="input"
v-model="bookUrl" v-model="bookUrl"
class="col q-mr-sm" class="col q-mr-sm"
rounded outlined dense outlined dense
bg-color="white" bg-color="white"
placeholder="Скопируйте сюда URL книги" placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown" @focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
> >
<template #prepend> <template #prepend>
@@ -99,7 +99,7 @@
</template> </template>
</q-input> </q-input>
<q-btn :disabled="!bookUrl" rounded color="green-7" no-caps size="14px" @click="submitUrl"> <q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
Открыть Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть в читалке Открыть в читалке
@@ -894,14 +894,15 @@ export default vueComponent(ExternalLibs);
background-color: #A0A0A0; background-color: #A0A0A0;
} }
.full-screen-button { .header-button {
width: 30px; width: 30px;
height: 30px; height: 30px;
cursor: pointer; cursor: pointer;
} }
.full-screen-button:hover { .header-button:hover {
background-color: #69C05F; color: white;
background-color: #39902F;
} }
.transparent-layout { .transparent-layout {

View File

@@ -23,15 +23,15 @@
<div class="q-mb-sm" /> <div class="q-mb-sm" />
<div v-show="selectedTab == 'contents'" class="tab-panel"> <div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
<div> <div>
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px"> <div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}"> <div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)"> <div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" /> <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
</div> </div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)"> <div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px" /> <q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
</div> </div>
<div class="col row clickable" @click="setBookPos(item.offset)"> <div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div> <div :style="item.indentStyle"></div>
@@ -42,8 +42,12 @@
</div> </div>
</div> </div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition"> <div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"> <div
v-for="subitem in item.list"
:ref="`subitem${subitem.key}`"
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
>
<div class="col row clickable" @click="setBookPos(subitem.offset)"> <div class="col row clickable" @click="setBookPos(subitem.offset)">
<div class="no-expand-button"></div> <div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div> <div :style="subitem.indentStyle"></div>
@@ -61,10 +65,10 @@
</div> </div>
</div> </div>
<div v-show="selectedTab == 'images'" class="tab-panel"> <div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<div> <div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px"> <div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}"> <div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)"> <div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center"> <div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"> <div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
@@ -124,7 +128,10 @@ const componentOptions = {
watch: { watch: {
bookPos() { bookPos() {
this.updateBookPosSelection(); this.updateBookPosSelection();
} },
selectedTab() {
this.updateBookPosScrollTop();
},
}, },
}; };
class ContentsPage { class ContentsPage {
@@ -282,31 +289,30 @@ class ContentsPage {
if (!this.isVisible) if (!this.isVisible)
return; return;
await utils.sleep(50); await this.$nextTick();
const bp = this.bookPos; const bp = this.bookPos;
for (let i = 0; i < this.contents.length; i++) { for (let i = 0; i < this.contents.length; i++) {
const item = this.contents[i]; const item = this.contents[i];
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength); const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
if (bp >= item.offset && bp < nextOffset) {
item.isBookPos = true;
} else if (item.isBookPos) {
item.isBookPos = false;
}
for (let j = 0; j < item.list.length; j++) { for (let j = 0; j < item.list.length; j++) {
const subitem = item.list[j]; const subitem = item.list[j];
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset); const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
if (bp >= subitem.offset && bp < nextSubOffset) { if (bp >= subitem.offset && bp < nextSubOffset) {
subitem.isBookPos = true; subitem.isBookPos = true;
this.contents[i] = Object.assign(item, {list: item.list}); this.updateBookPosScrollTop('contents', item, subitem, j);
} else if (subitem.isBookPos) { } else if (subitem.isBookPos) {
subitem.isBookPos = false; subitem.isBookPos = false;
this.contents[i] = Object.assign(item, {list: item.list});
} }
} }
if (bp >= item.offset && bp < nextOffset) {
this.contents[i] = Object.assign(item, {isBookPos: true});
} else if (item.isBookPos) {
this.contents[i] = Object.assign(item, {isBookPos: false});
}
} }
for (let i = 0; i < this.images.length; i++) { for (let i = 0; i < this.images.length; i++) {
@@ -314,11 +320,96 @@ class ContentsPage {
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength); const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
if (bp >= img.offset && bp < nextOffset) { if (bp >= img.offset && bp < nextOffset) {
this.images[i] = Object.assign(img, {isBookPos: true}); this.images[i].isBookPos = true;
} else if (img.isBookPos) { } else if (img.isBookPos) {
this.images[i] = Object.assign(img, {isBookPos: false}); this.images[i].isBookPos = false;
} }
} }
this.updateBookPosScrollTop();
}
/*getOffsetTop(key) {
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
return (el ? el.offsetTop : 0);
}*/
async updateBookPosScrollTop() {
try {
await this.$nextTick();
if (this.selectedTab == 'contents') {
let item;
let subitem;
let i;
//ищем выделенные item
for(const _item of this.contents) {
if (_item.isBookPos) {
item = _item;
for (let ii = 0; ii < item.list.length; ii++) {
const _subitem = item.list[ii];
if (_subitem.isBookPos) {
subitem = _subitem;
i = ii;
break;
}
}
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
let elShift = 0;
if (subitem && item.expanded) {
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
} else {
elShift = el.offsetHeight;
}
const tabPanel = this.$refs.tabPanelContents;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - elShift;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
if (this.selectedTab == 'images') {
let item;
//ищем выделенные item
for(const _item of this.images) {
if (_item.isBookPos) {
item = _item;
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
const tabPanel = this.$refs.tabPanelImages;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
} catch (e) {
console.error(e);
}
}
getFirstElem(items) {
return (Array.isArray(items) ? items[0] : items);
} }
async expandClick(key) { async expandClick(key) {
@@ -326,17 +417,17 @@ class ContentsPage {
const expanded = !item.expanded; const expanded = !item.expanded;
if (!expanded) { if (!expanded) {
const subitems = this.$refs[`subitem${key}`]; let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subitems.style.height = '0'; subdiv.style.height = '0';
await utils.sleep(200); await utils.sleep(200);
} }
this.contents[key] = Object.assign({}, item, {expanded}); this.contents[key].expanded = expanded;
if (expanded) { if (expanded) {
await this.$nextTick(); await this.$nextTick();
const subitems = this.$refs[`subitem${key}`]; let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subitems.style.height = subitems.scrollHeight + 'px'; subdiv.style.height = subdiv.scrollHeight + 'px';
} }
} }

View File

@@ -5,13 +5,20 @@
</template> </template>
<div class="col column" style="min-width: 600px"> <div class="col column" style="min-width: 600px">
<q-btn-toggle <div class="bg-grey-3 row">
v-model="selectedTab" <q-tabs
toggle-color="primary" v-model="selectedTab"
no-caps unelevated active-color="black"
:options="buttons" active-bg-color="white"
/> indicator-color="white"
<div class="separator"></div> dense
no-caps
inline-label
class="bg-grey-4 text-grey-7"
>
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
</q-tabs>
</div>
<keep-alive> <keep-alive>
<component :is="activePage" ref="page" class="col"></component> <component :is="activePage" ref="page" class="col"></component>
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
</script> </script>
<style scoped> <style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style> </style>

View File

@@ -12,13 +12,21 @@
</div> </div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden"> <div class="col-auto column justify-start items-center no-wrap overflow-hidden">
<q-input ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" placeholder="URL книги" @keydown="onInputKeydown"> <q-input
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
>
<template #append> <template #append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" /> <q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
</template> </template>
</q-input> </q-input>
<input id="file" ref="file" type="file" style="display: none;" @change="loadFile" /> <input
id="file" ref="file" type="file"
style="display: none;"
:accept="acceptFileExt"
@change="loadFile"
/>
<div class="q-my-sm"></div> <div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick"> <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
@@ -47,6 +55,7 @@
</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-->
@@ -55,6 +64,18 @@
</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>
@@ -64,6 +85,7 @@ import vueComponent from '../../vueComponent.js';
import GithubCorner from './GithubCorner/GithubCorner.vue'; import GithubCorner from './GithubCorner/GithubCorner.vue';
import Dialog from '../../share/Dialog.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue'; import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
@@ -71,6 +93,7 @@ import * as utils from '../../../share/utils';
const componentOptions = { const componentOptions = {
components: { components: {
GithubCorner, GithubCorner,
Dialog,
PasteTextPage, PasteTextPage,
}, },
}; };
@@ -80,6 +103,7 @@ 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;
@@ -112,6 +136,10 @@ class LoaderPage {
return this.$store.state.config.version; return this.$store.state.config.version;
} }
get acceptFileExt() {
return this.$store.state.config.acceptFileExt;
}
get isExternalConverter() { get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter; return this.$store.state.config.useExternalBookConverter;
} }
@@ -165,6 +193,10 @@ 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');
} }
@@ -181,6 +213,9 @@ 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);
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)"> <div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
<div class="column justify-start items-center" style="height: 250px"> <div class="column justify-start items-center" style="height: 250px">
<q-circular-progress <q-circular-progress
show-value show-value

View File

@@ -2,7 +2,7 @@
<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 justify-between no-wrap">
<div> <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%">
@@ -29,7 +29,8 @@
</button> </button>
</div> </div>
<div> <div class="row no-wrap">
<div class="space"></div>
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')"> <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-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%">
@@ -104,9 +105,10 @@
{{ rstore.readerActions['recentBooks'] }} {{ rstore.readerActions['recentBooks'] }}
</q-tooltip> </q-tooltip>
</button> </button>
<div class="space"></div>
</div> </div>
<div> <div class="row no-wrap">
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')"> <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-icon name="la la-mouse" 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%">
@@ -139,6 +141,7 @@
@load-file="loadFile" @load-file="loadFile"
@book-pos-changed="bookPosChanged" @book-pos-changed="bookPosChanged"
@do-action="doAction" @do-action="doAction"
@hide-tool-bar="hideToolBar"
></component> ></component>
</keep-alive> </keep-alive>
@@ -160,7 +163,7 @@
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage> <ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage> <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs> <ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
</div> </div>
</div> </div>
</template> </template>
@@ -191,6 +194,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager'; import bookManager from './share/bookManager';
import wallpaperStorage from './share/wallpaperStorage'; import wallpaperStorage from './share/wallpaperStorage';
import coversStorage from './share/coversStorage';
import dynamicCss from '../../share/dynamicCss'; import dynamicCss from '../../share/dynamicCss';
import rstore from '../../store/modules/reader'; import rstore from '../../store/modules/reader';
@@ -199,6 +203,7 @@ import miscApi from '../../api/misc';
import {versionHistory} from './versionHistory'; import {versionHistory} from './versionHistory';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import LockQueue from '../../share/LockQueue';
const componentOptions = { const componentOptions = {
components: { components: {
@@ -311,6 +316,8 @@ class Reader {
this.reader = this.$store.state.reader; this.reader = this.$store.state.reader;
this.config = this.$store.state.config; this.config = this.$store.state.config;
this.lock = new LockQueue(100);
this.$root.addEventHook('key', this.keyHook); this.$root.addEventHook('key', this.keyHook);
this.lastActivePage = false; this.lastActivePage = false;
@@ -343,6 +350,13 @@ class Reader {
this.debouncedSetRecentBook(newValue); this.debouncedSetRecentBook(newValue);
}, 15000, {maxWait: 20000}); }, 15000, {maxWait: 20000});
this.debouncedHideToolBar = _.debounce((event) => {
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
this.commit('reader/setToolBarActive', !!event.show);
this.$root.eventHook('resize');
}
}, 200);
document.addEventListener('fullscreenchange', () => { document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null); this.fullScreenActive = (document.fullscreenElement !== null);
}); });
@@ -351,10 +365,10 @@ class Reader {
} }
mounted() { mounted() {
this.updateHeaderMinWidth();
(async() => { (async() => {
await wallpaperStorage.init(); await wallpaperStorage.init();
await coversStorage.init();
await bookManager.init(this.settings); await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent); bookManager.addEventListener(this.bookManagerEvent);
@@ -402,6 +416,7 @@ class Reader {
this.clickControlActive = this.clickControl; this.clickControlActive = this.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton; this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.enableSitesFilter = settings.enableSitesFilter; this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify; this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara; this.splitToPara = settings.splitToPara;
@@ -416,8 +431,6 @@ class Reader {
return this.readerActionByKeyCode[utils.keyEventToCode(event)]; return this.readerActionByKeyCode[utils.keyEventToCode(event)];
} }
this.updateHeaderMinWidth();
this.loadWallpapers();//no await this.loadWallpapers();//no await
} }
@@ -440,22 +453,47 @@ class Reader {
//wallpaper css //wallpaper css
async loadWallpapers() { async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength(); if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
this.wallpaperDataLength = wallpaperDataLength;
let newCss = ''; let newCss = '';
let updated = false;
const wallpaperExists = new Set();
for (const wp of this.userWallpapers) { for (const wp of this.userWallpapers) {
const data = await wallpaperStorage.getData(wp.cssClass); wallpaperExists.add(wp.cssClass);
let data = await wallpaperStorage.getData(wp.cssClass);
if (!data) { if (!data) {
//здесь будем восстанавливать данные с сервера //здесь будем восстанавливать данные с сервера
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
try {
data = await readerApi.getUploadedFileBuf(url);
await wallpaperStorage.setData(wp.cssClass, data);
updated = true;
} catch (e) {
console.error(e);
}
} }
if (data) { if (data) {
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`; newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
} }
} }
//почистим wallpaperStorage
for (const key of await wallpaperStorage.getKeys()) {
if (!wallpaperExists.has(key)) {
await wallpaperStorage.removeData(key);
}
}
//обновим settings, если загружали обои из /upload/
if (updated) {
const newSettings = _.cloneDeep(this.settings);
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
this.commit('reader/setSettings', newSettings);
}
dynamicCss.replace('wallpapers', newCss); dynamicCss.replace('wallpapers', newCss);
} }
} }
@@ -484,17 +522,6 @@ class Reader {
} }
} }
updateHeaderMinWidth() {
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
if (this.$refs.buttons)
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
(async() => {
await utils.sleep(1000);
if (this.$refs.header)
this.$refs.header.style.overflowX = 'auto';
})();
}
checkSetStorageAccessKey() { checkSetStorageAccessKey() {
const q = this.$route.query; const q = this.$route.query;
@@ -675,6 +702,10 @@ class Reader {
this.$root.eventHook('resize'); this.$root.eventHook('resize');
} }
hideToolBar(event) {
this.debouncedHideToolBar(event);
}
fullScreenToggle() { fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive; this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) { if (this.fullScreenActive) {
@@ -910,7 +941,7 @@ class Reader {
refreshBook() { refreshBook() {
const mrb = this.mostRecentBook(); const mrb = this.mostRecentBook();
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true}); this.loadBook(Object.assign({}, mrb, {force: true}));
} }
undoAction() { undoAction() {
@@ -995,7 +1026,6 @@ class Reader {
classResult = classDisabled; classResult = classDisabled;
break; break;
case 'refresh': case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive) if (!this.mostRecentBookReactive)
classResult = classDisabled; classResult = classDisabled;
break; break;
@@ -1064,7 +1094,7 @@ class Reader {
return result; return result;
} }
async loadBook(opts) { async _loadBook(opts) {
if (!opts || !opts.url) { if (!opts || !opts.url) {
this.mostRecentBook(); this.mostRecentBook();
return; return;
@@ -1074,10 +1104,6 @@ class Reader {
let url = encodeURI(decodeURI(opts.url)); let url = encodeURI(decodeURI(opts.url));
//TODO: убрать конвертирование 'file://' после 06.2021
if (url.length == 71 && url.indexOf('file://') == 0)
url = url.replace(/^file/, 'disk');
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) && if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('disk://') != 0)) (url.indexOf('disk://') != 0))
url = 'http://' + url; url = 'http://' + url;
@@ -1104,33 +1130,37 @@ class Reader {
progress.show(); progress.show();
progress.setState({state: 'parse'}); progress.setState({state: 'parse'});
// есть ли среди недавних // есть ли среди загруженных
const key = bookManager.keyFromUrl(url); let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
let wasOpened = await bookManager.getRecentBook({key}); wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos); wasOpened = Object.assign(wasOpened, {
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen); url: (opts.url !== undefined ? opts.url : wasOpened.url),
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : ''); path: (opts.path !== undefined ? opts.path : wasOpened.path),
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
});
let book = null; let book = null;
if (!opts.force) { if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша // пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => { const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
progress.setState({progress: prog}); progress.setState({progress: prog});
}); });
// если есть в локальном кэше // если есть в локальном кэше
if (bookParsed) { if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed)); await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
this.mostRecentBook(); this.mostRecentBook();
this.addAction(bookPos); this.addAction(wasOpened.bookPos);
this.loaderActive = false; this.loaderActive = false;
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage(); this.blinkCachedLoadMessage();
this.checkBookPosPercent(); this.checkBookPosPercent();
await this.activateClickMapPage(); this.activateClickMapPage();//no await
return; return;
} }
@@ -1144,7 +1174,7 @@ class Reader {
}); });
book = Object.assign({}, wasOpened, {data: resp.data}); book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) { } catch (e) {
//молчим this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
} }
} }
} }
@@ -1155,7 +1185,7 @@ class Reader {
if (!book) { if (!book) {
book = await readerApi.loadBook({ book = await readerApi.loadBook({
url, url,
uploadFileName, uploadFileName: wasOpened.uploadFileName,
enableSitesFilter: this.enableSitesFilter, enableSitesFilter: this.enableSitesFilter,
skipHtmlCheck: (this.splitToPara ? true : false), skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false), isText: (this.splitToPara ? true : false),
@@ -1172,14 +1202,44 @@ class Reader {
// добавляем в bookManager // добавляем в bookManager
progress.setState({state: 'parse', step: 5}); progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (prog) => { const addedBook = await bookManager.addBook(book, (prog) => {
progress.setState({progress: prog}); progress.setState({progress: prog});
}); });
// sameBookKey
if (url.indexOf('disk://') == 0) {
//ищем такой файл в загруженных
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
found = (found ? _.cloneDeep(found) : found);
if (found) {
if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
await this.$root.stdDialog.askYesNo(`
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
<br>Объединить позицию?`, 'Найдена похожая книга');
if (askResult) {
wasOpened.bookPos = found.bookPos;
wasOpened.bookPosSeen = found.bookPosSeen;
wasOpened.sameBookKey = found.sameBookKey;
}
}
} else {
wasOpened.sameBookKey = wasOpened.uploadFileName;
}
} else {
wasOpened.sameBookKey = addedBook.url;
}
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
delete wasOpened.loadTime;
// добавляем в историю // добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook)); await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
this.mostRecentBook(); this.mostRecentBook();
this.addAction(bookPos); this.addAction(wasOpened.bookPos);
this.updateRoute(true); this.updateRoute(true);
this.loaderActive = false; this.loaderActive = false;
@@ -1190,11 +1250,11 @@ class Reader {
this.stopBlink = true; this.stopBlink = true;
this.checkBookPosPercent(); this.checkBookPosPercent();
await this.activateClickMapPage(); this.activateClickMapPage();//no await
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
if (!this.showHelpOnErrorIfNeeded(e.message)) { if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'}); this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
} }
} finally { } finally {
@@ -1202,7 +1262,16 @@ class Reader {
} }
} }
async loadFile(opts) { async loadBook(opts) {
await this.lock.get();
try {
await this._loadBook(opts);
} finally {
this.lock.ret();
}
}
async _loadFile(opts) {
this.progressActive = true; this.progressActive = true;
await this.$nextTick(); await this.$nextTick();
@@ -1218,7 +1287,7 @@ class Reader {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
await this.loadBook({url, uploadFileName: opts.file.name, force: true}); await this._loadBook({url, uploadFileName: opts.file.name, force: true});
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
@@ -1226,6 +1295,15 @@ class Reader {
} }
} }
async loadFile(opts) {
await this.lock.get();
try {
await this._loadFile(opts);
} finally {
this.lock.ret();
}
}
blinkCachedLoadMessage() { blinkCachedLoadMessage() {
if (!this.blinkCachedLoad) if (!this.blinkCachedLoad)
return; return;
@@ -1435,12 +1513,33 @@ export default vueComponent(Reader);
<style scoped> <style scoped>
.header { .header {
height: 50px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
background-color: #1B695F; background-color: #1B695F;
color: #000; color: #000;
overflow: hidden; overflow-x: auto;
height: 50px; overflow-y: hidden;
scrollbar-color: #c49a60 #e4e4e4;
}
.header::-webkit-scrollbar {
height: 10px;
}
.header::-webkit-scrollbar-track {
background-color: #e4e4e4;
border-radius: 4px;
}
.header::-webkit-scrollbar-thumb {
background-color: #c49a60;
border-radius: 4px;
border: 2px solid #e4e4e4;
}
.header::-webkit-scrollbar-thumb:hover {
background-color: #b48a50;
} }
.main { .main {

View File

@@ -54,7 +54,7 @@
<br><br> <br><br>
<div class="row justify-center"> <div class="row justify-center">
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate"> <!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
Помочь проекту Помочь проекту
</q-btn--> </q-btn-->
</div> </div>
@@ -162,6 +162,7 @@ class ReaderDialogs {
} }
loadBufferClick() { loadBufferClick() {
this.$emit('load-buffer-toggle');
this.urlHelpVisible = false; this.urlHelpVisible = false;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,10 @@
<span v-show="initStep">{{ initPercentage }}%</span> <span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input"> <div v-show="!initStep" class="input">
<!--input ref="input" <q-input
placeholder="что ищем" ref="input" v-model="needle"
:value="needle" @input="needle = $event.target.value"/-->
<q-input ref="input" v-model="needle"
class="col" outlined dense class="col" outlined dense
placeholder="что ищем" placeholder="Найти"
@keydown="inputKeyDown" @keydown="inputKeyDown"
/> />
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;"> <div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
@@ -108,7 +106,7 @@ class SearchPage {
this.parsed = parsed; this.parsed = parsed;
} }
this.header = 'Найти'; this.header = 'Поиск в тексте';
await this.$nextTick(); await this.$nextTick();
this.$refs.input.focus(); this.$refs.input.focus();
this.$refs.input.select(); this.$refs.input.select();

View File

@@ -728,10 +728,10 @@ class ServerStorage {
const ids = id.split('.'); const ids = id.split('.');
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey)) if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
throw new Error(`decodeStorageItems: bad id - ${id}`); throw new Error(`decodeStorageItems: bad id - ${id}`);
items[utils.fromBase58(ids[1])] = decoded; items[utils.fromBase58(ids[1]).toString()] = decoded;
} }
} }
result.items = items; result.items = items;
return result; return result;
} }

View File

@@ -1,19 +1,21 @@
<template> <template>
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close"> <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template #header> <template #header>
Установить позицию Установить позицию
</template> </template>
<div id="set-position-slider" class="slider q-px-md"> <div class="col column justify-center">
<q-slider <div id="set-position-slider" class="slider q-px-md column justify-center">
v-model="sliderValue" <q-slider
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0" v-model="sliderValue"
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
:max="sliderMax"
label :max="sliderMax"
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)" label
color="primary" :label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
/> color="primary"
/>
</div>
</div> </div>
</Window> </Window>
</template> </template>
@@ -76,7 +78,8 @@ export default vueComponent(SetPositionPage);
<style scoped> <style scoped>
.slider { .slider {
margin: 20px; margin: 0 20px 0 20px;
height: 35px;
background-color: #efefef; background-color: #efefef;
border-radius: 15px; border-radius: 15px;
} }

View File

@@ -1,9 +0,0 @@
<div class="part-header">Показывать кнопки панели</div>
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div>
<div class="col row">
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>
</div>

View File

@@ -1,10 +1,12 @@
<template> <template>
<Window ref="window" height="95%" width="600px" @close="close"> <Window ref="window" width="600px" @close="close">
<template #header> <template #header>
Настройки Настройки
</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"
@@ -24,7 +26,7 @@
<div v-show="tabsScrollable" class="q-pt-lg" /> <div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" /> <q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" /> <q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" /> <q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" /> <q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" /> <q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." /> <q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
@@ -82,8 +84,8 @@
</div> </div>
</div> </div>
<!-- Кнопки ----------------------------------------------------------------------> <!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel"> <div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ButtonsTab.inc'); @@include('./ToolBarTab.inc');
</div> </div>
<!-- Управление ------------------------------------------------------------------> <!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column"> <div v-if="selectedTab == 'keys'" class="fit column">
@@ -124,6 +126,7 @@ import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue'; import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage'; 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'; import defPalette from './defPalette';
@@ -636,8 +639,17 @@ class SettingsPage {
if (index < 0) if (index < 0)
newUserWallpapers.push({label, cssClass}); newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass)) if (!wallpaperStorage.keyExists(cssClass)) {
await wallpaperStorage.setData(cssClass, data); 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.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass; this.wallpaper = cssClass;
@@ -664,6 +676,27 @@ class SettingsPage {
} }
} }
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'});
}
}
keyHook(event) { keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') { if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close(); this.close();
@@ -702,11 +735,11 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px; margin-bottom: 5px;
} }
.label-1, .label-7 { .label-1, .label-3, .label-7 {
width: 75px; width: 75px;
} }
.label-2, .label-3, .label-4, .label-5 { .label-2, .label-4, .label-5 {
width: 110px; width: 110px;
} }

View File

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

View File

@@ -13,7 +13,7 @@
ref="input" ref="input"
v-model="search" v-model="search"
class="q-ml-sm col" class="q-ml-sm col"
outlined dense rounded outlined dense
bg-color="grey-4" bg-color="grey-4"
placeholder="Найти" placeholder="Найти"
@click.stop @click.stop

View File

@@ -102,6 +102,11 @@
Удалить выбранные обои Удалить выбранные обои
</q-tooltip> </q-tooltip>
</q-btn> </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> </div>

View File

@@ -6,29 +6,32 @@
</div> </div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd"> <div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div v-html="page1"></div> <div @copy.prevent="copyText" v-html="page1"></div>
</div> </div>
</div> </div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd"> <div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div v-html="page2"></div> <div @copy.prevent="copyText" v-html="page2"></div>
</div> </div>
</div> </div>
<div v-show="showStatusBar" ref="statusBar" class="layout"> <div v-show="showStatusBar" ref="statusBar" class="layout">
<div v-html="statusBar"></div> <div v-html="statusBar"></div>
</div> </div>
<div v-show="clickControl" ref="layoutEvents" class="layout events" <div
v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;" oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@wheel.prevent.stop="onMouseWheel" @wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel" @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
> >
<div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop <div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick" @click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable" v-html="statusBarClickable"
></div> ></div>
</div> </div>
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" <div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick" @click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable" v-html="statusBarClickable"
@@ -46,6 +49,7 @@ import vueComponent from '../../vueComponent.js';
import {loadCSS} from 'fg-loadcss'; import {loadCSS} from 'fg-loadcss';
import _ from 'lodash'; import _ from 'lodash';
import he from 'he';
import './TextPage.css'; import './TextPage.css';
@@ -62,7 +66,14 @@ const componentOptions = {
watch: { watch: {
bookPos: function() { bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
this.draw(); this.draw();
if (this.userBookPosChange) {
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
this.prevBookPos = this.bookPos;
this.userBookPosChange = false;
}
}, },
bookPosSeen: function() { bookPosSeen: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -95,6 +106,8 @@ class TextPage {
lastBook = null; lastBook = null;
bookPos = 0; bookPos = 0;
bookPosSeen = null; bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null; fontStyle = null;
fontSize = null; fontSize = null;
@@ -151,7 +164,7 @@ class TextPage {
this.$root.addEventHook('resize', async() => { this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
await utils.sleep(500); await utils.sleep(200);
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
}); });
} }
@@ -495,12 +508,25 @@ class TextPage {
} }
async onResize() { async onResize() {
if (this.resizing)
return;
this.resizing = true;
try { try {
const scrolled = this.doingScrolling;
if (scrolled)
await this.stopTextScrolling();
this.calcDrawProps(); this.calcDrawProps();
this.setBackground(); this.setBackground();
this.draw(); this.draw();
if (scrolled)
this.startTextScrolling();
} catch (e) { } catch (e) {
// //
} finally {
this.resizing = false;
} }
} }
@@ -648,7 +674,7 @@ class TextPage {
} }
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) { if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
this.doEnd(true); this.doEnd(true, false);
return; return;
} }
@@ -671,7 +697,7 @@ class TextPage {
this.debouncedDrawPageDividerAndOrnament(); this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) { if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true); this.doEnd(true, false);
return; return;
} }
} }
@@ -907,12 +933,14 @@ class TextPage {
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.bookPos = this.linesDown[1].begin; this.bookPos = this.linesDown[1].begin;
} }
} }
doUp() { doUp() {
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) { if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesUp[1].begin; this.bookPos = this.linesUp[1].begin;
} }
} }
@@ -925,6 +953,7 @@ class TextPage {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) { if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true; this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = this.linesDown[i].begin; this.bookPos = this.linesDown[i].begin;
} else } else
this.doEnd(); this.doEnd();
@@ -940,6 +969,7 @@ class TextPage {
if (i >= 0 && this.linesUp.length > i) { if (i >= 0 && this.linesUp.length > i) {
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false; this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = this.linesUp[i].begin; this.bookPos = this.linesUp[i].begin;
} }
} }
@@ -948,10 +978,11 @@ class TextPage {
doHome() { doHome() {
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false; this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = 0; this.bookPos = 0;
} }
doEnd(noAni) { doEnd(noAni, isUser = true) {
if (this.parsed.para.length && this.pageLineCount > 0) { if (this.parsed.para.length && this.pageLineCount > 0) {
let i = this.parsed.para.length - 1; let i = this.parsed.para.length - 1;
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1; let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
@@ -962,6 +993,7 @@ class TextPage {
if (!noAni) if (!noAni)
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true; this.pageChangeDirectionDown = true;
this.userBookPosChange = isUser;
this.bookPos = lines[i].begin; this.bookPos = lines[i].begin;
} }
} }
@@ -1201,8 +1233,54 @@ class TextPage {
} }
return action; return action;
} }
copyText(event) {
//все это для того, чтобы правильно расставить переносы \n при копировании текста
//прямо с текущей страницы
//подготовка, вытаскиваем весь текст страницы
const lines = this.getLines(this.bookPos);
const decodedLines = [];
for (const line of lines.linesDown) {
let lineText = '';
for (const part of line.parts) {
lineText += part.text;
}
decodedLines.push({text: he.decode(lineText), first: line.first});
}
let i = 0;
const findDecoded = (line) => {
for (let j = i; j < decodedLines.length; j++) {
const decoded = decodedLines[j];
if (decoded.text.indexOf(line) >= 0) {
i = j;
return decoded;
}
}
return;
}
const selection = document.getSelection();
const splitted = selection.toString().split(/[\n\r]/);
let filtered = '';
//формируем filtered, учитывая переносы из decodedLines
for (const line of splitted) {
const found = findDecoded(line);
if (found && found.first) {
filtered += (filtered ? '\n' : '') + line;
} else {
filtered += (filtered ? '\r ' : '') + line;
}
}
//маленькие хитрости, убираем переносы по слогам
filtered = filtered.replace(/-\r /g, '').replace(/\r /g, ' ');
event.clipboardData.setData('text/plain', filtered);
}
} }
export default vueComponent(TextPage); export default vueComponent(TextPage);

View File

@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
const maxImageLineCount = 100; const maxImageLineCount = 100;
const maxParaLength = 10000;
const maxParaTextLength = 10000;
// defaults // defaults
const defaultSettings = { const defaultSettings = {
@@ -83,6 +85,7 @@ export default class BookParser {
let binaryId = ''; let binaryId = '';
let binaryType = ''; let binaryType = '';
let dimPromises = []; let dimPromises = [];
this.coverPageId = '';
//оглавление //оглавление
this.contents = []; this.contents = [];
@@ -226,13 +229,26 @@ export default class BookParser {
paraOffset += len; paraOffset += len;
}; };
const growParagraph = (text, len) => { const growParagraph = (text, len, textRaw) => {
//начальный параграф
if (paraIndex < 0) { if (paraIndex < 0) {
newParagraph(); newParagraph();
growParagraph(text, len); growParagraph(text, len);
return; return;
} }
//ограничение на размер куска текста в параграфе
if (textRaw && textRaw.length > maxParaTextLength) {
while (textRaw.length > 0) {
const textPart = textRaw.substring(0, maxParaTextLength);
textRaw = textRaw.substring(maxParaTextLength);
newParagraph();
growParagraph(textPart, textPart.length);
}
return;
}
if (inSubtitle) { if (inSubtitle) {
curSubtitle.title += text; curSubtitle.title += text;
} else if (inTitle) { } else if (inTitle) {
@@ -240,6 +256,14 @@ export default class BookParser {
} }
const p = para[paraIndex]; const p = para[paraIndex];
//ограничение на размер параграфа
if (p.length > maxParaLength) {
newParagraph();
growParagraph(text, len);
return;
}
p.length += len; p.length += len;
p.text += text; p.text += text;
paraOffset += len; paraOffset += len;
@@ -266,7 +290,7 @@ export default class BookParser {
const href = attrs.href.value; const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : ''); const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href); const {id, local} = this.imageHrefToId(href);
if (href[0] == '#') {//local if (local) {//local
imageNum++; imageNum++;
if (inPara && !this.sets.showInlineImagesInCenter && !center) if (inPara && !this.sets.showInlineImagesInCenter && !center)
@@ -278,6 +302,11 @@ export default class BookParser {
if (inPara && this.sets.showInlineImagesInCenter) if (inPara && this.sets.showInlineImagesInCenter)
newParagraph(); newParagraph();
//coverpage
if (path == '/fictionbook/description/title-info/coverpage/image') {
this.coverPageId = id;
}
} else {//external } else {//external
imageNum++; imageNum++;
@@ -536,7 +565,7 @@ export default class BookParser {
tClose += (center ? '</center>' : ''); tClose += (center ? '</center>' : '');
if (text != ' ') if (text != ' ')
growParagraph(`${tOpen}${text}${tClose}`, text.length); growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else else
growParagraph(' ', 1); growParagraph(' ', 1);
} }

View File

@@ -1,10 +1,14 @@
import localForage from 'localforage'; import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser'; import BookParser from './BookParser';
import readerApi from '../../../api/reader';
import coversStorage from './coversStorage';
import * as utils from '../../../share/utils';
const maxDataSize = 500*1024*1024;//compressed bytes const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
//локальный кэш метаданных книг, ограничение maxDataSize //локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({ const bmMetaStore = localForage.createInstance({
@@ -17,9 +21,6 @@ const bmDataStore = localForage.createInstance({
}); });
//список недавно открытых книг //список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({ const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew' name: 'bmRecentStoreNew'
}); });
@@ -39,7 +40,7 @@ class BookManager {
this.saveRecentItem = _.debounce(() => { this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem); bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1); this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev); bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300}); }, 200, {maxWait: 300});
@@ -54,6 +55,9 @@ class BookManager {
if (this.recentItem) if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem; this.recent[this.recentItem.key] = this.recentItem;
//конвертируем в новые ключи
await this.convertRecent();
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key'); this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) { if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`); const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
@@ -70,6 +74,40 @@ class BookManager {
this.loadStored();//no await this.loadStored();//no await
} }
//TODO: убрать в 2025г
async convertRecent() {
const converted = await bmRecentStoreNew.getItem('recent-converted');
if (converted)
return;
const newRecent = {};
for (const book of Object.values(this.recent)) {
if (!book.path) {
continue;
}
const newKey = this.keyFromPath(book.path);
newRecent[newKey] = _.cloneDeep(book);
newRecent[newKey].key = newKey;
if (!newRecent[newKey].loadTime)
newRecent[newKey].loadTime = newRecent[newKey].addTime;
}
this.recent = newRecent;
//console.log(converted);
(async() => {
await utils.sleep(3000);
this.saveRecent();
this.emit('recent-changed');
this.emit('set-recent');
await bmRecentStoreNew.setItem('recent-converted', true);
})();
}
//Ленивая асинхронная загрузка bmMetaStore //Ленивая асинхронная загрузка bmMetaStore
async loadStored() { async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
@@ -196,8 +234,8 @@ 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};
meta.key = this.keyFromUrl(meta.url); meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now(); meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => { const cb = (perc) => {
const p = Math.round(30*perc/100); const p = Math.round(30*perc/100);
@@ -232,10 +270,10 @@ class BookManager {
async hasBookParsed(meta) { async hasBookParsed(meta) {
if (!this.books) if (!this.books)
return false; return false;
if (!meta.url) if (!meta.path)
return false; return false;
if (!meta.key) if (!meta.key)
meta.key = this.keyFromUrl(meta.url); meta.key = this.keyFromPath(meta.path);
let book = this.books[meta.key]; let book = this.books[meta.key];
@@ -250,8 +288,12 @@ class BookManager {
async getBook(meta, callback) { async getBook(meta, callback) {
let result = undefined; let result = undefined;
if (!meta.path)
return;
if (!meta.key) if (!meta.key)
meta.key = this.keyFromUrl(meta.url); meta.key = this.keyFromPath(meta.path);
result = this.books[meta.key]; result = this.books[meta.key];
@@ -261,11 +303,6 @@ class BookManager {
this.books[meta.key] = result; this.books[meta.key] = result;
} }
//Если файл на сервере изменился, считаем, что в кеше его нету
if (meta.path && result && meta.path != result.path) {
return;
}
if (result && !result.parsed) { if (result && !result.parsed) {
let data = await bmDataStore.getItem(`bmData-${meta.key}`); let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5); callback(5);
@@ -310,9 +347,38 @@ class BookManager {
const parsed = new BookParser(this.settings); const parsed = new BookParser(this.settings);
const parsedMeta = await parsed.parse(data, callback); const parsedMeta = await parsed.parse(data, callback);
//cover page
let coverPageUrl = '';
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
const bin = parsed.binary[parsed.coverPageId];
let dataUrl = `data:${bin.type};base64,${bin.data}`;
try {
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
} catch (e) {
console.error(e);
}
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
//далее асинхронно
(async() => {
//отправим dataUrl на сервер в /upload
try {
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
} catch (e) {
console.error(e);
}
//сохраним в storage
await coversStorage.setData(coverPageUrl, dataUrl);
})();
}
const result = Object.assign({}, meta, parsedMeta, { const result = Object.assign({}, meta, parsedMeta, {
length: data.length, length: data.length,
textLength: parsed.textLength, textLength: parsed.textLength,
coverPageUrl,
parsed parsed
}); });
@@ -325,10 +391,20 @@ class BookManager {
return result; return result;
} }
keyFromUrl(url) { /*keyFromUrl(url) {
return utils.stringToHex(url); return utils.stringToHex(url);
}*/
keyFromPath(bookPath) {
return path.basename(bookPath);
} }
keysEqual(bookPath1, bookPath2) {
if (bookPath1 === undefined || bookPath2 === undefined)
return false;
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
}
//-- recent -------------------------------------------------------------- //-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) { async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev'); const rev = await bmRecentStoreNew.getItem('rev');
@@ -369,7 +445,10 @@ class BookManager {
async setRecentBook(value) { async setRecentBook(value) {
let result = this.metaOnly(value); let result = this.metaOnly(value);
result.touchTime = Date.now(); result.touchTime = Date.now();//время последнего чтения
if (!result.loadTime)
result.loadTime = Date.now();//время загрузки файла
result.deleted = 0; result.deleted = 0;
if (this.recent[result.key]) { if (this.recent[result.key]) {
@@ -385,9 +464,9 @@ class BookManager {
return this.recent[value.key]; return this.recent[value.key];
} }
async delRecentBook(value) { async delRecentBook(value, delFlag = 1) {
const item = this.recent[value.key]; const item = this.recent[value.key];
item.deleted = 1; item.deleted = delFlag;
if (this.recentLastKey == value.key) { if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null); await this.recentSetLastKey(null);
@@ -397,11 +476,18 @@ class BookManager {
this.emit('recent-deleted', value.key); this.emit('recent-deleted', value.key);
} }
async restoreRecentBook(value) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
async cleanRecentBooks() { async cleanRecentBooks() {
const sorted = this.getSortedRecent(); const sorted = this.getSortedRecent();
let isDel = false; let isDel = false;
for (let i = 1000; i < sorted.length; i++) { for (let i = maxRecentLength; i < sorted.length; i++) {
delete this.recent[sorted[i].key]; delete this.recent[sorted[i].key];
isDel = true; isDel = true;
} }
@@ -421,7 +507,7 @@ class BookManager {
let max = 0; let max = 0;
let result = null; let result = null;
for (let key in this.recent) { for (const key in this.recent) {
const book = this.recent[key]; const book = this.recent[key];
if (!book.deleted && book.touchTime > max) { if (!book.deleted && book.touchTime > max) {
max = book.touchTime; max = book.touchTime;
@@ -452,6 +538,43 @@ class BookManager {
return result; return result;
} }
findRecentByUrlAndPath(url, bookPath) {
if (bookPath) {
const key = this.keyFromPath(bookPath);
const book = this.recent[key];
if (book && !book.deleted)
return book;
}
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.url == url && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
findRecentBySameBookKey(sameKey) {
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
async setRecent(value) { async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent); const mergedRecent = _.cloneDeep(this.recent);

View File

@@ -0,0 +1,61 @@
import localForage from 'localforage';
//import _ from 'lodash';
import * as utils from '../../../share/utils';
const maxDataSize = 100*1024*1024;
const coversStore = localForage.createInstance({
name: 'coversStorage'
});
class CoversStorage {
constructor() {
}
async init() {
this.cleanCovers(); //no await
}
async setData(key, data) {
await coversStore.setItem(key, {addTime: Date.now(), data});
}
async getData(key) {
const item = await coversStore.getItem(key);
return (item ? item.data : undefined);
}
async removeData(key) {
await coversStore.removeItem(key);
}
async cleanCovers() {
await utils.sleep(10000);
while (1) {// eslint-disable-line no-constant-condition
let size = 0;
let min = Date.now();
let toDel = null;
for (const key of (await coversStore.keys())) {
const item = await coversStore.getItem(key);
size += item.data.length;
if (item.addTime < min) {
toDel = key;
min = item.addTime;
}
}
if (size > maxDataSize && toDel) {
await this.removeData(toDel);
} else {
break;
}
}
}
}
export default new CoversStorage();

View File

@@ -32,6 +32,10 @@ class WallpaperStorage {
this.cachedKeys = await wpStore.keys(); this.cachedKeys = await wpStore.keys();
} }
async getKeys() {
return await wpStore.keys();
}
keyExists(key) {//не асинхронная keyExists(key) {//не асинхронная
return this.cachedKeys.includes(key); return this.cachedKeys.includes(key);
} }

View File

@@ -1,8 +1,58 @@
export const versionHistory = [ export const versionHistory = [
{ {
version: '0.11.4', version: '0.11.8',
releaseDate: '2022-04-14', releaseDate: '2022-07-14',
showUntil: '2022-04-13', showUntil: '2022-07-13',
content:
`
<ul>
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
<li>добавлена синхронизация обоев</li>
</ul>
`
},
{
version: '0.11.7',
releaseDate: '2022-07-12',
showUntil: '2022-07-19',
content:
`
<ul>
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
<li>изменения в окне загруженных книг:</li>
<ul>
<li>добавлена группировка по версиям файла одной и той же книги</li>
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
<li>добавлены различные методы сортировки списка загруженных книг</li>
<li>нумерация всегда осуществляется по времени загрузки</li>
</ul>
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.6',
releaseDate: '2022-07-02',
showUntil: '2022-07-01',
content:
`
<ul>
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
<li>актуализация используемых пакетов</li>
</ul>
`
},
{
version: '0.11.5',
releaseDate: '2022-04-15',
showUntil: '2022-04-14',
content: content:
` `
<ul> <ul>
@@ -13,32 +63,6 @@ export const versionHistory = [
` `
}, },
{
version: '0.11.3',
releaseDate: '2022-03-29',
showUntil: '2022-03-28',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.2',
releaseDate: '2022-01-11',
showUntil: '2022-01-10',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{ {
version: '0.11.1', version: '0.11.1',
releaseDate: '2021-12-03', releaseDate: '2021-12-03',

View File

@@ -55,6 +55,34 @@
</div> </div>
</div> </div>
<!--------------------------------------------------->
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Нет
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
Да
</q-btn>
</div>
</div>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'prompt'" class="bg-white no-wrap"> <div v-show="type == 'prompt'" class="bg-white no-wrap">
<div class="header row"> <div class="header row">
@@ -262,6 +290,23 @@ class StdDialog {
}); });
} }
askYesNo(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'askYesNo';
this.active = true;
});
}
prompt(message, caption, opts) { prompt(message, caption, opts) {
return new Promise((resolve) => { return new Promise((resolve) => {
this.enableValidator = false; this.enableValidator = false;

View File

@@ -153,7 +153,7 @@ export default vueComponent(Window);
} }
.header { .header {
background: linear-gradient(to bottom right, green, #59B04F); background: linear-gradient(to bottom right, #007000, #59B04F);
align-items: center; align-items: center;
height: 30px; height: 30px;
} }
@@ -161,8 +161,8 @@ export default vueComponent(Window);
.header-text { .header-text {
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
color: yellow; color: #FFFFA0;
text-shadow: 2px 1px 5px black, 2px 2px 5px black; text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
@@ -174,7 +174,8 @@ export default vueComponent(Window);
} }
.close-button:hover { .close-button:hover {
background-color: #69C05F; color: white;
background-color: #FF3030;
} }
</style> </style>

View File

@@ -32,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog'; import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip'; import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree'; import {QTree} from 'quasar/src/components/tree';
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
//import {QExpansionItem} from 'quasar/src/components/expansion-item'; //import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = { const components = {
@@ -62,6 +64,7 @@ const components = {
QChip, QChip,
QTree, QTree,
//QExpansionItem, //QExpansionItem,
QVirtualScroll,
}; };
//directives //directives

53
client/share/LockQueue.js Normal file
View File

@@ -0,0 +1,53 @@
class LockQueue {
constructor(queueSize) {
this.queueSize = queueSize;
this.freed = true;
this.waitingQueue = [];
}
//async
get(take = true) {
return new Promise((resolve, reject) => {
if (this.freed) {
if (take)
this.freed = false;
resolve();
return;
}
if (this.waitingQueue.length < this.queueSize) {
this.waitingQueue.push({resolve, reject});
} else {
reject(new Error('Lock queue is too long'));
}
});
}
ret() {
if (this.waitingQueue.length) {
this.waitingQueue.shift().resolve();
} else {
this.freed = true;
}
}
//async
wait() {
return this.get(false);
}
retAll() {
while (this.waitingQueue.length) {
this.waitingQueue.shift().resolve();
}
}
errAll(error = 'rejected') {
while (this.waitingQueue.length) {
this.waitingQueue.shift().reject(new Error(error));
}
}
}
export default LockQueue;

View File

@@ -90,7 +90,7 @@ export function toBase58(data) {
} }
export function fromBase58(data) { export function fromBase58(data) {
return bs58.decode(data); return Buffer.from(bs58.decode(data));
} }
//base-x слишком тормозит, используем sjcl //base-x слишком тормозит, используем sjcl
@@ -107,6 +107,10 @@ export function fromBase64(data) {
)); ));
} }
export function hasProp(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
export function getObjDiff(oldObj, newObj, opts = {}) { export function getObjDiff(oldObj, newObj, opts = {}) {
const { const {
exclude = [], exclude = [],
@@ -126,7 +130,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
for (const key of Object.keys(oldObj)) { for (const key of Object.keys(oldObj)) {
const kp = `${keyPath}${key}`; const kp = `${keyPath}${key}`;
if (newObj.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(newObj, key)) {
if (ex.has(kp)) if (ex.has(kp))
continue; continue;
@@ -149,7 +153,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
if (exAdd.has(kp)) if (exAdd.has(kp))
continue; continue;
if (!oldObj.hasOwnProperty(key)) { if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
result.add[key] = _.cloneDeep(newObj[key]); result.add[key] = _.cloneDeep(newObj[key]);
} }
} }
@@ -213,7 +217,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
const change = diff.change; const change = diff.change;
for (const key of Object.keys(change)) { for (const key of Object.keys(change)) {
if (result.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(result, key)) {
if (_.isObject(change[key])) { if (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], opts); result[key] = applyObjDiff(result[key], change[key], opts);
} else { } else {
@@ -359,4 +363,50 @@ export function getBookTitle(fb2) {
]).join(' - '); ]).join(' - ');
return result; return result;
}
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
return new Promise ((resolve, reject) => { (async() => {
const img = new Image();
let resolved = false;
img.onload = () => {
try {
let width = img.width;
let height = img.height;
if (width > height) {
if (width > toWidth) {
height = height * (toWidth / width);
width = toWidth;
}
} else {
if (height > toHeight) {
width = width * (toHeight / height);
height = toHeight;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
const result = canvas.toDataURL('image/jpeg', quality);
resolved = true;
resolve(result);
} catch (e) {
reject(e);
return;
}
};
img.onerror = reject;
img.src = dataUrl;
await sleep(1000);
if (!resolved)
reject('Не удалось изменить размер');
})().catch(reject); });
} }

View File

@@ -21,7 +21,7 @@ const readerActions = {
'offlineMode': 'Автономный режим (без интернета)', 'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки', 'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека', 'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние', 'recentBooks': 'Показать загруженные',
'switchToolbar': 'Показать/скрыть панель управления', 'switchToolbar': 'Показать/скрыть панель управления',
'donate': '', 'donate': '',
'bookBegin': 'В начало книги', 'bookBegin': 'В начало книги',
@@ -70,7 +70,7 @@ const hotKeys = [
{name: 'scrolling', codes: ['Z']}, {name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']}, {name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']}, {name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']}, {name: 'copyText', codes: ['Ctrl+Space']},
{name: 'convOptions', codes: ['Ctrl+M']}, {name: 'convOptions', codes: ['Ctrl+M']},
{name: 'refresh', codes: ['R']}, {name: 'refresh', codes: ['R']},
{name: 'contents', codes: ['C']}, {name: 'contents', codes: ['C']},
@@ -185,8 +185,14 @@ const settingDefaults = {
fontShifts: {}, fontShifts: {},
showToolButton: {}, showToolButton: {},
toolBarHideOnScroll: true,
userHotKeys: {}, userHotKeys: {},
userWallpapers: [], userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
needUpdateSettingsView: 0,
}; };
for (const font of fonts) for (const font of fonts)
@@ -222,9 +228,6 @@ const libsDefaults = {
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [ {r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'}, {l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]}, ]},
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
{l: 'https://flibs.in', c: 'Flibs'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [ {r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'}, {l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]}, ]},

View File

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.liberama.top; server_name beta.liberama.top;
set $liberama http://127.0.0.1:34082;
client_max_body_size 50m; client_max_body_size 50m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
@@ -15,15 +16,20 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:34082; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:34082; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -32,6 +38,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
@@ -50,6 +61,7 @@ server {
server { server {
listen 80; listen 80;
server_name b.beta.liberama.top; server_name b.beta.liberama.top;
set $liberama http://127.0.0.1:34082;
client_max_body_size 50m; client_max_body_size 50m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
@@ -59,15 +71,20 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:34082; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:34082; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -76,6 +93,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.omnireader.ru; server_name beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m; client_max_body_size 50m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
@@ -15,15 +16,20 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:34081; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:34081; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -32,6 +38,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -1,6 +1,7 @@
server { server {
listen 80; listen 80;
server_name beta.omnireader.ru; server_name beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m; client_max_body_size 50m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
@@ -10,15 +11,20 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:34081; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:34081; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -27,6 +33,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -17,8 +17,9 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name liberama.top; server_name liberama.top;
set $liberama http://127.0.0.1:55081;
client_max_body_size 50m; client_max_body_size 100m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
gzip on; gzip on;
@@ -26,12 +27,16 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:55081; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:55081; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
@@ -44,6 +49,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
@@ -62,8 +72,9 @@ server {
server { server {
listen 80; listen 80;
server_name b.liberama.top; server_name b.liberama.top;
set $liberama http://127.0.0.1:55081;
client_max_body_size 50m; client_max_body_size 100m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
gzip on; gzip on;
@@ -71,15 +82,20 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:55081; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:55081; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -88,6 +104,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
@@ -140,5 +161,6 @@ server {
location / { location / {
proxy_pass http://fantasy-worlds.org; proxy_pass http://fantasy-worlds.org;
proxy_hide_header x-frame-options;
} }
} }

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
if ! pgrep -x "liberama" > /dev/null ; then if ! pgrep -x "liberama" > /dev/null ; then
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null"
else else
echo "Process 'liberama' already running" echo "Process 'liberama' already running"
fi fi

View File

@@ -6,8 +6,9 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name omnireader.ru; server_name omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 50m; client_max_body_size 100m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
gzip on; gzip on;
@@ -15,12 +16,16 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:44081; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:44081; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
@@ -33,6 +38,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
@@ -52,7 +62,7 @@ server {
listen 80; listen 80;
server_name old.omnireader.ru; server_name old.omnireader.ru;
client_max_body_size 50m; client_max_body_size 100m;
gzip on; gzip on;
gzip_min_length 1024; gzip_min_length 1024;

View File

@@ -1,6 +1,7 @@
server { server {
listen 80; listen 80;
server_name omnireader.ru; server_name omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 50m; client_max_body_size 50m;
proxy_read_timeout 1h; proxy_read_timeout 1h;
@@ -10,12 +11,16 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1:44081; proxy_pass $liberama;
} }
location /ws { location /ws {
proxy_pass http://127.0.0.1:44081; proxy_pass $liberama;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
@@ -27,6 +32,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null & disown"
sudo service cron start sudo service cron start

11271
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.11.4", "version": "0.11.8",
"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",
@@ -28,17 +28,17 @@
"@babel/preset-env": "^7.16.0", "@babel/preset-env": "^7.16.0",
"@vue/compiler-sfc": "^3.2.22", "@vue/compiler-sfc": "^3.2.22",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"copy-webpack-plugin": "^9.1.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.5.1", "css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^3.1.3", "css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.2.0", "eslint": "^8.19.0",
"eslint-plugin-vue": "^8.0.3", "eslint-plugin-vue": "^9.2.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.4.4",
"pkg": "^5.5.1", "pkg": "^5.5.1",
"terser-webpack-plugin": "^5.2.5", "terser-webpack-plugin": "^5.2.5",
"vue-eslint-parser": "^8.0.1", "vue-eslint-parser": "^9.0.3",
"vue-loader": "^16.8.3", "vue-loader": "^17.0.0",
"vue-style-loader": "^4.1.3", "vue-style-loader": "^4.1.3",
"webpack": "^5.64.1", "webpack": "^5.64.1",
"webpack-cli": "^4.9.1", "webpack-cli": "^4.9.1",
@@ -50,25 +50,24 @@
"dependencies": { "dependencies": {
"@quasar/extras": "^1.12.0", "@quasar/extras": "^1.12.0",
"@vue/compat": "^3.2.21", "@vue/compat": "^3.2.21",
"axios": "^0.24.0", "axios": "^0.27.2",
"base-x": "^3.0.9", "base-x": "^4.0.0",
"chardet": "^1.4.0", "chardet": "^1.4.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.17.1", "express": "^4.17.1",
"fg-loadcss": "^3.1.0", "fg-loadcss": "^3.1.0",
"fs-extra": "^9.0.1", "fs-extra": "^10.1.0",
"got": "^11.8.2",
"he": "^1.2.0", "he": "^1.2.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jembadb": "^2.3.0", "jembadb": "^3.0.8",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"multer": "^1.4.3", "multer": "^1.4.5-lts.1",
"pako": "^2.0.4", "pako": "^2.0.4",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"pidusage": "^3.0.0", "pidusage": "^3.0.0",
"quasar": "^2.3.2", "quasar": "^2.7.5",
"safe-buffer": "^5.2.1", "safe-buffer": "^5.2.1",
"sanitize-html": "^2.5.3", "sanitize-html": "^2.5.3",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
@@ -77,8 +76,8 @@
"sqlite3": "^5.0.2", "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.22", "vue": "^3.2.37",
"vue-router": "^4.0.12", "vue-router": "^4.1.1",
"vuex": "^4.0.2", "vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0", "vuex-persistedstate": "^4.1.0",
"webdav": "^4.7.0", "webdav": "^4.7.0",

View File

@@ -22,7 +22,8 @@ module.exports = {
maxUploadPublicDirSize: 200*1024*1024,//100Мб maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false, useExternalBookConverter: false,
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'], 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'],
db: [ db: [
{ {
@@ -48,14 +49,14 @@ module.exports = {
servers: [ servers: [
{ {
serverName: '1', serverName: '1',
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader' mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
ip: '0.0.0.0', ip: '0.0.0.0',
port: '33080', port: '33080',
}, },
], ],
remoteWebDavStorage: false,
/* /*
remoteWebDavStorage: false,
remoteWebDavStorage: { remoteWebDavStorage: {
url: '127.0.0.1:1900', url: '127.0.0.1:1900',
username: '', username: '',
@@ -63,5 +64,12 @@ module.exports = {
}, },
*/ */
remoteStorage: false,
/*
remoteStorage: {
url: 'https://127.0.0.1:11900',
accessToken: '',
},
*/
}; };

View File

@@ -0,0 +1,95 @@
const WebSocket = require ('ws');
//const _ = require('lodash');
const log = new (require('../core/AppLogger'))().log;//singleton
//const utils = require('../core/utils');
const cleanPeriod = 1*60*1000;//1 минута
const closeSocketOnIdle = 5*60*1000;//5 минут
class BookUpdateCheckerController {
constructor(wss, config) {
this.config = config;
this.isDevelopment = (config.branch == 'development');
//this.readerStorage = new JembaReaderStorage();
this.wss = wss;
wss.on('connection', (ws) => {
ws.on('message', (message) => {
this.onMessage(ws, message.toString());
});
ws.on('error', (err) => {
log(LM_ERR, err);
});
});
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
periodicClean() {
try {
const now = Date.now();
this.wss.clients.forEach((ws) => {
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
ws.terminate();
}
});
} finally {
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
async onMessage(ws, message) {
let req = {};
try {
if (this.isDevelopment) {
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
}
req = JSON.parse(message);
ws.lastActivity = Date.now();
//pong for WebSocketConnection
this.send({_rok: 1}, req, ws);
switch (req.action) {
case 'test':
await this.test(req, ws); break;
default:
throw new Error(`Action not found: ${req.action}`);
}
} catch (e) {
this.send({error: e.message}, req, ws);
}
}
send(res, req, ws) {
if (ws.readyState == WebSocket.OPEN) {
ws.lastActivity = Date.now();
let r = res;
if (req.requestId)
r = Object.assign({requestId: req.requestId}, r);
const message = JSON.stringify(r);
ws.send(message);
if (this.isDevelopment) {
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
}
}
}
//Actions ------------------------------------------------------------------
async test(req, ws) {
this.send({message: 'Liberama project is awesome'}, req, ws);
}
}
module.exports = BookUpdateCheckerController;

View File

@@ -68,24 +68,6 @@ class ReaderController extends BaseController {
res.status(400).send({error}); res.status(400).send({error});
return false; return false;
} }
async restoreCachedFile(req, res) {
const request = req.body;
let error = '';
try {
if (!request.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(request.path);
const state = this.workerState.getState(workerId);
return (state ? state : {});
} catch (e) {
error = e.message;
}
//bad request
res.status(400).send({error});
return false;
}
} }
module.exports = ReaderController; module.exports = ReaderController;

View File

@@ -25,6 +25,10 @@ class WebSocketController {
ws.on('message', (message) => { ws.on('message', (message) => {
this.onMessage(ws, message.toString()); this.onMessage(ws, message.toString());
}); });
ws.on('error', (err) => {
log(LM_ERR, err);
});
}); });
setTimeout(() => { this.periodicClean(); }, cleanPeriod); setTimeout(() => { this.periodicClean(); }, cleanPeriod);
@@ -66,10 +70,12 @@ class WebSocketController {
await this.workerGetState(req, ws); break; await this.workerGetState(req, ws); break;
case 'worker-get-state-finish': case 'worker-get-state-finish':
await this.workerGetStateFinish(req, ws); break; await this.workerGetStateFinish(req, ws); break;
case 'reader-restore-cached-file':
await this.readerRestoreCachedFile(req, ws); break;
case 'reader-storage': case 'reader-storage':
await this.readerStorageDo(req, ws); break; await this.readerStorageDo(req, ws); break;
case 'upload-file-buf':
await this.uploadFileBuf(req, ws); break;
case 'upload-file-touch':
await this.uploadFileTouch(req, ws); break;
default: default:
throw new Error(`Action not found: ${req.action}`); throw new Error(`Action not found: ${req.action}`);
@@ -149,15 +155,6 @@ class WebSocketController {
} }
} }
async readerRestoreCachedFile(req, ws) {
if (!req.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(req.path);
const state = this.workerState.getState(workerId);
this.send((state ? state : {}), req, ws);
}
async readerStorageDo(req, ws) { async readerStorageDo(req, ws) {
if (!req.body) if (!req.body)
throw new Error(`key 'body' is empty`); throw new Error(`key 'body' is empty`);
@@ -168,6 +165,20 @@ class WebSocketController {
this.send(await this.readerStorage.doAction(req.body), req, ws); this.send(await this.readerStorage.doAction(req.body), req, ws);
} }
async uploadFileBuf(req, ws) {
if (!req.buf)
throw new Error(`key 'buf' is empty`);
this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
}
async uploadFileTouch(req, ws) {
if (!req.url)
throw new Error(`key 'url' is empty`);
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
}
} }
module.exports = WebSocketController; module.exports = WebSocketController;

View File

@@ -25,60 +25,7 @@ class WorkerController extends BaseController {
res.status(400).send({error}); res.status(400).send({error});
return false; return false;
} }
//TODO: удалить бесполезную getStateFinish
async getStateFinish(req, res) {
const request = req.body;
let error = '';
try {
if (!request.workerId)
throw new Error(`key 'workerId' is wrong`);
res.writeHead(200, {
'Content-Type': 'text/json; charset=utf-8',
});
const splitter = '-- aod2t5hDXU32bUFyqlFE next status --';
const refreshPause = 200;
let i = 0;
let prevProgress = -1;
let prevState = '';
let state;
while (1) {// eslint-disable-line no-constant-condition
state = this.workerState.getState(request.workerId);
if (!state) break;
res.write(splitter + JSON.stringify(state));
res.flush();
if (state.state != 'finish' && state.state != 'error')
await utils.sleep(refreshPause);
else
break;
i++;
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
res.write(splitter + JSON.stringify({state: 'error', error: 'Слишком долгое время ожидания'}));
break;
}
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
prevProgress = state.progress;
prevState = state.state;
}
if (!state) {
res.write(splitter + JSON.stringify({}));
}
res.end();
return false;
} catch (e) {
error = e.message;
}
//bad request
res.status(400).send({error});
return false;
}
} }
module.exports = WorkerController; module.exports = WorkerController;

View File

@@ -3,4 +3,5 @@ module.exports = {
ReaderController: require('./ReaderController'), ReaderController: require('./ReaderController'),
WorkerController: require('./WorkerController'), WorkerController: require('./WorkerController'),
WebSocketController: require('./WebSocketController'), WebSocketController: require('./WebSocketController'),
BookUpdateCheckerController: require('./BookUpdateCheckerController'),
} }

View File

@@ -21,10 +21,10 @@ class AsyncExit {
} }
_init(signals, codeOnSignal) { _init(signals, codeOnSignal) {
const runSingalCallbacks = async(signal) => { const runSingalCallbacks = async(signal, err, origin) => {
for (const signalCallback of this.onSignalCallbacks.keys()) { for (const signalCallback of this.onSignalCallbacks.keys()) {
try { try {
await signalCallback(signal); await signalCallback(signal, err, origin);
} catch(e) { } catch(e) {
console.error(e); console.error(e);
} }
@@ -32,8 +32,8 @@ class AsyncExit {
}; };
for (const signal of signals) { for (const signal of signals) {
process.once(signal, async() => { process.once(signal, async(err, origin) => {
await runSingalCallbacks(signal); await runSingalCallbacks(signal, err, origin);
this.exit(codeOnSignal); this.exit(codeOnSignal);
}); });
} }

View File

@@ -0,0 +1,24 @@
let instance = null;
//singleton
class BUCServer {
constructor(config) {
if (!instance) {
this.config = Object.assign({}, config);
this.config.tempDownloadDir = `${config.tempDir}/download`;
fs.ensureDirSync(this.config.tempDownloadDir);
this.down = new FileDownloader(config.maxUploadFileSize);
instance = this;
}
return instance;
}
async main() {
}
}
module.exports = BUCServer;

View File

@@ -1,4 +1,4 @@
const got = require('got'); const axios = require('axios');
class FileDownloader { class FileDownloader {
constructor(limitDownloadSize = 0) { constructor(limitDownloadSize = 0) {
@@ -7,54 +7,82 @@ class FileDownloader {
async load(url, callback, abort) { async load(url, callback, abort) {
let errMes = ''; let errMes = '';
const options = { const options = {
headers: { headers: {
'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0' 'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'
}, },
responseType: 'buffer', responseType: 'stream',
}; };
const response = await got(url, Object.assign({}, options, {method: 'HEAD'}));
let estSize = 0;
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
let prevProg = 0;
const request = got(url, options);
request.on('downloadProgress', progress => {
if (this.limitDownloadSize) {
if (progress.transferred > this.limitDownloadSize) {
errMes = 'Файл слишком большой';
request.cancel();
}
}
let prog = 0;
if (estSize)
prog = Math.round(progress.transferred/estSize*100);
else if (progress.transferred)
prog = Math.round(progress.transferred/(progress.transferred + 200000)*100);
if (prog != prevProg && callback)
callback(prog);
prevProg = prog;
if (abort && abort()) {
errMes = 'abort';
request.cancel();
}
});
try { try {
return (await request).body; const res = await axios.get(url, options);
let estSize = 0;
if (res.headers['content-length']) {
estSize = res.headers['content-length'];
}
if (this.limitDownloadSize && estSize > this.limitDownloadSize) {
throw new Error('Файл слишком большой');
}
let prevProg = 0;
let transferred = 0;
const download = this.streamToBuffer(res.data, (chunk) => {
transferred += chunk.length;
if (this.limitDownloadSize) {
if (transferred > this.limitDownloadSize) {
errMes = 'Файл слишком большой';
res.request.abort();
}
}
let prog = 0;
if (estSize)
prog = Math.round(transferred/estSize*100);
else
prog = Math.round(transferred/(transferred + 200000)*100);
if (prog != prevProg && callback)
callback(prog);
prevProg = prog;
if (abort && abort()) {
errMes = 'abort';
res.request.abort();
}
});
return await download;
} catch (error) { } catch (error) {
errMes = (errMes ? errMes : error.message); errMes = (errMes ? errMes : error.message);
throw new Error(errMes); throw new Error(errMes);
} }
} }
streamToBuffer(stream, progress) {
return new Promise((resolve, reject) => {
if (!progress)
progress = () => {};
const _buf = [];
stream.on('data', (chunk) => {
_buf.push(chunk);
progress(chunk);
});
stream.on('end', () => resolve(Buffer.concat(_buf)));
stream.on('error', (err) => {
reject(err);
});
stream.on('aborted', () => {
reject(new Error('aborted'));
});
});
}
} }
module.exports = FileDownloader; module.exports = FileDownloader;

View File

@@ -49,7 +49,7 @@ class BaseLog {
this.outputBuffer = []; this.outputBuffer = [];
await this.flushImpl(this.data) await this.flushImpl(this.data)
.catch(e => { console.log(e); ayncExit.exit(1); } ); .catch(e => { console.error(`Logger error: ${e}`); ayncExit.exit(1); } );
this.flushing = false; this.flushing = false;
} }
@@ -188,8 +188,8 @@ class Logger {
} }
this.closed = false; this.closed = false;
ayncExit.onSignal((signal) => { ayncExit.onSignal((signal, err) => {
this.log(LM_FATAL, `Signal ${signal} received, exiting...`); this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", exiting...`);
}); });
ayncExit.addAfter(this.close.bind(this)); ayncExit.addAfter(this.close.bind(this));
} }
@@ -218,6 +218,8 @@ class Logger {
} else { } else {
console.log(mes); console.log(mes);
} }
return mes;
} }
async close() { async close() {

View File

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

View File

@@ -2,6 +2,7 @@ const _ = require('lodash');
const utils = require('../utils'); const utils = require('../utils');
const JembaConnManager = require('../../db/JembaConnManager');//singleton const JembaConnManager = require('../../db/JembaConnManager');//singleton
const log = new (require('../AppLogger'))().log;//singleton
let instance = null; let instance = null;
@@ -20,25 +21,30 @@ class JembaReaderStorage {
} }
async doAction(act) { async doAction(act) {
if (!_.isObject(act.items)) try {
throw new Error('items is not an object'); if (!_.isObject(act.items))
throw new Error('items is not an object');
let result = {}; let result = {};
switch (act.action) { switch (act.action) {
case 'check': case 'check':
result = await this.checkItems(act.items); result = await this.checkItems(act.items);
break; break;
case 'get': case 'get':
result = await this.getItems(act.items); result = await this.getItems(act.items);
break; break;
case 'set': case 'set':
result = await this.setItems(act.items, act.force); result = await this.setItems(act.items, act.force);
break; break;
default: default:
throw new Error('Unknown action'); throw new Error('Unknown action');
}
return result;
} catch (e) {
log(LM_ERR, `JembaReaderStorage: ${e.message}`);
throw e;
} }
return result;
} }
async checkItems(items) { async checkItems(items) {

View File

@@ -6,12 +6,12 @@ const WorkerState = require('../WorkerState');//singleton
const FileDownloader = require('../FileDownloader'); const FileDownloader = require('../FileDownloader');
const FileDecompressor = require('../FileDecompressor'); const FileDecompressor = require('../FileDecompressor');
const BookConverter = require('./BookConverter'); const BookConverter = require('./BookConverter');
const RemoteWebDavStorage = require('../RemoteWebDavStorage'); const RemoteStorage = require('../RemoteStorage');
const utils = require('../utils'); const utils = require('../utils');
const log = new (require('../AppLogger'))().log;//singleton const log = new (require('../AppLogger'))().log;//singleton
const cleanDirPeriod = 60*60*1000;//1 раз в час const cleanDirPeriod = 30*60*1000;//раз в полчаса
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
let instance = null; let instance = null;
@@ -33,15 +33,27 @@ class ReaderWorker {
this.decomp = new FileDecompressor(3*config.maxUploadFileSize); this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
this.bookConverter = new BookConverter(this.config); this.bookConverter = new BookConverter(this.config);
this.remoteWebDavStorage = false; this.remoteStorage = false;
if (config.remoteWebDavStorage) { if (config.remoteStorage) {
this.remoteWebDavStorage = new RemoteWebDavStorage( this.remoteStorage = new RemoteStorage(
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteWebDavStorage) Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteStorage)
); );
} }
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, cleanDirPeriod); this.remoteConfig = {
this.periodicCleanDir(this.config.uploadDir, this.config.maxUploadPublicDirSize, cleanDirPeriod); '/tmp': {
dir: this.config.tempPublicDir,
maxSize: this.config.maxTempPublicDirSize,
moveToRemote: true,
},
'/upload': {
dir: this.config.uploadDir,
maxSize: this.config.maxUploadPublicDirSize,
moveToRemote: true,
}
};
this.periodicCleanDir(this.remoteConfig);//no await
instance = this; instance = this;
} }
@@ -54,7 +66,6 @@ class ReaderWorker {
let decompDir = ''; let decompDir = '';
let downloadedFilename = ''; let downloadedFilename = '';
let isUploaded = false; let isUploaded = false;
let isRestored = false;
let convertFilename = ''; let convertFilename = '';
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.'; const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
@@ -94,8 +105,7 @@ class ReaderWorker {
if (!await fs.pathExists(downloadedFilename)) { if (!await fs.pathExists(downloadedFilename)) {
//если удалено из upload, попробуем восстановить из удаленного хранилища //если удалено из upload, попробуем восстановить из удаленного хранилища
try { try {
downloadedFilename = await this.restoreRemoteFile(fileHash); await this.restoreRemoteFile(fileHash, '/upload');
isRestored = true;
} catch(e) { } catch(e) {
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.'); throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
} }
@@ -144,33 +154,6 @@ class ReaderWorker {
const finishFilename = path.basename(compFilename); const finishFilename = path.basename(compFilename);
wState.finish({path: `/tmp/${finishFilename}`, size: stat.size}); wState.finish({path: `/tmp/${finishFilename}`, size: stat.size});
//лениво сохраним compFilename в удаленном хранилище
if (this.remoteWebDavStorage) {
(async() => {
await utils.sleep(20*1000);
try {
//log(`remoteWebDavStorage.putFile ${path.basename(compFilename)}`);
await this.remoteWebDavStorage.putFile(compFilename);
} catch (e) {
log(LM_ERR, e.stack);
}
})();
}
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
(async() => {
await utils.sleep(30*1000);
try {
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
} catch (e) {
log(LM_ERR, e.stack);
}
})();
}
} catch (e) { } catch (e) {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
let mes = e.message.split('|FORLOG|'); let mes = e.message.split('|FORLOG|');
@@ -219,14 +202,41 @@ class ReaderWorker {
return `disk://${hash}`; return `disk://${hash}`;
} }
async restoreRemoteFile(filename) { async saveFileBuf(buf) {
const hash = await utils.getBufHash(buf, 'sha256', 'hex');
const outFilename = `${this.config.uploadDir}/${hash}`;
if (!await fs.pathExists(outFilename)) {
await fs.writeFile(outFilename, buf);
} else {
await utils.touchFile(outFilename);
}
return `disk://${hash}`;
}
async uploadFileTouch(url) {
const outFilename = `${this.config.uploadDir}/${url.replace('disk://', '')}`;
await utils.touchFile(outFilename);
return url;
}
async restoreRemoteFile(filename, remoteDir) {
let targetDir = '';
if (this.remoteConfig[remoteDir])
targetDir = this.remoteConfig[remoteDir].dir;
else
throw new Error(`restoreRemoteFile: unknown remoteDir value (${remoteDir})`);
const basename = path.basename(filename); const basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`; const targetName = `${targetDir}/${basename}`;
if (!await fs.pathExists(targetName)) { if (!await fs.pathExists(targetName)) {
let found = false; let found = false;
if (this.remoteWebDavStorage) { if (this.remoteStorage) {
found = await this.remoteWebDavStorage.getFileSuccess(targetName); found = await this.remoteStorage.getFileSuccess(targetName, remoteDir);
} }
if (!found) { if (!found) {
@@ -237,83 +247,81 @@ class ReaderWorker {
return targetName; return targetName;
} }
restoreCachedFile(filename) { async cleanDir(dir, remoteDir, maxSize, moveToRemote) {
const workerId = this.workerState.generateWorkerId(); if (!this.remoteSent)
const wState = this.workerState.getControl(workerId); this.remoteSent = {};
wState.set({state: 'start'}); if (!this.remoteSent[remoteDir])
this.remoteSent[remoteDir] = {};
(async() => { const sent = this.remoteSent[remoteDir];
try {
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
const targetName = await this.restoreRemoteFile(filename); const list = await fs.readdir(dir);
const stat = await fs.stat(targetName);
const basename = path.basename(filename); let size = 0;
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100}); let files = [];
} catch (e) { for (const filename of list) {
if (e.message.indexOf('404') < 0) const filePath = `${dir}/${filename}`;
log(LM_ERR, e.stack); const stat = await fs.stat(filePath);
wState.set({state: 'error', error: e.message}); if (!stat.isDirectory()) {
size += stat.size;
files.push({name: filePath, stat});
} }
})(); }
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
return workerId; files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
if (moveToRemote && this.remoteStorage) {
for (const file of files) {
if (sent[file.name])
continue;
//отправляем в remoteStorage
try {
log(`remoteStorage.putFile ${remoteDir}/${path.basename(file.name)}`);
await this.remoteStorage.putFile(file.name, remoteDir);
sent[file.name] = true;
} catch (e) {
log(LM_ERR, e.stack);
}
}
}
let i = 0;
let j = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
const oldFile = file.name;
//реально удаляем только если сохранили в хранилище или размер dir увеличен в 1.5 раза
if (!(moveToRemote && this.remoteStorage)
|| (moveToRemote && this.remoteStorage && sent[oldFile])
|| size > maxSize*1.5) {
await fs.remove(oldFile);
delete sent[oldFile];
j++;
}
size -= file.stat.size;
i++;
}
log(`removed ${j} files`);
} }
async periodicCleanDir(dir, maxSize, timeout) { async periodicCleanDir(cleanConfig) {
try { while (1) {// eslint-disable-line no-constant-condition
const list = await fs.readdir(dir); for (const [remoteDir, config] of Object.entries(cleanConfig)) {
try {
let size = 0; await this.cleanDir(config.dir, remoteDir, config.maxSize, config.moveToRemote);
let files = []; } catch(e) {
for (const name of list) { log(LM_ERR, e.stack);
const stat = await fs.stat(`${dir}/${name}`);
if (!stat.isDirectory()) {
size += stat.size;
files.push({name, stat});
} }
} }
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs); await utils.sleep(cleanDirPeriod);
let i = 0;
let j = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
const oldFile = `${dir}/${file.name}`;
let remoteSuccess = true;
//отправляем только this.config.tempPublicDir
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
remoteSuccess = false;
try {
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
await this.remoteWebDavStorage.putFile(oldFile);
remoteSuccess = true;
} catch (e) {
log(LM_ERR, e.stack);
}
}
//реально удаляем только если сохранили в хранилище
if (remoteSuccess || size > maxSize*1.2) {
await fs.remove(oldFile);
j++;
}
size -= file.stat.size;
i++;
}
log(`removed ${j} files`);
} catch(e) {
log(LM_ERR, e.stack);
} finally {
setTimeout(() => {
this.periodicCleanDir(dir, maxSize, timeout);
}, timeout);
} }
} }
} }
module.exports = ReaderWorker; module.exports = ReaderWorker;

View File

@@ -0,0 +1,98 @@
const fs = require('fs-extra');
const path = require('path');
const WebSocketConnection = require('./WebSocketConnection');
class RemoteStorage {
constructor(config) {
this.config = Object.assign({}, config);
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
this.accessToken = this.config.accessToken;
this.wsc = new WebSocketConnection(config.url);
}
async wsQuery(query) {
const response = await this.wsc.message(
await this.wsc.send(Object.assign({accessToken: this.accessToken}, query), 30),
300
);
if (response.error)
throw new Error(response.error);
return response;
}
async wsStat(fileName) {
return await this.wsQuery({action: 'get-stat', fileName});
}
async wsGetFile(fileName) {
return this.wsQuery({action: 'get-file', fileName});
}
async wsPutFile(fileName, data) {//data base64 encoded string
return this.wsQuery({action: 'put-file', fileName, data});
}
async wsDelFile(fileName) {
return this.wsQuery({action: 'del-file', fileName});
}
makeRemoteFileName(fileName, dir = '') {
const base = path.basename(fileName);
if (base.length > 3) {
return `${dir}/${base.substr(0, 3)}/${base}`;
} else {
return `${dir}/${base}`;
}
}
async putFile(fileName, dir = '') {
if (!await fs.pathExists(fileName)) {
throw new Error(`File not found: ${fileName}`);
}
const remoteFilename = this.makeRemoteFileName(fileName, dir);
try {
const localStat = await fs.stat(fileName);
let remoteStat = await this.wsStat(remoteFilename);
remoteStat = remoteStat.stat;
if (remoteStat.isFile && localStat.size == remoteStat.size) {
return;
}
await this.wsDelFile(remoteFilename);
} catch (e) {
//
}
const data = await fs.readFile(fileName, 'base64');
await this.wsPutFile(remoteFilename, data);
}
async getFile(fileName, dir = '') {
if (await fs.pathExists(fileName)) {
return;
}
const remoteFilename = this.makeRemoteFileName(fileName, dir);
const response = await this.wsGetFile(remoteFilename);
await fs.writeFile(fileName, response.data, 'base64');
}
async getFileSuccess(filename, dir = '') {
try {
await this.getFile(filename, dir);
return true;
} catch (e) {
//
}
return false;
}
}
module.exports = RemoteStorage;

View File

@@ -46,16 +46,16 @@ class RemoteWebDavStorage {
return await this.wdc.createDirectory(dirname); return await this.wdc.createDirectory(dirname);
} }
async putFile(filename) { async putFile(filename, dir = '') {
if (!await fs.pathExists(filename)) { if (!await fs.pathExists(filename)) {
throw new Error(`File not found: ${filename}`); throw new Error(`File not found: ${filename}`);
} }
const base = path.basename(filename); const base = path.basename(filename);
let remoteFilename = `/${base}`; let remoteFilename = `${dir}/${base}`;
if (base.length > 3) { if (base.length > 3) {
const remoteDir = `/${base.substr(0, 3)}`; const remoteDir = `${dir}/${base.substr(0, 3)}`;
try { try {
await this.mkdir(remoteDir); await this.mkdir(remoteDir);
} catch (e) { } catch (e) {
@@ -79,24 +79,24 @@ class RemoteWebDavStorage {
await this.writeFile(remoteFilename, data); await this.writeFile(remoteFilename, data);
} }
async getFile(filename) { async getFile(filename, dir = '') {
if (await fs.pathExists(filename)) { if (await fs.pathExists(filename)) {
return; return;
} }
const base = path.basename(filename); const base = path.basename(filename);
let remoteFilename = `/${base}`; let remoteFilename = `${dir}/${base}`;
if (base.length > 3) { if (base.length > 3) {
remoteFilename = `/${base.substr(0, 3)}/${base}`; remoteFilename = `${dir}/${base.substr(0, 3)}/${base}`;
} }
const data = await this.readFile(remoteFilename); const data = await this.readFile(remoteFilename);
await fs.writeFile(filename, data); await fs.writeFile(filename, data);
} }
async getFileSuccess(filename) { async getFileSuccess(filename, dir = '') {
try { try {
await this.getFile(filename); await this.getFile(filename, dir);
return true; return true;
} catch (e) { } catch (e) {
// //

View File

@@ -94,7 +94,7 @@ class WebSocketConnection {
this.ws = new this.WebSocket(this.url); this.ws = new this.WebSocket(this.url);
} }
const onopen = (e) => { const onopen = () => {
this.connecting = false; this.connecting = false;
resolve(this.ws); resolve(this.ws);
}; };

View File

@@ -13,7 +13,7 @@ function toBase36(data) {
} }
function fromBase36(data) { function fromBase36(data) {
return bs36.decode(data); return Buffer.from(bs36.decode(data));
} }
function bufferRemoveZeroes(buf) { function bufferRemoveZeroes(buf) {
@@ -34,6 +34,12 @@ function getFileHash(filename, hashName, enc) {
}); });
} }
function getBufHash(buf, hashName, enc) {
const hash = crypto.createHash(hashName);
hash.update(buf);
return hash.digest(enc);
}
function sleep(ms) { function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
@@ -129,6 +135,7 @@ module.exports = {
fromBase36, fromBase36,
bufferRemoveZeroes, bufferRemoveZeroes,
getFileHash, getFileHash,
getBufHash,
sleep, sleep,
toUnixTime, toUnixTime,
randomHexString, randomHexString,

View File

@@ -0,0 +1,16 @@
module.exports = {
up: [
['create', {
table: 'checked',
index: [
{field: 'queryTime', type: 'number'},
{field: 'checkTime', type: 'number'},
]
}],
],
down: [
['drop', {
table: 'checked'
}],
]
};

View File

@@ -0,0 +1,6 @@
module.exports = {
table: 'migration1',
data: [
{id: 1, name: 'create', data: require('./001-create')}
]
}

View File

@@ -1,4 +1,4 @@
module.exports = { module.exports = {
//'app': require('./jembaMigrations/app'),
'reader-storage': require('./reader-storage'), 'reader-storage': require('./reader-storage'),
'book-update-server': require('./book-update-server'),
}; };

View File

@@ -1,6 +1,7 @@
require('tls').DEFAULT_MIN_VERSION = 'TLSv1'; require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path');
const argv = require('minimist')(process.argv.slice(2)); const argv = require('minimist')(process.argv.slice(2));
const express = require('express'); const express = require('express');
const compression = require('compression'); const compression = require('compression');
@@ -11,6 +12,8 @@ const ayncExit = new (require('./core/AsyncExit'))();
let log = null; let log = null;
const maxPayloadSize = 50;//in MB
async function init() { async function init() {
//config //config
const configManager = new (require('./config'))();//singleton const configManager = new (require('./config'))();//singleton
@@ -63,7 +66,7 @@ async function main() {
if (serverCfg.mode !== 'none') { if (serverCfg.mode !== 'none') {
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const wss = new WebSocket.Server({ server, maxPayload: 10*1024*1024 }); const wss = new WebSocket.Server({ server, maxPayload: maxPayloadSize*1024*1024 });
const serverConfig = Object.assign({}, config, serverCfg); const serverConfig = Object.assign({}, config, serverCfg);
@@ -75,20 +78,10 @@ async function main() {
} }
app.use(compression({ level: 1 })); app.use(compression({ level: 1 }));
app.use(express.json({limit: '10mb'})); app.use(express.json({limit: `${maxPayloadSize}mb`}));
if (devModule) if (devModule)
devModule.logQueries(app); devModule.logQueries(app);
app.use(express.static(serverConfig.publicDir, {
maxAge: '30d',
setHeaders: (res, filePath) => {
if (path.basename(path.dirname(filePath)) == 'tmp') {
res.set('Content-Type', 'application/xml');
res.set('Content-Encoding', 'gzip');
}
}
}));
require('./routes').initRoutes(app, wss, serverConfig); require('./routes').initRoutes(app, wss, serverConfig);
if (devModule) { if (devModule) {

View File

@@ -1,8 +1,24 @@
const c = require('./controllers'); const fs = require('fs-extra');
const utils = require('./core/utils'); const path = require('path');
const express = require('express');
const multer = require('multer'); const multer = require('multer');
const ReaderWorker = require('./core/Reader/ReaderWorker');//singleton
const log = new (require('./core/AppLogger'))().log;//singleton
const c = require('./controllers');
const utils = require('./core/utils');
function initRoutes(app, wss, config) { function initRoutes(app, wss, config) {
//эксклюзив для update_checker
if (config.mode === 'book_update_checker') {
new c.BookUpdateCheckerController(wss, config);
return;
}
initStatic(app, config);
const misc = new c.MiscController(config); const misc = new c.MiscController(config);
const reader = new c.ReaderController(config); const reader = new c.ReaderController(config);
const worker = new c.WorkerController(config); const worker = new c.WorkerController(config);
@@ -29,9 +45,7 @@ function initRoutes(app, wss, config) {
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}], ['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}], ['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}],
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}], ['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}], ['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
['POST', '/api/worker/get-state-finish', worker.getStateFinish.bind(worker), [aAll], {}],
]; ];
//to app //to app
@@ -78,6 +92,48 @@ function initRoutes(app, wss, config) {
} }
} }
function initStatic(app, config) {
const readerWorker = new ReaderWorker(config);
//восстановление файлов в /tmp и /upload из webdav-storage, при необходимости
app.use(async(req, res, next) => {
if ((req.method !== 'GET' && req.method !== 'HEAD') ||
!(req.path.indexOf('/tmp/') === 0 || req.path.indexOf('/upload/') === 0)
) {
return next();
}
const filePath = `${config.publicDir}${req.path}`;
//восстановим
try {
if (!await fs.pathExists(filePath)) {
if (req.path.indexOf('/tmp/') === 0) {
await readerWorker.restoreRemoteFile(req.path, '/tmp');
} else if (req.path.indexOf('/upload/') === 0) {
await readerWorker.restoreRemoteFile(req.path, '/upload');
}
}
} catch(e) {
log(LM_ERR, `Static.restoreRemoteFile: ${e.message}`);
}
return next();
});
const tmpDir = `${config.publicDir}/tmp`;
app.use(express.static(config.publicDir, {
maxAge: '30d',
setHeaders: (res, filePath) => {
if (path.dirname(filePath) == tmpDir) {
res.set('Content-Type', 'application/xml');
res.set('Content-Encoding', 'gzip');
}
},
}));
}
module.exports = { module.exports = {
initRoutes initRoutes
} }