Compare commits

...

156 Commits

Author SHA1 Message Date
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
Book Pauk
48668d94ad Merge branch 'release/0.11.4' 2022-04-14 19:05:31 +07:00
Book Pauk
e08c431dd9 Версия 0.11.4 2022-04-14 19:05:07 +07:00
Book Pauk
5ee58ad6f0 Поправка багов 2022-04-14 19:00:04 +07:00
Book Pauk
ac0a4f0586 Добавлена кнопка 'Управление кликом' 2022-04-14 18:50:11 +07:00
Book Pauk
b6f4c153e5 Добавлена кнопка 'Загрузить из буфера обмена' 2022-04-14 18:34:41 +07:00
Book Pauk
4fdaf5f555 Добавлена кнопка 'Загрузить файл с диска' 2022-04-14 17:48:51 +07:00
Book Pauk
b4ee9d6c00 Скрыта опция "Помочь проекту".
Добавлена кнопка "Вызвать справку".
2022-04-14 17:27:29 +07:00
Book Pauk
7c73c74730 Добавлена подсказка при невалидном URL книги 2022-04-14 17:13:38 +07:00
Book Pauk
c20aa089fa npm 2022-03-29 17:45:57 +07:00
Book Pauk
b0e15c22ea Merge tag '0.11.3' into develop
0.11.3
2022-03-29 17:41:03 +07:00
Book Pauk
d58a2c065a Merge branch 'release/0.11.3' 2022-03-29 17:40:57 +07:00
Book Pauk
53135e7ee8 Поправка даты 2022-03-29 17:40:29 +07:00
Book Pauk
5c48ca9e6c Рефакторинг versionHistory, небольшие поправки 2022-03-29 17:37:24 +07:00
Book Pauk
c4a280f3d8 Скрыл устаревший чекбокс 2022-03-29 16:52:03 +07:00
Book Pauk
ba2943c722 Поправлен баг 2022-03-29 16:49:04 +07:00
Book Pauk
26f6ffc83a Убрал PayPal из списка 2022-03-29 16:25:26 +07:00
Book Pauk
bcf075a72c Доработки WebSocketConnection 2022-03-29 16:23:34 +07:00
Book Pauk
02d458d192 Миграция "jembadb" => "^2.3.0" 2022-03-29 15:49:48 +07:00
Book Pauk
a349d8af68 Обновил пакет JembaDb 2022-02-08 20:55:31 +07:00
Book Pauk
0dbaf32aac Merge tag '0.11.2' into develop
0.11.2
2022-01-11 23:25:23 +07:00
Book Pauk
e8c41ef3a8 Merge branch 'release/0.11.2' 2022-01-11 23:24:58 +07:00
Book Pauk
e43a44e986 0.11.2 2022-01-11 23:24:37 +07:00
Book Pauk
f14b8ed277 Добавлена реакция на сигнал SIGUSR2 2022-01-11 23:23:54 +07:00
Book Pauk
bbfe8a64cb Мелкая поправка 2022-01-11 23:11:04 +07:00
Book Pauk
bcf3c2dab0 Улучшение обработки ошибок 2022-01-11 22:23:35 +07:00
Book Pauk
d5404fd260 Убрал устаревший код 2022-01-11 21:30:43 +07:00
Book Pauk
54bc662e43 Поправил конфиг для nginx 2021-12-24 17:59:26 +07:00
Book Pauk
42546ca97e Обновление jembadb до версии 1.3.0 2021-12-21 20:21:32 +07:00
Book Pauk
5c13cf0eb9 Добавил -C GZip для pkg 2021-12-20 17:27:04 +07:00
Book Pauk
2a9d44ae9a Поправка конфига для eslint 2021-12-20 17:26:19 +07:00
Book Pauk
38414ae7b6 Переход на пакет jembadb 2021-12-17 20:05:57 +07:00
Book Pauk
3ecb3e80ac Удалил комментарии 2021-12-12 01:56:24 +07:00
Book Pauk
4968828488 Merge tag '0.11.1-2' into develop
0.11.1-2
2021-12-03 15:25:17 +07:00
Book Pauk
4db3cd24df Merge branch 'release/0.11.1-2' 2021-12-03 15:25:11 +07:00
Book Pauk
45c6d3da77 Поправил таймаут, улучшение скорости синхронизации 2021-12-03 15:16:39 +07:00
Book Pauk
4aab1da3c6 Merge tag '0.11.1-1' into develop
0.11.1-1
2021-12-03 15:03:46 +07:00
Book Pauk
bf5dfa1c15 Merge branch 'release/0.11.1-1' 2021-12-03 15:03:37 +07:00
Book Pauk
7549bdd2b4 Обновил pkg 2021-12-03 15:02:56 +07:00
Book Pauk
1bb2525ab2 Merge tag '0.11.1' into develop
0.11.1
2021-12-03 14:35:04 +07:00
66 changed files with 8198 additions and 10547 deletions

View File

@@ -12,6 +12,7 @@
"@babel"
],
"env": {
"es6": true,
"browser": true,
"node": true
},
@@ -30,6 +31,7 @@
"vue/max-attributes-per-line": "off",
"vue/html-self-closing": "off",
"vue/no-v-html": "off",
"vue/no-v-model-argument": "off",
"strict": 0,
"indent": [0, 4, {

View File

@@ -4,7 +4,7 @@ const util = require('util');
const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);
const got = require('got');
const axios = require('axios');
const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist');
@@ -29,7 +29,8 @@ async function main() {
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем 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}`);
//распаковываем
@@ -46,7 +47,8 @@ async function main() {
// Скачиваем ipfs
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}`);
//распаковываем

View File

@@ -5,11 +5,12 @@ const { VueLoaderPlugin } = require('vue-loader');
const clientDir = path.resolve(__dirname, '../client');
module.exports = {
/*resolve: {
resolve: {
alias: {
vue: '@vue/compat'
ws: false,
//vue: '@vue/compat'
}
},*/
},
entry: [`${clientDir}/main.js`],
output: {
publicPath: '/app/',
@@ -62,34 +63,6 @@ module.exports = {
filename: 'fonts/[name]-[hash:6][ext]'
},
},
/*{
test: /\.gif$/,
loader: "url-loader",
options: {
name: "images/[name]-[hash:6].[ext]"
}
},
{
test: /\.png$/,
loader: "url-loader",
options: {
name: "images/[name]-[hash:6].[ext]"
}
},
{
test: /\.jpg$/,
loader: "file-loader",
options: {
name: "images/[name]-[hash:6].[ext]"
}
},
{
test: /\.(ttf|eot|woff|woff2)$/,
loader: "file-loader",
options: {
name: "fonts/[name]-[hash:6].[ext]"
}
},*/
]
},

View File

@@ -4,7 +4,7 @@ const util = require('util');
const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);
const got = require('got');
const axios = require('axios');
const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist');
@@ -29,7 +29,8 @@ async function main() {
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем 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}`);
//распаковываем
@@ -46,7 +47,8 @@ async function main() {
// Скачиваем ipfs
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}`);
//распаковываем

View File

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

View File

@@ -219,7 +219,7 @@ class Reader {
const state = response.state;
if (!state)
throw new Error('Неверный ответ api');
if (response.state == 'error') {
if (state == 'error') {
throw new Error(response.error);
}

View File

@@ -11,7 +11,7 @@
Открыть выбранную закладку
</q-tooltip>
</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>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
</template>
@@ -55,16 +55,16 @@
<div class="col fit tree">
<div v-show="nodes.length" class="checkbox-tick-all">
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @input="makeTickAll" />
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
</div>
<q-tree
v-model:selected="selected"
v-model:ticked="ticked"
v-model:expanded="expanded"
class="q-my-xs"
:nodes="nodes"
node-key="key"
tick-strategy="leaf"
v-model:selected="selected"
v-model:ticked="ticked"
v-model:expanded="expanded"
selected-color="black"
:filter="search"
no-nodes-label="Закладок пока нет"
@@ -97,7 +97,7 @@ const componentOptions = {
Window,
},
watch: {
ticked: function() {
ticked() {
this.checkAllTicked();
},
}

View File

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

View File

@@ -23,15 +23,15 @@
<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 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)">
<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 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 class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
@@ -42,8 +42,12 @@
</div>
</div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
<div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
<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="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
@@ -61,10 +65,10 @@
</div>
</div>
<div v-show="selectedTab == 'images'" class="tab-panel">
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div :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="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
@@ -124,7 +128,10 @@ const componentOptions = {
watch: {
bookPos() {
this.updateBookPosSelection();
}
},
selectedTab() {
this.updateBookPosScrollTop();
},
},
};
class ContentsPage {
@@ -282,31 +289,30 @@ class ContentsPage {
if (!this.isVisible)
return;
await utils.sleep(50);
await this.$nextTick();
const bp = this.bookPos;
for (let i = 0; i < this.contents.length; i++) {
const item = this.contents[i];
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++) {
const subitem = item.list[j];
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
if (bp >= subitem.offset && bp < nextSubOffset) {
subitem.isBookPos = true;
this.contents[i] = Object.assign(item, {list: item.list});
this.updateBookPosScrollTop('contents', item, subitem, j);
} else if (subitem.isBookPos) {
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++) {
@@ -314,11 +320,96 @@ class ContentsPage {
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
if (bp >= img.offset && bp < nextOffset) {
this.images[i] = Object.assign(img, {isBookPos: true});
this.images[i].isBookPos = true;
} 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) {
@@ -326,17 +417,17 @@ class ContentsPage {
const expanded = !item.expanded;
if (!expanded) {
const subitems = this.$refs[`subitem${key}`];
subitems.style.height = '0';
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = '0';
await utils.sleep(200);
}
this.contents[key] = Object.assign({}, item, {expanded});
this.contents[key].expanded = expanded;
if (expanded) {
await this.$nextTick();
const subitems = this.$refs[`subitem${key}`];
subitems.style.height = subitems.scrollHeight + 'px';
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = subdiv.scrollHeight + 'px';
}
}

View File

@@ -19,7 +19,7 @@
</div>
</div>
<div class="address">
<!--div class="address">
<img class="logo" src="./assets/paypal.png">
<div class="para">
{{ paypalAddress }}
@@ -29,7 +29,7 @@
</q-tooltip>
</q-icon>
</div>
</div>
</div-->
<div class="address">
<img class="logo" src="./assets/bitcoin.png">

View File

@@ -5,13 +5,20 @@
</template>
<div class="col column" style="min-width: 600px">
<q-btn-toggle
v-model="selectedTab"
toggle-color="primary"
no-caps unelevated
:options="buttons"
/>
<div class="separator"></div>
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
inline-label
class="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>
<component :is="activePage" ref="page" class="col"></component>
@@ -29,14 +36,14 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
const pages = {
'CommonHelpPage': CommonHelpPage,
'HotkeysHelpPage': HotkeysHelpPage,
'MouseHelpPage': MouseHelpPage,
'VersionHistoryPage': VersionHistoryPage,
'DonateHelpPage': DonateHelpPage,
//'DonateHelpPage': DonateHelpPage,
};
const tabs = [
@@ -44,7 +51,7 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'],
//['DonateHelpPage', 'Помочь проекту'],
];
const componentOptions = {
@@ -73,7 +80,7 @@ class HelpPage {
}
activateDonateHelpPage() {
this.selectedTab = 'DonateHelpPage';
//this.selectedTab = 'DonateHelpPage';
}
activateVersionHistoryHelpPage() {
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
</script>
<style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style>

View File

@@ -33,14 +33,15 @@ class VersionHistoryPage {
mounted() {
let vh = [];
for (const version of versionHistory) {
vh.push(version.header);
for (const v of versionHistory) {
vh.push(`${v.version} (${v.releaseDate})`);
}
this.versionHeader = vh;
let vc = [];
for (const version of versionHistory) {
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
for (const v of versionHistory) {
let header = `${v.version} (${v.releaseDate})`;
vc.push({key: header, content: 'Версия ' + header + v.content});
}
this.versionContent = vc;
}

View File

@@ -12,21 +12,31 @@
</div>
<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>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
</template>
</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>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
Загрузить файл с диска
</q-btn>
<div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена
</q-btn>
@@ -45,14 +55,27 @@
</div>
<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="openDonate">Помочь проекту</span>
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
</div>
<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>
</template>
@@ -62,12 +85,15 @@ import vueComponent from '../../vueComponent.js';
import GithubCorner from './GithubCorner/GithubCorner.vue';
import Dialog from '../../share/Dialog.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory';
import * as utils from '../../../share/utils';
const componentOptions = {
components: {
GithubCorner,
Dialog,
PasteTextPage,
},
};
@@ -77,6 +103,7 @@ class LoaderPage {
bookUrl = null;
loadPercent = 0;
pasteTextActive = false;
findBookVisible = false;
created() {
this.commit = this.$store.commit;
@@ -109,14 +136,16 @@ class LoaderPage {
return this.$store.state.config.version;
}
get acceptFileExt() {
return this.$store.state.config.acceptFileExt;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
get clientVersion() {
let v = versionHistory[0].header;
v = v.split(' ')[0];
return v;
return versionHistory[0].version;
}
submitUrl() {
@@ -138,7 +167,7 @@ class LoaderPage {
}
loadBufferClick() {
this.pasteTextToggle();
this.showPasteText();
}
loadBuffer(opts) {
@@ -148,6 +177,10 @@ class LoaderPage {
}
}
showPasteText() {
this.pasteTextActive = true;
}
pasteTextToggle() {
this.pasteTextActive = !this.pasteTextActive;
}
@@ -160,6 +193,10 @@ class LoaderPage {
this.$emit('do-action', {action: 'donate'});
}
findBook() {
this.findBookVisible = true;
}
openComments() {
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
}
@@ -168,26 +205,24 @@ class LoaderPage {
window.open('http://old.omnireader.ru', '_blank');
}
onInputKeydown(event) {
async onInputKeydown(event) {
if (event.key == 'Enter') {
await utils.sleep(100);
this.submitUrl();
}
}
keyHook(event) {
if (this.$refs.dialog1.active)
return true;
if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event);
}
const input = this.$refs.input.getNativeElement();
if (event.type == 'keydown' && document.activeElement !== input) {
const action = this.$root.readerActionByKeyEvent(event);
switch (action) {
case 'help':
this.openHelp(event);
return true;
}
}
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
return true;
return false;
}

View File

@@ -1,5 +1,5 @@
<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">
<q-circular-progress
show-value

View File

@@ -2,16 +2,35 @@
<div class="column no-wrap">
<div v-show="toolBarActive" ref="header" class="header">
<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')">
<q-icon name="la la-arrow-left" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loader'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
<q-icon name="la la-caret-square-up" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadFile'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
<q-icon name="la la-comment" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadBuffer'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
<q-icon name="la la-question" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['help'] }}
</q-tooltip>
</button>
</div>
<div>
<div class="row no-wrap">
<div class="space"></div>
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
<q-icon name="la la-angle-left" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -86,9 +105,16 @@
{{ rstore.readerActions['recentBooks'] }}
</q-tooltip>
</button>
<div class="space"></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')">
<q-icon name="la la-mouse" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['clickControl'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
<q-icon name="la la-unlink" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -115,6 +141,7 @@
@load-file="loadFile"
@book-pos-changed="bookPosChanged"
@do-action="doAction"
@hide-tool-bar="hideToolBar"
></component>
</keep-alive>
@@ -136,7 +163,7 @@
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
</div>
</div>
</template>
@@ -175,6 +202,7 @@ import miscApi from '../../api/misc';
import {versionHistory} from './versionHistory';
import * as utils from '../../share/utils';
import LockQueue from '../../share/LockQueue';
const componentOptions = {
components: {
@@ -245,6 +273,8 @@ class Reader {
rstore = {};
loaderActive = false;
loadFileActive = false;
loadBufferActive = false;
fullScreenActive = false;
setPositionActive = false;
searchActive = false;
@@ -254,6 +284,7 @@ class Reader {
contentsActive = false;
libsActive = false;
recentBooksActive = false;
clickControlActive = false;
offlineModeActive = false;
settingsActive = false;
@@ -284,6 +315,8 @@ class Reader {
this.reader = this.$store.state.reader;
this.config = this.$store.state.config;
this.lock = new LockQueue(100);
this.$root.addEventHook('key', this.keyHook);
this.lastActivePage = false;
@@ -310,12 +343,19 @@ class Reader {
await this.$nextTick();
this.paramPosIgnore = false;
}
}, 500, {maxWait: 5000});
}, 250, {maxWait: 5000});
this.scrollingSetRecentBook = _.debounce((newValue) => {
this.debouncedSetRecentBook(newValue);
}, 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', () => {
this.fullScreenActive = (document.fullscreenElement !== null);
});
@@ -324,8 +364,6 @@ class Reader {
}
mounted() {
this.updateHeaderMinWidth();
(async() => {
await wallpaperStorage.init();
await bookManager.init(this.settings);
@@ -372,8 +410,10 @@ class Reader {
this.copyFullText = settings.copyFullText;
this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl;
this.clickControlActive = this.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara;
@@ -388,11 +428,26 @@ class Reader {
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
}
this.updateHeaderMinWidth();
this.loadWallpapers();//no await
}
showHelpOnErrorIfNeeded(errorMessage) {
//небольшая эвристика
let i = errorMessage.indexOf('http://');
if (i < 0)
i = errorMessage.indexOf('https://');
errorMessage = errorMessage.substring(i + 7);
const perCount = errorMessage.split('%').length - 1;
if (perCount > errorMessage.length/3.2) {
this.$refs.dialogs.showUrlHelp();
return true;
}
return false;
}
//wallpaper css
async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength();
@@ -439,17 +494,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() {
const q = this.$route.query;
@@ -525,9 +569,7 @@ class Reader {
}
get clientVersion() {
let v = versionHistory[0].header;
v = v.split(' ')[0];
return v;
return versionHistory[0].version;
}
get routeParamUrl() {
@@ -585,7 +627,20 @@ class Reader {
//сохранение в serverStorage
if (value) {
await utils.sleep(500);
await this.$refs.serverStorage.saveRecent(value);
let timer = setTimeout(() => {
if (!this.offlineModeActive)
this.$root.notify.error('Таймаут соединения');
}, 10000);
try {
await this.$refs.serverStorage.saveRecent(value);
} catch (e) {
if (!this.offlineModeActive)
this.$root.notify.error(e.message);
} finally {
clearTimeout(timer);
}
}
}
}
@@ -619,6 +674,10 @@ class Reader {
this.$root.eventHook('resize');
}
hideToolBar(event) {
this.debouncedHideToolBar(event);
}
fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) {
@@ -646,6 +705,28 @@ class Reader {
}
}
loadFileToggle() {
if (!this.loaderActive)
this.loaderToggle();
this.$nextTick(() => {
const page = this.$refs.page;
if (this.activePage == 'LoaderPage' && page.loadFileClick) {
page.loadFileClick();
}
});
}
loadBufferToggle() {
if (!this.loaderActive)
this.loaderToggle();
this.$nextTick(() => {
const page = this.$refs.page;
if (this.activePage == 'LoaderPage' && page.showPasteText) {
page.showPasteText();
}
});
}
setPositionToggle() {
this.setPositionActive = !this.setPositionActive;
const page = this.$refs.page;
@@ -773,6 +854,12 @@ class Reader {
}
}
clickControlToggle() {
const newSettings = _.cloneDeep(this.settings);
newSettings.clickControl = !this.clickControl;
this.commit('reader/setSettings', newSettings);
}
offlineModeToggle() {
this.offlineModeActive = !this.offlineModeActive;
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
@@ -826,7 +913,7 @@ class Reader {
refreshBook() {
const mrb = this.mostRecentBook();
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
this.loadBook(Object.assign({}, mrb, {force: true}));
}
undoAction() {
@@ -861,6 +948,9 @@ class Reader {
switch (action) {
case 'loader':
case 'loadFile':
case 'loadBuffer':
case 'help':
case 'fullScreen':
case 'setPosition':
case 'search':
@@ -870,6 +960,7 @@ class Reader {
case 'contents':
case 'libs':
case 'recentBooks':
case 'clickControl':
case 'offlineMode':
case 'settings':
if (this.progressActive) {
@@ -907,7 +998,6 @@ class Reader {
classResult = classDisabled;
break;
case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive)
classResult = classDisabled;
break;
@@ -976,7 +1066,7 @@ class Reader {
return result;
}
async loadBook(opts) {
async _loadBook(opts) {
if (!opts || !opts.url) {
this.mostRecentBook();
return;
@@ -986,10 +1076,6 @@ class Reader {
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) &&
(url.indexOf('disk://') != 0))
url = 'http://' + url;
@@ -1016,33 +1102,36 @@ class Reader {
progress.show();
progress.setState({state: 'parse'});
// есть ли среди недавних
const key = bookManager.keyFromUrl(url);
let wasOpened = await bookManager.getRecentBook({key});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
// есть ли среди загруженных
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
wasOpened = Object.assign(wasOpened, {
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;
if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
progress.setState({progress: prog});
});
// если есть в локальном кэше
if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
this.mostRecentBook();
this.addAction(bookPos);
this.addAction(wasOpened.bookPos);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage();
this.checkBookPosPercent();
await this.activateClickMapPage();
this.activateClickMapPage();//no await
return;
}
@@ -1056,7 +1145,7 @@ class Reader {
});
book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) {
//молчим
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
}
}
}
@@ -1067,7 +1156,7 @@ class Reader {
if (!book) {
book = await readerApi.loadBook({
url,
uploadFileName,
uploadFileName: wasOpened.uploadFileName,
enableSitesFilter: this.enableSitesFilter,
skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false),
@@ -1084,14 +1173,44 @@ class Reader {
// добавляем в bookManager
progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (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.addAction(bookPos);
this.addAction(wasOpened.bookPos);
this.updateRoute(true);
this.loaderActive = false;
@@ -1102,17 +1221,28 @@ class Reader {
this.stopBlink = true;
this.checkBookPosPercent();
await this.activateClickMapPage();
this.activateClickMapPage();//no await
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
} finally {
this.checkNewVersionAvailable();
}
}
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;
await this.$nextTick();
@@ -1128,7 +1258,7 @@ class Reader {
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) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
@@ -1136,6 +1266,15 @@ class Reader {
}
}
async loadFile(opts) {
await this.lock.get();
try {
await this._loadFile(opts);
} finally {
this.lock.ret();
}
}
blinkCachedLoadMessage() {
if (!this.blinkCachedLoad)
return;
@@ -1172,6 +1311,12 @@ class Reader {
case 'loader':
this.loaderToggle();
break;
case 'loadFile':
this.loadFileToggle();
break;
case 'loadBuffer':
this.loadBufferToggle();
break;
case 'help':
this.helpToggle();
break;
@@ -1214,6 +1359,9 @@ class Reader {
case 'recentBooks':
this.recentBooksToggle();
break;
case 'clickControl':
this.clickControlToggle();
break;
case 'offlineMode':
this.offlineModeToggle();
break;
@@ -1316,13 +1464,14 @@ class Reader {
if (!result && event.type == 'keydown') {
const action = this.$root.readerActionByKeyEvent(event);
if (action == 'loader') {
/*if (action == 'loader') {
result = this.doAction({action, event});
}
if (!result && this.activePage == 'TextPage') {
result = this.doAction({action, event});
}
}*/
result = this.doAction({action, event});
}
}
return result;
@@ -1335,12 +1484,33 @@ export default vueComponent(Reader);
<style scoped>
.header {
height: 50px;
padding-left: 5px;
padding-right: 5px;
background-color: #1B695F;
color: #000;
overflow: hidden;
height: 50px;
overflow-x: auto;
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 {

View File

@@ -5,12 +5,17 @@
Что нового:
</template>
<div style="line-height: 20px" v-html="whatsNewContent"></div>
<div style="line-height: 20px; min-width: 300px">
<div v-html="whatsNewContent"></div>
</div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
</span>
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
<template #footer>
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
Больше не показывать
</q-btn>
</template>
</Dialog>
<Dialog ref="dialog2" v-model="donationVisible">
@@ -49,17 +54,40 @@
<br><br>
<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>
<span slot="footer">
<template #footer>
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">
Напомнить позже
</q-btn>
</template>
</Dialog>
<Dialog ref="dialog3" v-model="urlHelpVisible">
<template #header>
Обнаружена невалидная ссылка в поле "URL книги".
<br>
</template>
<div style="word-break: normal">
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
<a href="https://liberama.top" target="_blank">liberama.top</a>
<br><br>
Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена
</q-btn>
на странице загрузки.
</div>
</Dialog>
</div>
</template>
@@ -88,6 +116,7 @@ class ReaderDialogs {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
urlHelpVisible = false;
created() {
this.commit = this.$store.commit;
@@ -112,9 +141,9 @@ class ReaderDialogs {
const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) {
this.whatsNewHeader != this.whatsNewContentHash) {
await utils.sleep(2000);
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
this.whatsNewVisible = true;
}
}
@@ -128,6 +157,15 @@ class ReaderDialogs {
}
}
async showUrlHelp() {
this.urlHelpVisible = true;
}
loadBufferClick() {
this.$emit('load-buffer-toggle');
this.urlHelpVisible = false;
}
donationDialogDisable() {
this.donationVisible = false;
if (this.showDonationDialog2020) {
@@ -160,8 +198,11 @@ class ReaderDialogs {
whatsNewDisable() {
this.whatsNewVisible = false;
const whatsNew = versionHistory[0];
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
}
get whatsNewHeader() {
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
}
get mode() {
@@ -181,7 +222,7 @@ class ReaderDialogs {
}
keyHook() {
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
return true;
return false;
}

View File

@@ -7,90 +7,197 @@
</span>
</template>
<template #buttons>
<div
class="row justify-center items-center"
:class="{'header-button': !archive, 'header-button-pressed': archive}"
@mousedown.stop @click="archiveToggle"
>
<q-icon class="q-mr-xs" name="la la-archive" size="20px" />
<span style="font-size: 90%">Архив</span>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ (archive ? 'Скрыть архивные' : 'Показать архивные') }}
</q-tooltip>
</div>
</template>
<a ref="download" style="display: none;" target="_blank"></a>
<q-table
class="recent-books-table col"
:rows="tableData"
row-key="key"
:columns="columns"
:pagination="pagination"
separator="cell"
hide-bottom
virtual-scroll
dense
>
<template #header="props">
<q-tr :props="props">
<q-th key="num" class="td-mp" style="width: 25px" :props="props">
<span v-html="props.cols[0].label"></span>
</q-th>
<q-th key="date" class="td-mp break-word" style="width: 77px" :props="props">
<span v-html="props.cols[1].label"></span>
</q-th>
<q-th key="desc" class="td-mp" style="width: 332px" :props="props" colspan="4">
<q-input ref="input" v-model="search"
outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
placeholder="Найти"
@click.stop
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
<div ref="header" class="scroll-header row bg-blue-2">
<q-btn class="tool-button" round @click="showSameBookClick">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Показать/скрыть версии книг
</q-tooltip>
</q-btn>
<q-btn class="tool-button" round @click="scrollToBegin">
<q-icon name="la la-arrow-up" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В начало списка
</q-tooltip>
</q-btn>
<q-btn class="tool-button" round @click="scrollToEnd">
<q-icon name="la la-arrow-down" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В конец списка
</q-tooltip>
</q-btn>
<q-btn class="tool-button" round @click="scrollToActiveBook">
<q-icon name="la la-location-arrow" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
На текущую книгу
</q-tooltip>
</q-btn>
<q-input
ref="input"
v-model="search"
class="q-ml-sm q-mt-xs"
outlined dense
style="width: 185px"
bg-color="white"
placeholder="Найти"
@click.stop
>
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
</template>
</q-input>
<q-select
ref="sortMethod"
v-model="sortMethod"
class="q-ml-sm q-mt-xs"
:options="sortMethodOptions"
style="width: 180px"
bg-color="white"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options display-value-sanitize options-sanitize
options-html display-value-html
@update:model-value="sortMethodSelected"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Метод сортировки
</q-tooltip>
<template #selected-item="scope">
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
</template>
</q-select>
</div>
<q-virtual-scroll
ref="virtualScroll"
v-slot="{ item, index }"
:items="tableData"
scroll-target="#vs-container"
virtual-scroll-item-size="80"
@virtual-scroll="onScroll"
>
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px">
<q-icon name="la la-code-branch" size="24px" style="color: green" />
</div>
<div class="row-part column justify-center items-stretch" style="width: 80px">
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
</div>
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
{{ (item.group ? item.group.length + 1 : 0) }} верси{{ wordEnding((item.group ? item.group.length + 1 : 0), 1) }}
</div>
</div>
<div class="row-part column items-stretch clickable break-word" @click="loadBook(item)">
<div
class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
:style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
>
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
</template>
</q-input>
<span v-html="props.cols[2].label"></span>
</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props">
<q-td key="num" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 25px">
{{ props.row.num }}
</div>
</q-td>
<q-td key="date" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
<div class="break-word" style="width: 68px">
{{ props.row.touchDate }}<br>
{{ props.row.touchTime }}
</div>
</q-td>
<q-td key="desc" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
<div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">
{{ props.row.desc.author }}
<div class="text-green-10" style="font-size: 80%">
{{ item.desc.author }}
</div>
<div style="font-size: 75%">
{{ item.desc.title }}
</div>
<div>{{ props.row.desc.title }}</div>
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
</div>
</q-td>
<q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%">
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
</div>
</q-td>
<div class="row" style="font-size: 10px">
<div class="row justify-center items-center row-info-top" style="width: 60px">
{{ item.desc.textLen }}
</div>
<q-td key="close" :props="props" class="td-mp" auto-width>
<div style="width: 38px">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(props.row.key)"
>
<q-icon class="la la-times" size="14px" />
</q-btn>
<div class="row items-center row-info-top" :style="`width: ${(260 - 40*(+item.inGroup))}px; padding: 1px`">
<div class="read-bar" :style="`width: ${100*item.readPart}%`"></div>
</div>
<div class="row justify-center items-center row-info-top" style="width: 59px">
{{ item.desc.perc }}
</div>
<div class="row-info-top" style="width: 1px">
</div>
</div>
</q-td>
<q-td key="last" :props="props" class="no-mp">
</q-td>
</q-tr>
</template>
</q-table>
<div class="row" style="font-size: 10px" :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }">
<div class="row justify-center items-center row-info-bottom" style="width: 30px">
{{ item.num }}
</div>
<div class="col row">
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
Загружен: {{ item.loadTime }}
</div>
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
Читался: {{ item.touchTime }}
</div>
</div>
<div class="row-info-bottom" style="width: 1px">
</div>
</div>
</div>
<div
class="row-part column"
style="width: 90px;"
>
<div
class="col column justify-center"
style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
>
<div :style="`margin-top: ${(archive ? 20 : 0)}px`">
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
</div>
</div>
<div
class="del-button self-end row justify-center items-center clickable"
@click="handleDel(item.key)"
>
<q-icon class="la la-times" size="12px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ (archive ? 'Удалить окончательно' : 'Перенести в архив') }}
</q-tooltip>
</div>
<div
v-show="archive"
class="restore-button self-start row justify-center items-center clickable"
@click="handleRestore(item.key)"
>
<q-icon class="la la-arrow-left" size="14px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Восстановить из архива
</q-tooltip>
</div>
</div>
</div>
</q-virtual-scroll>
</div>
</Window>
</template>
@@ -99,9 +206,10 @@
import vueComponent from '../../vueComponent.js';
import path from 'path-browserify';
//import _ from 'lodash';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import LockQueue from '../../../share/LockQueue';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
@@ -111,9 +219,15 @@ const componentOptions = {
Window,
},
watch: {
search: function() {
search() {
this.updateTableData();
}
},
sortMethod() {
this.updateTableData();
},
settings() {
this.loadSettings();
},
},
};
class RecentBooksPage {
@@ -122,52 +236,19 @@ class RecentBooksPage {
loading = false;
search = '';
tableData = [];
columns = [];
pagination = {};
sortMethod = '';
showSameBook = false;
archive = false;
created() {
this.firstInit = true;
this.pagination = {rowsPerPage: 0};
this.commit = this.$store.commit;
this.columns = [
{
name: 'num',
label: '#',
align: 'center',
sortable: true,
field: 'num',
},
{
name: 'date',
label: 'Время<br>просм.',
align: 'left',
field: 'touchDateTime',
sortable: true,
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
},
{
name: 'desc',
label: 'Название',
align: 'left',
field: 'descString',
sortable: true,
},
{
name: 'links',
label: '',
align: 'left',
},
{
name: 'close',
label: '',
align: 'left',
},
{
name: 'last',
label: '',
align: 'left',
},
];
this.lastScrollTop1 = 0;
this.lastScrollTop2 = 0;
this.lock = new LockQueue(100);
this.loadSettings();
}
init() {
@@ -176,89 +257,185 @@ class RecentBooksPage {
this.$nextTick(() => {
//this.$refs.input.focus();//плохо на планшетах
});
(async() => {//подгрузка списка
if (this.initing)
return;
this.initing = true;
if (this.firstInit) {//для отзывчивости
await this.updateTableData(20);
this.firstInit = false;
}
await utils.sleep(50);
this.inited = true;
(async() => {
this.showBar();
await this.updateTableData();
this.initing = false;
await this.scrollToActiveBook();
})();
}
async updateTableData(limit) {
while (this.updating) await utils.sleep(100);
this.updating = true;
let result = [];
loadSettings() {
const settings = this.settings;
this.showSameBook = settings.recentShowSameBook;
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
}
this.loading = !!limit;
const sorted = bookManager.getSortedRecent();
get settings() {
return this.$store.state.reader.settings;
}
let num = 0;
for (let i = 0; i < sorted.length; i++) {
const book = sorted[i];
if (book.deleted)
continue;
async updateTableData() {
if (!this.inited)
return;
num++;
if (limit && result.length >= limit)
break;
await this.lock.get();
try {
let result = [];
let d = new Date();
d.setTime(book.touchTime);
const t = utils.formatDate(d).split(' ');
const sorted = bookManager.getSortedRecent();
const activeBook = bookManager.mostRecentBook();
let readPart = 0;
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
readPart = p/book.textLength;
perc = ` [${(readPart*100).toFixed(2)}%]`;
textLen = ` ${Math.round(book.textLength/1000)}k`;
//подготовка полей
for (const book of sorted) {
if ((!this.archive && book.deleted) || (this.archive && book.deleted != 1))
continue;
let d = new Date();
d.setTime(book.touchTime);
const touchTime = utils.formatDate(d);
const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
d.setTime(loadTimeRaw);
const loadTime = utils.formatDate(d);
let readPart = 0;
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
readPart = p/book.textLength;
perc = `${(readPart*100).toFixed(2)}%`;
textLen = `${Math.floor(readPart*book.textLength/1000)}/${Math.floor(book.textLength/1000)}`;
}
const bt = utils.getBookTitle(book.fb2);
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
result.push({
touchTime,
loadTime,
desc: {
author,
title,
perc,
textLen,
},
readPart,
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
sameBookKey: book.sameBookKey,
active: (activeBook.key == book.key),
activeParent: false,
inGroup: false,
//для сортировки
loadTimeRaw,
touchTimeRaw: book.touchTime,
});
}
const bt = utils.getBookTitle(book.fb2);
//нумерация
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
let num = 0;
for (let i = result.length - 1; i >= 0; i--) {
num++;
result[i].num = num;
}
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
//фильтрация
const search = this.search;
if (search) {
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.loadTime.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
}
result.push({
num,
touchDateTime: book.touchTime,
touchDate: t[0],
touchTime: t[1],
desc: {
author,
title: `${title}${perc}${textLen}`,
},
readPart,
descString: `${author}${title}${perc}${textLen}`,//для сортировки
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
});
//сортировка
switch (this.sortMethod) {
case 'loadTimeDesc':
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
break;
case 'loadTimeAsc':
result.sort((a, b) => a.loadTimeRaw - b.loadTimeRaw);
break;
case 'touchTimeDesc':
result.sort((a, b) => b.touchTimeRaw - a.touchTimeRaw);
break;
case 'touchTimeAsc':
result.sort((a, b) => a.touchTimeRaw - b.touchTimeRaw);
break;
case 'authorDesc':
result.sort((a, b) => b.desc.author.localeCompare(a.desc.author));
break;
case 'authorAsc':
result.sort((a, b) => a.desc.author.localeCompare(b.desc.author));
break;
case 'titleDesc':
result.sort((a, b) => b.desc.title.localeCompare(a.desc.title));
break;
case 'titleAsc':
result.sort((a, b) => a.desc.title.localeCompare(b.desc.title));
break;
}
//группировка
const groups = {};
const parents = {};
let newResult = [];
for (const book of result) {
if (book.sameBookKey !== undefined) {
if (!groups[book.sameBookKey]) {
groups[book.sameBookKey] = [];
parents[book.sameBookKey] = book;
book.group = groups[book.sameBookKey];
newResult.push(book);
} else {
book.inGroup = true;
if (book.active)
parents[book.sameBookKey].activeParent = true;
groups[book.sameBookKey].push(book);
}
} else {
newResult.push(book);
}
}
result = newResult;
//showSameBook
if (this.showSameBook) {
newResult = [];
for (const book of result) {
newResult.push(book);
if (book.group) {
for (const sameBook of book.group) {
newResult.push(sameBook);
}
}
}
result = newResult;
}
//другие стадии
//.....
this.tableData = result;
} finally {
this.lock.ret();
}
const search = this.search;
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.touchDate.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
this.tableData = result;
this.updating = false;
}
resetSearch() {
@@ -266,19 +443,23 @@ class RecentBooksPage {
this.$refs.input.focus();
}
wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
wordEnding(num, type = 0) {
const endings = [
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],
['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о']
];
const deci = num % 100;
if (deci > 10 && deci < 20) {
return '';
return endings[type][0];
} else {
return endings[num % 10];
return endings[type][num % 10];
}
}
get header() {
const len = (this.tableData ? this.tableData.length : 0);
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
return `${(this.search ? `Найден${this.wordEnding(len, 2)}` : 'Всего')} ${len} файл${this.wordEnding(len)}${this.archive ? ' в архиве' : ''}`;
}
async downloadBook(fb2path, fullTitle) {
@@ -304,15 +485,24 @@ class RecentBooksPage {
}
async handleDel(key) {
await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
if (!bookManager.mostRecentBook())
this.close();
if (!this.archive) {
await bookManager.delRecentBook({key});
this.$root.notify.info('Перенесено в архив');
} else {
if (await this.$root.stdDialog.confirm('Подтвердите удаление из архива:', ' ')) {
await bookManager.delRecentBook({key}, 2);
this.$root.notify.info('Удалено безвозвратно');
}
}
}
loadBook(url) {
this.$emit('load-book', {url});
async handleRestore(key) {
await bookManager.restoreRecentBook({key});
this.$root.notify.info('Восстановлено из архива');
}
loadBook(row) {
this.$emit('load-book', {url: row.url, path: row.path});
this.close();
}
@@ -323,6 +513,116 @@ class RecentBooksPage {
return false;
}
showBar() {
this.lastScrollTop1 = this.$refs.vsContainer.scrollTop;
this.$refs.header.style.position = 'sticky';
this.$refs.header.style.top = 0;
}
onScroll() {
const curScrollTop = this.$refs.vsContainer.scrollTop;
if (this.lockScroll) {
this.lastScrollTop1 = curScrollTop;
return;
}
if (curScrollTop - this.lastScrollTop1 > 100) {
this.$refs.header.style.top = `-${this.$refs.header.offsetHeight}px`;
this.$refs.header.style.transition = 'top 0.2s ease 0s';
this.lastScrollTop1 = curScrollTop;
} else if (curScrollTop - this.lastScrollTop2 < 0) {
this.$refs.header.style.position = 'sticky';
this.$refs.header.style.top = 0;
this.lastScrollTop1 = curScrollTop;
}
this.lastScrollTop2 = curScrollTop;
}
showSameBookClick() {
this.showSameBook = !this.showSameBook;
const newSettings = _.cloneDeep(this.settings);
newSettings.recentShowSameBook = this.showSameBook;
this.commit('reader/setSettings', newSettings);
this.updateTableData();
}
sortMethodSelected() {
const newSettings = _.cloneDeep(this.settings);
newSettings.recentSortMethod = this.sortMethod;
this.commit('reader/setSettings', newSettings);
}
async scrollToActiveBook() {
this.lockScroll = true;
try {
let activeIndex = -1;
let activeParentIndex = -1;
for (let i = 0; i < this.tableData.length; i++) {
const book = this.tableData[i];
if (book.active)
activeIndex = i;
if (book.activeParent)
activeParentIndex = i;
if (activeIndex >= 0 && activeParentIndex >= 0)
break;
}
const index = (activeIndex >= 0 ? activeIndex : activeParentIndex);
if (index >= 0) {
this.$refs.virtualScroll.scrollTo(index, 'center');
}
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
async scrollToBegin() {
this.lockScroll = true;
try {
this.$refs.virtualScroll.scrollTo(0, 'center');
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
async scrollToEnd() {
this.lockScroll = true;
try {
this.$refs.virtualScroll.scrollTo(this.tableData.length, 'center');
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
get sortMethodOptions() {
return [
{label: '<span style="font-size: 150%">&uarr;</span> Время загрузки', value: 'loadTimeDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Время загрузки', value: 'loadTimeAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Время чтения', value: 'touchTimeDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Время чтения', value: 'touchTimeAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Автор', value: 'authorDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Автор', value: 'authorAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Название', value: 'titleDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Название', value: 'titleAsc'},
];
}
archiveToggle() {
this.archive = !this.archive;
this.updateTableData();
}
close() {
this.$emit('recent-books-close');
}
@@ -340,55 +640,139 @@ export default vueComponent(RecentBooksPage);
</script>
<style scoped>
.recent-books-table {
width: 600px;
.recent-books-scroll {
width: 573px;
overflow-y: auto;
overflow-x: hidden;
}
.scroll-header {
height: 50px;
position: sticky;
z-index: 1;
top: 0;
border-bottom: 2px solid #aaaaaa;
padding-left: 5px;
}
.table-row {
min-height: 80px;
}
.row-part {
padding: 4px 0px 4px 0px;
}
.clickable {
cursor: pointer;
}
.td-mp {
margin: 0 !important;
padding: 4px 4px 4px 4px !important;
border-bottom: 1px solid #ddd;
}
.no-mp {
margin: 0 !important;
padding: 0 !important;
border: 0;
border-left: 1px solid #ddd !important;
}
.break-word {
line-height: 180%;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
}
.even {
background-color: #f2f2f2;
}
.active-book {
background-color: #b0f0b0 !important;
}
.active-parent-book {
background-color: #ffbbbb !important;
}
.icon {
transition: transform 0.2s;
}
.expanded-icon {
transform: rotate(90deg);
}
.tool-button {
min-width: 30px;
width: 30px;
min-height: 30px;
height: 30px;
margin: 10px 6px 0px 3px;
background-color: white;
}
.row-info-bottom {
line-height: 110%;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
height: 12px;
}
.row-info-top {
line-height: 110%;
border: 1px solid #cccccc;
border-right: 0;
height: 12px;
}
.time-info, .row-info-top {
color: #888888;
}
.read-bar {
height: 3px;
background-color: #aaaaaa;
}
</style>
<style>
.recent-books-table .q-table__middle {
height: 100%;
overflow-x: hidden;
height: 4px;
background-color: #bbbbbb;
}
.recent-books-table thead tr:first-child th {
position: sticky;
z-index: 1;
top: 0;
background-color: #c1f4cd;
.del-button {
width: 25px;
height: 20px;
position: absolute;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
border-radius: 0 0 0 10px;
margin: 1px;
}
.recent-books-table tr:nth-child(even) {
background-color: #f8f8f8;
.del-button:hover {
color: white;
background-color: #FF3030;
}
.restore-button {
width: 25px;
height: 20px;
position: absolute;
border-right: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
border-radius: 0 0 10px 0;
margin: 1px;
}
.restore-button:hover {
color: white;
background-color: #00bb00;
}
.header-button, .header-button-pressed {
width: 80px;
height: 30px;
cursor: pointer;
color: #555555;
}
.header-button:hover {
color: white;
background-color: #39902F;
}
.header-button-pressed {
color: black;
background-color: yellow;
}
.header-button-pressed:hover {
color: black;
}
</style>

View File

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

View File

@@ -576,7 +576,7 @@ class ServerStorage {
newRecentPatch.rev++;
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
let applyMod = this.cachedRecentMod.data;
const applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
@@ -627,7 +627,7 @@ class ServerStorage {
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) {
this.savingRecent = false;
this.saveRecent(itemKey, true);
await this.saveRecent(itemKey, true);
return;
}
} else if (result.state == 'success') {
@@ -728,10 +728,10 @@ class ServerStorage {
const ids = id.split('.');
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
throw new Error(`decodeStorageItems: bad id - ${id}`);
items[utils.fromBase58(ids[1])] = decoded;
items[utils.fromBase58(ids[1]).toString()] = decoded;
}
}
result.items = items;
return result;
}

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

@@ -52,7 +52,7 @@
</q-checkbox>
</div>
<div class="item row">
<!--div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showDonationDialog2020">
Показывать "Оплатим хостинг вместе"
@@ -60,7 +60,7 @@
Показывать уведомление "Оплатим хостинг вместе"
</q-tooltip>
</q-checkbox>
</div>
</div-->
<!---------------------------------------------->
<div class="part-header">Другое</div>

View File

@@ -1,5 +1,5 @@
<template>
<Window ref="window" height="95%" width="600px" @close="close">
<Window ref="window" width="600px" @close="close">
<template #header>
Настройки
</template>
@@ -24,7 +24,7 @@
<div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
@@ -82,8 +82,8 @@
</div>
</div>
<!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
@@include('./ButtonsTab.inc');
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ToolBarTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column">
@@ -702,11 +702,11 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px;
}
.label-1, .label-7 {
.label-1, .label-3, .label-7 {
width: 75px;
}
.label-2, .label-3, .label-4, .label-5 {
.label-2, .label-4, .label-5 {
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"
v-model="search"
class="q-ml-sm col"
outlined dense rounded
outlined dense
bg-color="grey-4"
placeholder="Найти"
@click.stop

View File

@@ -6,29 +6,32 @@
</div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<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 ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<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 v-show="showStatusBar" ref="statusBar" class="layout">
<div v-html="statusBar"></div>
</div>
<div v-show="clickControl" ref="layoutEvents" class="layout events"
<div
v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
>
<div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
<div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div>
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
<div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
@@ -46,6 +49,7 @@ import vueComponent from '../../vueComponent.js';
import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import he from 'he';
import './TextPage.css';
@@ -62,7 +66,14 @@ const componentOptions = {
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
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() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -95,6 +106,8 @@ class TextPage {
lastBook = null;
bookPos = 0;
bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null;
fontSize = null;
@@ -151,7 +164,7 @@ class TextPage {
this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize);
await utils.sleep(500);
await utils.sleep(200);
this.$nextTick(this.onResize);
});
}
@@ -495,12 +508,25 @@ class TextPage {
}
async onResize() {
if (this.resizing)
return;
this.resizing = true;
try {
const scrolled = this.doingScrolling;
if (scrolled)
await this.stopTextScrolling();
this.calcDrawProps();
this.setBackground();
this.draw();
if (scrolled)
this.startTextScrolling();
} catch (e) {
//
} finally {
this.resizing = false;
}
}
@@ -648,7 +674,7 @@ class TextPage {
}
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
@@ -671,7 +697,7 @@ class TextPage {
this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
}
@@ -907,12 +933,14 @@ class TextPage {
doDown() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesDown[1].begin;
}
}
doUp() {
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesUp[1].begin;
}
}
@@ -925,6 +953,7 @@ class TextPage {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = this.linesDown[i].begin;
} else
this.doEnd();
@@ -940,6 +969,7 @@ class TextPage {
if (i >= 0 && this.linesUp.length > i) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = this.linesUp[i].begin;
}
}
@@ -948,10 +978,11 @@ class TextPage {
doHome() {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = 0;
}
doEnd(noAni) {
doEnd(noAni, isUser = true) {
if (this.parsed.para.length && this.pageLineCount > 0) {
let i = this.parsed.para.length - 1;
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
@@ -962,6 +993,7 @@ class TextPage {
if (!noAni)
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = isUser;
this.bookPos = lines[i].begin;
}
}
@@ -1201,8 +1233,54 @@ class TextPage {
}
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);

View File

@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
const maxParaLength = 10000;
const maxParaTextLength = 10000;
// defaults
const defaultSettings = {
@@ -226,13 +228,26 @@ export default class BookParser {
paraOffset += len;
};
const growParagraph = (text, len) => {
const growParagraph = (text, len, textRaw) => {
//начальный параграф
if (paraIndex < 0) {
newParagraph();
growParagraph(text, len);
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) {
curSubtitle.title += text;
} else if (inTitle) {
@@ -240,6 +255,14 @@ export default class BookParser {
}
const p = para[paraIndex];
//ограничение на размер параграфа
if (p.length > maxParaLength) {
newParagraph();
growParagraph(text, len);
return;
}
p.length += len;
p.text += text;
paraOffset += len;
@@ -536,7 +559,7 @@ export default class BookParser {
tClose += (center ? '</center>' : '');
if (text != ' ')
growParagraph(`${tOpen}${text}${tClose}`, text.length);
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
}

View File

@@ -1,10 +1,12 @@
import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
@@ -17,9 +19,6 @@ const bmDataStore = localForage.createInstance({
});
//список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
@@ -39,7 +38,7 @@ class BookManager {
this.saveRecentItem = _.debounce(() => {
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);
}, 200, {maxWait: 300});
@@ -54,6 +53,9 @@ class BookManager {
if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem;
//конвертируем в новые ключи
await this.convertRecent();
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
@@ -63,48 +65,6 @@ class BookManager {
}
await this.cleanRecentBooks();
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
{
await this.convertFileToDiskPrefix();
if (this.recentRev > 10)
await bmRecentStoreOld.clear();
}
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
let key = null;
const len = await bmRecentStoreOld.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStoreOld.key(i);
if (key) {
let r = await bmRecentStoreOld.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStoreOld.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await bmRecentStoreNew.setItem('recent', this.recent);
this.recentRev = 1;
await bmRecentStoreNew.setItem('rev', this.recentRev);
}
this.recentChanged = true;
@@ -112,6 +72,40 @@ class BookManager {
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
async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
@@ -238,8 +232,8 @@ class BookManager {
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now();
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => {
const p = Math.round(30*perc/100);
@@ -274,10 +268,10 @@ class BookManager {
async hasBookParsed(meta) {
if (!this.books)
return false;
if (!meta.url)
if (!meta.path)
return false;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
let book = this.books[meta.key];
@@ -292,8 +286,12 @@ class BookManager {
async getBook(meta, callback) {
let result = undefined;
if (!meta.path)
return;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
result = this.books[meta.key];
@@ -303,11 +301,6 @@ class BookManager {
this.books[meta.key] = result;
}
//Если файл на сервере изменился, считаем, что в кеше его нету
if (meta.path && result && meta.path != result.path) {
return;
}
if (result && !result.parsed) {
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5);
@@ -367,14 +360,24 @@ class BookManager {
return result;
}
keyFromUrl(url) {
/*keyFromUrl(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 --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev');
if (rev != this.recentRev && !skipCheck) {
if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
const newRecent = await bmRecentStoreNew.getItem('recent');
Object.assign(this.recent, newRecent);
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
@@ -411,7 +414,10 @@ class BookManager {
async setRecentBook(value) {
let result = this.metaOnly(value);
result.touchTime = Date.now();
result.touchTime = Date.now();//время последнего чтения
if (!result.loadTime)
result.loadTime = Date.now();//время загрузки файла
result.deleted = 0;
if (this.recent[result.key]) {
@@ -427,9 +433,9 @@ class BookManager {
return this.recent[value.key];
}
async delRecentBook(value) {
async delRecentBook(value, delFlag = 1) {
const item = this.recent[value.key];
item.deleted = 1;
item.deleted = delFlag;
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
@@ -439,11 +445,18 @@ class BookManager {
this.emit('recent-deleted', value.key);
}
async restoreRecentBook(value) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
async cleanRecentBooks() {
const sorted = this.getSortedRecent();
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];
isDel = true;
}
@@ -455,33 +468,6 @@ class BookManager {
return isDel;
}
async convertFileToDiskPrefix() {
let isConverted = false;
const newRecent = {};
for (let key of Object.keys(this.recent)) {
let newKey = key;
let newUrl = this.recent[key].url;
if (newKey.indexOf('66696c65') == 0) {
newKey = newKey.replace(/^66696c65/, '6469736b');
if (newUrl)
newUrl = newUrl.replace(/^file/, 'disk');
isConverted = true;
}
newRecent[newKey] = this.recent[key];
newRecent[newKey].key = newKey;
if (newUrl)
newRecent[newKey].url = newUrl;
}
if (isConverted) {
this.recent = newRecent;
await this.recentSetItem(null, true);
}
return isConverted;
}
mostRecentBook() {
if (this.recentLastKey) {
return this.recent[this.recentLastKey];
@@ -490,7 +476,7 @@ class BookManager {
let max = 0;
let result = null;
for (let key in this.recent) {
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.touchTime > max) {
max = book.touchTime;
@@ -521,6 +507,43 @@ class BookManager {
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) {
const mergedRecent = _.cloneDeep(this.recent);

View File

@@ -1,62 +1,123 @@
export const versionHistory = [
{
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:
`
<ul>
<li>небольшие дополнения интерфейса</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.1',
releaseDate: '2021-12-03',
showUntil: '2021-12-02',
header: '0.11.1 (2021-12-03)',
content:
`
<ul>
<li>переход на JembaDb вместо SQLite</li>
</ul>
`
},
{
version: '0.11.0',
releaseDate: '2021-11-18',
showUntil: '2021-11-17',
header: '0.11.0 (2021-11-18)',
content:
`
<ul>
<li>переход на Vue 3</li>
</ul>
`
},
{
version: '0.10.3',
releaseDate: '2021-10-24',
showUntil: '2021-10-23',
header: '0.10.3 (2021-10-24)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.2',
releaseDate: '2021-10-19',
showUntil: '2021-10-18',
header: '0.10.2 (2021-10-19)',
content:
`
<ul>
<li>актуализация версий пакетов и стека используемых технологий</li>
</ul>
`
},
{
version: '0.10.1',
releaseDate: '2021-10-10',
showUntil: '2021-10-09',
header: '0.10.1 (2021-10-10)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.0',
releaseDate: '2021-02-09',
showUntil: '2021-02-16',
header: '0.10.0 (2021-02-09)',
content:
`
<ul>
@@ -65,12 +126,14 @@ export const versionHistory = [
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
<li>немного улучшен парсинг fb2</li>
</ul>
`
},
{
version: '0.9.12',
releaseDate: '2020-12-18',
showUntil: '2020-12-17',
header: '0.9.12 (2020-12-18)',
content:
`
<ul>
@@ -79,23 +142,27 @@ export const versionHistory = [
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
<li>улучшения работы конвертеров</li>
</ul>
`
},
{
version: '0.9.11',
releaseDate: '2020-12-09',
showUntil: '2020-12-08',
header: '0.9.11 (2020-12-09)',
content:
`
<ul>
<li>оптимизации, улучшения работы конвертеров</li>
</ul>
`
},
{
version: '0.9.10',
releaseDate: '2020-12-03',
showUntil: '2020-12-10',
header: '0.9.10 (2020-12-03)',
content:
`
<ul>
@@ -103,69 +170,81 @@ export const versionHistory = [
<li>добавлена поддержка Rar-архивов</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.9',
releaseDate: '2020-11-21',
showUntil: '2020-11-20',
header: '0.9.9 (2020-11-21)',
content:
`
<ul>
<li>оптимизации, исправления багов</li>
</ul>
`
},
{
version: '0.9.8',
releaseDate: '2020-11-13',
showUntil: '2020-11-12',
header: '0.9.8 (2020-11-13)',
content:
`
<ul>
<li>добавлено окно "Оглавление/закладки"</li>
</ul>
`
},
{
version: '0.9.7',
releaseDate: '2020-11-12',
showUntil: '2020-11-11',
header: '0.9.7 (2020-11-12)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.6',
releaseDate: '2020-11-06',
showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content:
`
<ul>
<li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.5',
releaseDate: '2020-11-01',
showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)',
content:
`
<ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.4',
releaseDate: '2020-10-29',
showUntil: '2020-10-28',
header: '0.9.4 (2020-10-29)',
content:
`
<ul>
@@ -173,23 +252,27 @@ export const versionHistory = [
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.3',
releaseDate: '2020-05-21',
showUntil: '2020-05-20',
header: '0.9.3 (2020-05-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.2',
releaseDate: '2020-03-15',
showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content:
`
<ul>
@@ -197,119 +280,139 @@ export const versionHistory = [
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.1',
releaseDate: '2020-03-03',
showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content:
`
<ul>
<li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
version: '0.9.0',
releaseDate: '2020-02-26',
showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',
content:
`
<ul>
<li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
version: '0.8.4',
releaseDate: '2020-02-06',
showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content:
`
<ul>
<li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.8.3',
releaseDate: '2020-01-28',
showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content:
`
<ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li>
</ul>
`
},
{
version: '0.8.2',
releaseDate: '2020-01-20',
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{
version: '0.8.1',
releaseDate: '2020-01-07',
showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content:
`
<ul>
<li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li>
</ul>
`
},
{
version: '0.8.0',
releaseDate: '2020-01-02',
showUntil: '2020-01-05',
header: '0.8.0 (2020-01-02)',
content:
`
<ul>
<li>окончательный переход на https</li>
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
</ul>
`
},
{
version: '0.7.9',
releaseDate: '2019-11-27',
showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content:
`
<ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.8',
releaseDate: '2019-11-25',
showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)',
content:
`
<ul>
<li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.7',
releaseDate: '2019-11-06',
showUntil: '2019-11-10',
header: '0.7.7 (2019-11-06)',
content:
`
<ul>
@@ -321,34 +424,40 @@ export const versionHistory = [
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
</ul>
</ul>
`
},
{
version: '0.7.6',
releaseDate: '2019-10-30',
showUntil: '2019-10-29',
header: '0.7.6 (2019-10-30)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.5',
releaseDate: '2019-10-22',
showUntil: '2019-10-21',
header: '0.7.5 (2019-10-22)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.3',
releaseDate: '2019-10-18',
showUntil: '2019-10-17',
header: '0.7.3 (2019-10-18)',
content:
`
<ul>
@@ -357,12 +466,14 @@ export const versionHistory = [
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.1',
releaseDate: '2019-09-20',
showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content:
`
<ul>
@@ -370,12 +481,14 @@ export const versionHistory = [
<li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li>
</ul>
`
},
{
version: '0.7.0',
releaseDate: '2019-09-07',
showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content:
`
<ul>
@@ -386,23 +499,27 @@ export const versionHistory = [
<li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul>
`
},
{
version: '0.6.10',
releaseDate: '2019-07-21',
showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.6.9',
releaseDate: '2019-06-23',
showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content:
`
<ul>
@@ -413,12 +530,14 @@ export const versionHistory = [
<li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li>
</ul>
`
},
{
version: '0.6.7',
releaseDate: '2019-05-30',
showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content:
`
<ul>
@@ -431,36 +550,42 @@ export const versionHistory = [
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li>
</ul>
`
},
{
version: '0.6.6',
releaseDate: '2019-03-29',
showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content:
`
<ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul>
`
},
{
version: '0.6.4',
releaseDate: '2019-03-24',
showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content:
`
<ul>
<li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li>
</ul>
`
},
{
version: '0.5.4',
releaseDate: '2019-03-04',
showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content:
`
<ul>
@@ -469,12 +594,14 @@ export const versionHistory = [
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul>
`
},
{
version: '0.3.0',
releaseDate: '2019-02-17',
showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content:
`
<ul>
@@ -482,12 +609,14 @@ export const versionHistory = [
<li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul>
`
},
{
version: '0.1.7',
releaseDate: '2019-02-14',
showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content:
`
<ul>
@@ -497,17 +626,20 @@ export const versionHistory = [
<li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul>
`
},
{
version: '0.1.0',
releaseDate: '2019-02-12',
showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content:
`
<ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul>
`
},

View File

@@ -55,6 +55,34 @@
</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 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) {
return new Promise((resolve) => {
this.enableValidator = false;

View File

@@ -153,7 +153,7 @@ export default vueComponent(Window);
}
.header {
background: linear-gradient(to bottom right, green, #59B04F);
background: linear-gradient(to bottom right, #007000, #59B04F);
align-items: center;
height: 30px;
}
@@ -161,8 +161,8 @@ export default vueComponent(Window);
.header-text {
margin-left: 10px;
margin-right: 10px;
color: yellow;
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
color: #FFFFA0;
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
overflow: hidden;
white-space: nowrap;
}
@@ -174,7 +174,8 @@ export default vueComponent(Window);
}
.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 {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = {
@@ -62,6 +64,7 @@ const components = {
QChip,
QTree,
//QExpansionItem,
QVirtualScroll,
};
//directives
@@ -86,7 +89,6 @@ const plugins = {
import '@quasar/extras/line-awesome/line-awesome.css';
import lineAwesome from 'quasar/icon-set/line-awesome.js'
//const q: {Quasar, QuasarOptions: { config, components, directives, plugins }};
export default {
quasar: Quasar,
options: { config, components, directives, plugins },

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) {
return bs58.decode(data);
return Buffer.from(bs58.decode(data));
}
//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 = {}) {
const {
exclude = [],
@@ -126,7 +130,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
for (const key of Object.keys(oldObj)) {
const kp = `${keyPath}${key}`;
if (newObj.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
if (ex.has(kp))
continue;
@@ -149,7 +153,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
if (exAdd.has(kp))
continue;
if (!oldObj.hasOwnProperty(key)) {
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
result.add[key] = _.cloneDeep(newObj[key]);
}
}
@@ -213,7 +217,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
const change = diff.change;
for (const key of Object.keys(change)) {
if (result.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
if (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], opts);
} else {

View File

@@ -2,8 +2,10 @@ import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json';
const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки',
'loadFile': 'Загрузить файл с диска',
'loadBuffer': 'Загрузить из буфера обмена',
'help': 'Вызвать cправку',
'settings': 'Настроить',
'undoAction': 'Действие назад',
'redoAction': 'Действие вперед',
@@ -15,10 +17,11 @@ const readerActions = {
'copyText': 'Скопировать текст со страницы',
'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу',
'clickControl': 'Управление кликом',
'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние',
'recentBooks': 'Показать загруженные',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
'bookBegin': 'В начало книги',
@@ -35,6 +38,9 @@ const readerActions = {
//readerActions[name]
const toolButtons = [
{name: 'loadFile', show: true},
{name: 'loadBuffer', show: true},
{name: 'help', show: true},
{name: 'undoAction', show: true},
{name: 'redoAction', show: true},
{name: 'fullScreen', show: true},
@@ -47,13 +53,16 @@ const toolButtons = [
{name: 'contents', show: true},
{name: 'libs', show: true},
{name: 'recentBooks', show: true},
{name: 'clickControl', show: false},
{name: 'offlineMode', show: false},
];
//readerActions[name]
const hotKeys = [
{name: 'help', codes: ['F1', 'H']},
{name: 'loader', codes: ['Escape']},
{name: 'loadFile', codes: ['F3']},
{name: 'loadBuffer', codes: ['F4']},
{name: 'help', codes: ['F1', 'H']},
{name: 'settings', codes: ['S']},
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
@@ -61,12 +70,13 @@ const hotKeys = [
{name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'copyText', codes: ['Ctrl+Space']},
{name: 'convOptions', codes: ['Ctrl+M']},
{name: 'refresh', codes: ['R']},
{name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']},
{name: 'clickControl', codes: ['Ctrl+B']},
{name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']},
@@ -175,8 +185,12 @@ const settingDefaults = {
fontShifts: {},
showToolButton: {},
toolBarHideOnScroll: true,
userHotKeys: {},
userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
};
for (const font of fonts)
@@ -212,9 +226,6 @@ const libsDefaults = {
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{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: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},

View File

@@ -18,7 +18,7 @@ server {
server_name liberama.top;
client_max_body_size 50m;
client_max_body_size 100m;
proxy_read_timeout 1h;
gzip on;
@@ -35,6 +35,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -62,7 +63,7 @@ server {
listen 80;
server_name b.liberama.top;
client_max_body_size 50m;
client_max_body_size 100m;
proxy_read_timeout 1h;
gzip on;
@@ -139,5 +140,6 @@ server {
location / {
proxy_pass http://fantasy-worlds.org;
proxy_hide_header x-frame-options;
}
}

View File

@@ -1,7 +1,7 @@
#!/bin/bash
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
echo "Process 'liberama' already running"
fi

View File

@@ -7,7 +7,7 @@ server {
server_name omnireader.ru;
client_max_body_size 50m;
client_max_body_size 100m;
proxy_read_timeout 1h;
gzip on;
@@ -24,6 +24,7 @@ server {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -51,7 +52,7 @@ server {
listen 80;
server_name old.omnireader.ru;
client_max_body_size 50m;
client_max_body_size 100m;
gzip on;
gzip_min_length 1024;

View File

@@ -1,4 +1,4 @@
#!/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

12125
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.11.1",
"version": "0.11.7",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -10,8 +10,8 @@
"scripts": {
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
"build:client": "webpack --config build/webpack.prod.config.js",
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -o dist/win/liberama .",
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -C GZip -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -C GZip -o dist/win/liberama .",
"lint": "eslint --ext=.js,.vue client server",
"build:client-dev": "webpack --config build/webpack.dev.config.js",
"postinstall": "npm run build:client-dev && node build/linux"
@@ -28,16 +28,17 @@
"@babel/preset-env": "^7.16.0",
"@vue/compiler-sfc": "^3.2.22",
"babel-loader": "^8.2.3",
"copy-webpack-plugin": "^9.1.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^3.1.3",
"eslint": "^8.2.0",
"eslint-plugin-vue": "^8.0.3",
"css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.19.0",
"eslint-plugin-vue": "^9.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.4",
"pkg": "^5.5.1",
"terser-webpack-plugin": "^5.2.5",
"vue-eslint-parser": "^8.0.1",
"vue-loader": "^16.8.3",
"vue-eslint-parser": "^9.0.3",
"vue-loader": "^17.0.0",
"vue-style-loader": "^4.1.3",
"webpack": "^5.64.1",
"webpack-cli": "^4.9.1",
@@ -49,25 +50,24 @@
"dependencies": {
"@quasar/extras": "^1.12.0",
"@vue/compat": "^3.2.21",
"axios": "^0.24.0",
"base-x": "^3.0.9",
"axios": "^0.27.2",
"base-x": "^4.0.0",
"chardet": "^1.4.0",
"compression": "^1.7.4",
"express": "^4.17.1",
"fg-loadcss": "^3.1.0",
"fs-extra": "^9.0.1",
"got": "^11.8.2",
"fs-extra": "^10.1.0",
"he": "^1.2.0",
"iconv-lite": "^0.6.3",
"jembadb": "^3.0.8",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"minimist": "^1.2.5",
"multer": "^1.4.3",
"multer": "^1.4.5-lts.1",
"pako": "^2.0.4",
"path-browserify": "^1.0.1",
"pidusage": "^3.0.0",
"pkg": "^4.4.9",
"quasar": "^2.3.2",
"quasar": "^2.7.5",
"safe-buffer": "^5.2.1",
"sanitize-html": "^2.5.3",
"sjcl": "^1.0.8",
@@ -76,8 +76,8 @@
"sqlite3": "^5.0.2",
"tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3",
"vue": "^3.2.22",
"vue-router": "^4.0.12",
"vue": "^3.2.37",
"vue-router": "^4.1.1",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0",
"webdav": "^4.7.0",

View File

@@ -22,7 +22,8 @@ module.exports = {
maxUploadPublicDirSize: 200*1024*1024,//100Мб
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: [
{
@@ -48,7 +49,7 @@ module.exports = {
servers: [
{
serverName: '1',
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader'
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top'
ip: '0.0.0.0',
port: '33080',
},

View File

@@ -55,8 +55,7 @@ class WebSocketController {
ws.lastActivity = Date.now();
//pong for WebSocketConnection
if (req._rpo === 1)
this.send({_rok: 1}, req, ws);
this.send({_rok: 1}, req, ws);
switch (req.action) {
case 'test':

View File

@@ -25,60 +25,7 @@ class WorkerController extends BaseController {
res.status(400).send({error});
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;

View File

@@ -5,30 +5,26 @@ const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtExcepti
//singleton
class AsyncExit {
constructor() {
constructor(signals = exitSignals, codeOnSignal = 2) {
if (!instance) {
this.onSignalCallbacks = new Map();
this.callbacks = new Map();
this.afterCallbacks = new Map();
this.exitTimeout = defaultTimeout;
this.inited = false;
this._init(signals, codeOnSignal);
instance = this;
}
return instance;
}
init(signals = null, codeOnSignal = 2) {
if (this.inited)
throw new Error('AsyncExit: initialized already');
if (!signals)
signals = exitSignals;
const runSingalCallbacks = async(signal) => {
_init(signals, codeOnSignal) {
const runSingalCallbacks = async(signal, err, origin) => {
for (const signalCallback of this.onSignalCallbacks.keys()) {
try {
await signalCallback(signal);
await signalCallback(signal, err, origin);
} catch(e) {
console.error(e);
}
@@ -36,13 +32,11 @@ class AsyncExit {
};
for (const signal of signals) {
process.once(signal, async() => {
await runSingalCallbacks(signal);
process.once(signal, async(err, origin) => {
await runSingalCallbacks(signal, err, origin);
this.exit(codeOnSignal);
});
}
this.inited = true;
}
onSignal(signalCallback) {

View File

@@ -1,4 +1,4 @@
const got = require('got');
const axios = require('axios');
class FileDownloader {
constructor(limitDownloadSize = 0) {
@@ -7,54 +7,82 @@ class FileDownloader {
async load(url, callback, abort) {
let errMes = '';
const options = {
headers: {
'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'
},
responseType: 'buffer',
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 {
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 (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) {
errMes = (errMes ? errMes : error.message);
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 = [];
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;
}
@@ -188,8 +188,8 @@ class Logger {
}
this.closed = false;
ayncExit.onSignal((signal) => {
this.log(LM_FATAL, `Signal ${signal} received, exiting...`);
ayncExit.onSignal((signal, err) => {
this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", exiting...`);
});
ayncExit.addAfter(this.close.bind(this));
}
@@ -218,6 +218,8 @@ class Logger {
} else {
console.log(mes);
}
return mes;
}
async close() {

View File

@@ -3,7 +3,7 @@ const chardet = require('chardet');
function getEncoding(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));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {

View File

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

View File

@@ -9,8 +9,7 @@ const cleanPeriod = 5*1000;//5 секунд
class WebSocketConnection {
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
//const ws = 'ws';//for nodejs
this.WebSocket = (isBrowser ? WebSocket : null/*for nodejs require(ws)*/);
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
this.url = url;
this.ws = null;
this.listeners = [];
@@ -166,7 +165,7 @@ class WebSocketConnection {
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
const requestId = this.requestId;//реентерабельность!!!
this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
let resp = {};
try {

View File

@@ -13,7 +13,7 @@ function toBase36(data) {
}
function fromBase36(data) {
return bs36.decode(data);
return Buffer.from(bs36.decode(data));
}
function bufferRemoveZeroes(buf) {

View File

@@ -2,7 +2,7 @@ const fs = require('fs-extra');
const _ = require('lodash');
const ayncExit = new (require('../core/AsyncExit'))();//singleton
const { JembaDb, JembaDbThread } = require('./JembaDb');
const { JembaDb, JembaDbThread } = require('jembadb');
const log = new (require('../core/AppLogger'))().log;//singleton
const jembaMigrations = require('./jembaMigrations');
@@ -14,6 +14,7 @@ class JembaConnManager {
constructor() {
if (!instance) {
this.inited = false;
this._db = {};
instance = this;
}
@@ -28,6 +29,8 @@ class JembaConnManager {
this.config = config;
this._db = {};
ayncExit.add(this.close.bind(this));
for (const dbConfig of this.config.jembaDb) {
const dbPath = `${this.config.dataDir}/db/${dbConfig.dbName}`;
@@ -44,11 +47,23 @@ class JembaConnManager {
} else {
dbConn = new JembaDb();
}
this._db[dbConfig.dbName] = dbConn;
log(`Open "${dbConfig.dbName}" begin`);
await dbConn.openDb({dbPath, cacheSize: dbConfig.cacheSize, compressed: dbConfig.compressed, forceFileClosing: dbConfig.forceFileClosing});
await dbConn.lock({
dbPath,
create: true,
softLock: true,
if (dbConfig.openAll) {
tableDefaults: {
cacheSize: dbConfig.cacheSize,
compressed: dbConfig.compressed,
forceFileClosing: dbConfig.forceFileClosing,
typeCompatMode: true,
},
});
if (dbConfig.openAll || forceAutoRepair || dbConfig.autoRepair) {
try {
await dbConn.openAll();
} catch(e) {
@@ -77,21 +92,15 @@ class JembaConnManager {
if (applied.length)
log(`${applied.length} migrations applied to "${dbConfig.dbName}"`);
}
this._db[dbConfig.dbName] = dbConn;
}
ayncExit.add(this.close.bind(this));
this.inited = true;
}
async close() {
if (!this.inited)
return;
for (const dbConfig of this.config.jembaDb) {
await this._db[dbConfig.dbName].closeDb();
if (this._db[dbConfig.dbName])
await this._db[dbConfig.dbName].unlock();
}
this._db = {};

View File

@@ -1,536 +0,0 @@
const fs = require('fs').promises;
const Table = require('./Table');
const utils = require('./utils');
/* API methods:
openDb
closeDb
create
drop
open
openAll
close
closeAll
tableExists
getDbInfo
getDbSize
select
insert
update
delete
esc
*/
class JembaDb {
constructor() {
this.opened = false;
}
/*
query = {
dbPath: String,
//table open defaults
inMemory: Boolean, false
cacheSize: Number, 5
compressed: Number, {0..9}, 0
recreate: Boolean, false,
autoRepair: Boolean, false,
forceFileClosing: Boolean, false,
lazyOpen: Boolean, false,
}
*/
async openDb(query = {}) {
if (this.opened)
throw new Error(`Database ${this.dbPath} has already been opened`);
if (!query.dbPath)
throw new Error(`'query.dbPath' parameter is required`);
this.dbPath = query.dbPath;
await fs.mkdir(this.dbPath, { recursive: true });
this.table = new Map();
this.tableOpenDefaults = {
inMemory: query.inMemory,
cacheSize: query.cacheSize,
compressed: query.compressed,
recreate: query.recreate,
autoRepair: query.autoRepair,
forceFileClosing: query.forceFileClosing,
lazyOpen: query.lazyOpen,
};
this.opened = true;
}
async closeDb() {
if (!this.opened)
return;
await this.closeAll();
this.opened = false;
//console.log('closed');
}
checkOpened() {
if (!this.opened)
throw new Error('Database closed');
}
/*
query = {
table: 'tableName',
quietIfExists: Boolean,
inMemory: Boolean, false
cacheSize: Number, 5
compressed: Number, {0..9}, 0
recreate: Boolean, false,
autoRepair: Boolean, false,
forceFileClosing: Boolean, false,
lazyOpen: Boolean, false,
in: 'tableName',
flag: Object || Array, {name: 'flag1', check: '(r) => r.id > 10'}
hash: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
index: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
}
result = {}
*/
async create(query = {}) {
this.checkOpened();
if ((!query.table && !query.in) || (query.table && query.in))
throw new Error(`One of 'query.table' or 'query.in' parameters is required, but not both`);
let table;
if (query.table) {
if (await this.tableExists({table: query.table})) {
if (!query.quietIfExists)
throw new Error(`Table '${query.table}' already exists`);
table = this.table.get(query.table);
} else {
table = new Table();
this.table.set(query.table, table);
await this.open(query);
}
} else {
if (await this.tableExists({table: query.in})) {
table = this.table.get(query.in);
} else {
throw new Error(`Table '${query.in}' does not exist`);
}
}
if (query.flag || query.hash || query.index) {
await table.create({
quietIfExists: query.quietIfExists,
flag: query.flag,
hash: query.hash,
index: query.index,
});
}
return {};
}
/*
query = {
table: 'tableName',
in: 'tableName',
flag: Object || Array, {name: 'flag1'}
hash: Object || Array, {field: 'field1'}
index: Object || Array, {field: 'field1'}
}
result = {}
*/
async drop(query = {}) {
this.checkOpened();
if ((!query.table && !query.in) || (query.table && query.in))
throw new Error(`One of 'query.table' or 'query.in' parameters is required, but not both`);
if (query.table) {
if (await this.tableExists({table: query.table})) {
const table = this.table.get(query.table);
if (table && table.opened) {
await table.close();
}
const basePath = `${this.dbPath}/${query.table}`;
await fs.rmdir(basePath, { recursive: true });
this.table.delete(query.table);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
} else {
if (await this.tableExists({table: query.in})) {
const table = this.table.get(query.in);
if (table) {
if (query.flag || query.hash || query.index) {
await table.drop({
flag: query.flag,
hash: query.hash,
index: query.index,
});
}
} else {
throw new Error(`Table '${query.in}' has not been opened yet`);
}
} else {
throw new Error(`Table '${query.in}' does not exist`);
}
}
return {};
}
/*
query = {
(!) table: 'tableName',
inMemory: Boolean, false
cacheSize: Number, 5
compressed: Number, {0..9}, 0
recreate: Boolean, false,
autoRepair: Boolean, false,
forceFileClosing: Boolean, false,
lazyOpen: Boolean, false,
}
*/
async open(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
if (await this.tableExists({table: query.table})) {
let table = this.table.get(query.table);
if (!table) {
table = new Table();
}
if (!table.opened) {
const opts = Object.assign({}, this.tableOpenDefaults, query);
opts.tablePath = `${this.dbPath}/${query.table}`;
await table.open(opts);
}
this.table.set(query.table, table);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
}
async _getTableList() {
const result = [];
const files = await fs.readdir(this.dbPath, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
if (file.name.indexOf('___temporary_recreating') >= 0)
continue;
result.push(file.name);
}
}
return result;
}
/*
query = {
inMemory: Boolean, false
cacheSize: Number, 5
compressed: Number, {0..9}, 0
recreate: Boolean, false,
autoRepair: Boolean, false,
forceFileClosing: Boolean, false,
lazyOpen: Boolean, false,
}
*/
async openAll(query = {}) {
this.checkOpened();
const tables = await this._getTableList();
//sequentially
for (const table of tables) {
this.checkOpened();
await this.open(Object.assign({}, query, {table}));
}
/*const promises = [];
for (const table of tables) {
promises.push(this.open(Object.assign({}, query, {table})));
}
await Promise.all(promises);*/
}
/*
query = {
(!) table: 'tableName',
}
*/
async close(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
if (await this.tableExists({table: query.table})) {
let table = this.table.get(query.table);
if (table) {
await table.close();
}
this.table.delete(query.table);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
}
async closeAll() {
this.checkOpened();
const promises = [];
for (const table of this.table.keys()) {
promises.push(this.close({table}));
}
await Promise.all(promises);
}
/*
query = {
(!) table: 'tableName'
},
result = Boolean
*/
async tableExists(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
if (this.table.has(query.table))
return true;
if (await utils.pathExists(`${this.dbPath}/${query.table}`))
return true;
return false;
}
/*
query = {
table: 'tableName'
},
result = {
dbPath: String,
tableName1: {opened: Boolean, ...},
tableName2: {opened: Boolean, ...},
...
}
*/
async getDbInfo(query = {}) {
this.checkOpened();
const tables = await this._getTableList();
const result = {dbPath: this.dbPath};
for (const table of tables) {
if (!query.table || (query.table && table == query.table)) {
const tableInstance = this.table.get(table);
if (tableInstance && tableInstance.opened) {
result[table] = await tableInstance.getMeta();
result[table].opened = true;
} else {
result[table] = {opened: false};
}
}
}
return result;
}
/*
result = {
total: Number,
tables: {
tableName1: Number,
tableName2: Number,
...
}
}
*/
async getDbSize() {
this.checkOpened();
const dirs = await fs.readdir(this.dbPath, { withFileTypes: true });
const result = {total: 0, tables: {}};
for (const dir of dirs) {
if (dir.isDirectory()) {
const table = dir.name;
const tablePath = `${this.dbPath}/${table}`;
const files = await fs.readdir(tablePath, { withFileTypes: true });
if (!result.tables[table])
result.tables[table] = 0;
for (const file of files) {
if (file.isFile()) {
let size = 0;
try {
size = (await fs.stat(`${tablePath}/${file.name}`)).size;
} catch(e) {
//
}
result.tables[table] += size;
result.total += size;
}
}
}
}
return result;
}
/*
query = {
(!) table: 'tableName',
distinct: 'fieldName' || Array,
count: Boolean,
map: '(r) => ({id1: r.id, ...})',
where: `@@index('field1', 10, 20)`,
sort: '(a, b) => a.id - b.id',
limit: 10,
offset: 10,
}
result = Array
*/
async select(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
const table = this.table.get(query.table);
if (table) {
return await table.select(query);
} else {
if (await this.tableExists({table: query.table})) {
throw new Error(`Table '${query.table}' has not been opened yet`);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
}
}
/*
query = {
(!) table: 'tableName',
replace: Boolean,
(!) rows: Array,
}
result = {
(!) inserted: Number,
(!) replaced: Number,
}
*/
async insert(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
const table = this.table.get(query.table);
if (table) {
return await table.insert(query);
} else {
if (await this.tableExists({table: query.table})) {
throw new Error(`Table '${query.table}' has not been opened yet`);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
}
}
/*
query = {
(!) table: 'tableName',
(!) mod: '(r) => r.count++',
where: `@@index('field1', 10, 20)`,
sort: '(a, b) => a.id - b.id',
limit: 10,
offset: 10,
}
result = {
(!) updated: Number,
}
*/
async update(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
const table = this.table.get(query.table);
if (table) {
return await table.update(query);
} else {
if (await this.tableExists({table: query.table})) {
throw new Error(`Table '${query.table}' has not been opened yet`);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
}
}
/*
query = {
(!) table: 'tableName',
where: `@@index('field1', 10, 20)`,
sort: '(a, b) => a.id - b.id',
limit: 10,
offset: 10,
}
result = {
(!) deleted: Number,
}
*/
async delete(query = {}) {
this.checkOpened();
if (!query.table)
throw new Error(`'query.table' parameter is required`);
const table = this.table.get(query.table);
if (table) {
return await table.delete(query);
} else {
if (await this.tableExists({table: query.table})) {
throw new Error(`Table '${query.table}' has not been opened yet`);
} else {
throw new Error(`Table '${query.table}' does not exist`);
}
}
}
esc(obj) {
return utils.esc(obj);
}
}
module.exports = JembaDb;

View File

@@ -1,49 +0,0 @@
const { parentPort } = require('worker_threads');
const JembaDb = require('./JembaDb');
const db = new JembaDb();
if (parentPort) {
parentPort.on('message', async(mes) => {
let result = {};
try {
if (db[mes.action])
result.result = await db[mes.action](mes.query);
else
result = {error: 'Action not found: ' + mes.action};
} catch (e) {
result = {error: e.message};
}
result.requestId = mes.requestId;
parentPort.postMessage(result);
});
}
//This is for proper working of pkg (by zeit) and worker_threads
//just a copy of the above code as a string
module.exports = `
const { parentPort } = require('worker_threads');
const JembaDb = require('./JembaDb');
const db = new JembaDb();
if (parentPort) {
parentPort.on('message', async(mes) => {
let result = {};
try {
if (db[mes.action])
result.result = await db[mes.action](mes.query);
else
result = {error: 'Action not found: ' + mes.action};
} catch (e) {
result = {error: e.message};
}
result.requestId = mes.requestId;
parentPort.postMessage(result);
});
}
`.replace('./JembaDb', `${__dirname.replace(/\\/g, '/')}/JembaDb`);

View File

@@ -1,119 +0,0 @@
const { Worker } = require('worker_threads');
const utils = require('./utils');
const JembaDbChild = require('./JembaDbChild');
/* API methods:
openDb
closeDb
create
drop
open
openAll
close
closeAll
tableExists
getInfo
getDbSize
select
insert
update
delete
esc
*/
class JembaDbThread {
constructor() {
this.worker = null;
this.listeners = new Map();
this.requestId = 0;
const apiMethods = [
'create', 'drop', 'open', 'openAll', 'close', 'closeAll',
'tableExists', 'getDbInfo', 'getDbSize', 'select', 'insert', 'update', 'delete', 'dumpTables'
];
for (const action of apiMethods) {
this[action] = async(query) => this._action(action, query);
}
}
_terminate() {
if (this.worker) {
for (const listener of this.listeners.values()) {
listener({error: 'Worker terminated'});
}
this.worker.terminate();
}
this.worker = null;
}
_runWoker() {
//const worker = new Worker(`${__dirname}/JembaDbChild.js`);
const worker = new Worker(JembaDbChild, {eval: true});
worker.on('message', (mes) => {
const listener = this.listeners.get(mes.requestId);
if (listener)
listener(mes);
});
worker.on('error', (err) => {
console.error(err);
});
worker.on('exit', () => {
this._terminate();
});
this.worker = worker;
}
_action(action, query) {
return new Promise((resolve, reject) => {
this.requestId++;
const requestId = this.requestId; //!!!
this.listeners.set(requestId, (mes) => {
this.listeners.delete(requestId);
if (mes.error)
reject(new Error(mes.error));
else
resolve(mes.result);
});
if (this.worker) {
this.worker.postMessage({requestId: this.requestId, action, query});
} else {
reject(new Error('Worker does not exist (database closed?)'));
}
});
}
async openDb(query = {}) {
if (!this.worker) {
this._runWoker();
} else {
throw new Error('Worker has been created already');
}
return this._action('openDb', query);
}
async closeDb() {
const result = await this._action('closeDb');
this._terminate();
//console.log('DB closed');
return result;
}
esc(obj) {
return utils.esc(obj);
}
}
module.exports = JembaDbThread;

View File

@@ -1,38 +0,0 @@
class LockQueue {
constructor(queueSize) {
this.queueSize = queueSize;
this.freed = true;
this.waitingQueue = [];
}
ret() {
this.freed = true;
if (this.waitingQueue.length) {
this.waitingQueue.shift().onFreed();
}
}
get(take = true) {
return new Promise((resolve) => {
if (this.freed) {
if (take)
this.freed = false;
resolve();
return;
}
if (this.waitingQueue.length >= this.queueSize)
throw new Error('Lock queue is too long');
this.waitingQueue.push({
onFreed: () => {
if (take)
this.freed = false;
resolve();
},
});
});
}
}
module.exports = LockQueue;

View File

@@ -1,852 +0,0 @@
const fs = require('fs').promises;
const utils = require('./utils');
const TableReducer = require('./TableReducer');
const TableRowsMem = require('./TableRowsMem');
const TableRowsFile = require('./TableRowsFile');
const LockQueue = require('./LockQueue');
const maxChangesLength = 10;
class Table {
constructor() {
this.rowsInterface = new TableRowsMem();
this.autoIncrement = 0;
this.fileError = '';
this.openingLock = new LockQueue(100);
this.lock = new LockQueue(100);
this.opened = false;
this.closed = false;
this.deltaStep = 0;
this.changes = [];
//table options defaults
this.inMemory = false;
this.compressed = 0;
this.cacheSize = 5;
this.compressed = 0;
this.recreate = false;
this.autoRepair = false;
this.forceFileClosing = false;
}
checkErrors() {
if (this.fileError)
throw new Error(this.fileError);
if (this.closed)
throw new Error('Table closed');
if (!this.opened)
throw new Error('Table has not been opened yet');
}
async waitForSaveChanges() {
if (this.changes.length > maxChangesLength) {
let i = this.changes.length - maxChangesLength;
while (i > 0 && this.changes.length > maxChangesLength) {
i--;
await utils.sleep(10);
}
}
}
async recreateTable() {
const tempTablePath = `${this.tablePath}___temporary_recreating`;
await fs.rmdir(tempTablePath, { recursive: true });
await fs.mkdir(tempTablePath, { recursive: true });
const tableRowsFileSrc = new TableRowsFile(this.tablePath, this.cacheSize);
const tableRowsFileDest = new TableRowsFile(tempTablePath, this.cacheSize, this.compressed);
const reducerDest = new TableReducer(false, tempTablePath, this.compressed, tableRowsFileDest);
try {
await tableRowsFileSrc.loadCorrupted();
} catch (e) {
console.error(e);
}
try {
await reducerDest._load(true, `${this.tablePath}/meta.0`);
} catch (e) {
console.error(e);
}
const putRows = async(rows) => {
const oldRows = [];
const newRows = [];
const newRowsStr = [];
//checks
for (const row of rows) {
if (!row) {
continue;
}
const t = typeof(row.id);
if (t !== 'number' && t !== 'string') {
continue;
}
const oldRow = await tableRowsFileDest.getRow(row.id);
if (oldRow) {
continue;
}
let str = '';
try {
str = JSON.stringify(row);//because of stringify errors
} catch(e) {
continue;
}
newRows.push(row);
oldRows.push({});
newRowsStr.push(str);
}
try {
//reducer
reducerDest._update(oldRows, newRows, 1);
//insert
for (let i = 0; i < newRows.length; i++) {
const newRow = newRows[i];
const newRowStr = newRowsStr[i];
tableRowsFileDest.setRow(newRow.id, newRow, newRowStr, 1);
}
await tableRowsFileDest.saveDelta(1);
await reducerDest._saveDelta(1);
} catch(e) {
console.error(e);
}
};
let rows = [];
for (const id of tableRowsFileSrc.getAllIds()) {
if (this.closed)
throw new Error('Table closed');
let row = null;
try {
row = await tableRowsFileSrc.getRow(id);
} catch(e) {
console.error(e);
continue;
}
rows.push(row);
if (rows.length > 1000) {
await putRows(rows);
rows = [];
}
}
if (rows.length)
await putRows(rows);
await tableRowsFileDest.saveDelta(0);
const delta = reducerDest._getDelta(0);
delta.dumpMeta = true;
await reducerDest._saveDelta(0);
await tableRowsFileSrc.destroy();
await reducerDest._destroy();
await tableRowsFileDest.destroy();
await fs.writeFile(`${tempTablePath}/state`, '1');
await fs.rmdir(this.tablePath, { recursive: true });
await fs.rename(tempTablePath, this.tablePath);
}
/*
query: {
tablePath: String,
inMemory: Boolean,
cacheSize: Number,
compressed: Number, 0..9
recreate: Boolean, false,
autoRepair: Boolean, false,
forceFileClosing: Boolean, false,
lazyOpen: Boolean, false,
}
*/
async _open(query = {}) {
if (this.opening)
return;
this.opening = true;
await this.openingLock.get();
//console.log(query);
try {
if (this.opened)
throw new Error('Table has already been opened');
if (this.closed)
throw new Error('Table instance has been destroyed. Please create a new one.');
this.inMemory = !!query.inMemory;
if (this.inMemory) {
this.reducer = new TableReducer(this.inMemory, '', 0, this.rowsInterface);
} else {
if (!query.tablePath)
throw new Error(`'query.tablePath' parameter is required`);
this.tablePath = query.tablePath;
this.cacheSize = query.cacheSize || 5;
this.compressed = query.compressed || 0;
this.recreate = query.recreate || false;
this.autoRepair = query.autoRepair || false;
this.forceFileClosing = query.forceFileClosing || false;
await fs.mkdir(this.tablePath, { recursive: true });
this.tableRowsFile = new TableRowsFile(query.tablePath, this.cacheSize, this.compressed);
this.rowsInterface = this.tableRowsFile;
this.reducer = new TableReducer(this.inMemory, this.tablePath, this.compressed, this.rowsInterface);
const statePath = `${this.tablePath}/state`;
let state = null;
if (await utils.pathExists(statePath)) {
state = await fs.readFile(statePath, 'utf8');
}
if (state === null) {//check if other files exists
const files = await fs.readdir(this.tablePath);
if (files.length)
state = '0';
}
if (this.recreate) {
await this.recreateTable();
state = '1';
}
if (state !== null) {
try {
if (state === '1') {
// load tableRowsFile & reducer
this.autoIncrement = await this.tableRowsFile.load();
await this.reducer._load();
} else {
throw new Error('Table corrupted')
}
} catch(e) {
if (this.autoRepair) {
console.error(e.message);
await this.recreateTable();
} else {
throw e;
}
// load tableRowsFile & reducer
this.autoIncrement = await this.tableRowsFile.load();
await this.reducer._load();
}
}
}
this.opened = true;
} catch(e) {
await this.close();
const errMes = `Open table (${query.tablePath}): ${e.message}`;
if (!query.lazyOpen)
throw new Error(errMes);
else
this.fileError = errMes;
} finally {
this.openingLock.ret();
this.opening = false;
}
}
async open(query = {}) {
if (query.lazyOpen) {
this._open(query);
} else {
await this._open(query);
}
}
async close() {
if (this.closed)
return;
this.opened = false;
this.closed = true;
if (!this.inMemory) {
while (this.savingChanges) {
await utils.sleep(10);
}
}
//for GC
if (this.reducer)
await this.reducer._destroy();
this.reducer = null;
if (this.rowsInterface)
await this.rowsInterface.destroy();
this.rowsInterface = null;
this.tableRowsFile = null;
}
/*
query = {
quietIfExists: Boolean,
flag: Object || Array, {name: 'flag1', check: '(r) => r.id > 10'}
hash: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
index: Object || Array, {field: 'field1', type: 'string', depth: 11, allowUndef: false}
}
result = {}
*/
async create(query) {
await this.openingLock.get(false);
this.checkErrors();
await this.lock.get();
try {
this.deltaStep++;
try {
if (query.flag) {
for (const flag of utils.paramToArray(query.flag)) {
await this.reducer._addFlag(flag, query.quietIfExists, this.deltaStep);
}
}
if (query.hash) {
for (const hash of utils.paramToArray(query.hash)) {
await this.reducer._addHash(hash, query.quietIfExists, this.deltaStep);
}
}
if (query.index) {
for (const index of utils.paramToArray(query.index)) {
await this.reducer._addIndex(index, query.quietIfExists, this.deltaStep);
}
}
this.changes.push([this.deltaStep, 1]);
} catch(e) {
this.changes.push([this.deltaStep, 0]);
throw e;
}
return {};
} finally {
this.saveChanges();//no await
this.lock.ret();
}
}
/*
query = {
flag: Object || Array, {name: 'flag1'}
hash: Object || Array, {field: 'field1'}
index: Object || Array, {field: 'field1'}
}
result = {}
*/
async drop(query) {
await this.openingLock.get(false);
this.checkErrors();
await this.lock.get();
try {
this.deltaStep++;
try {
if (query.flag) {
for (const flag of utils.paramToArray(query.flag)) {
await this.reducer._delFlag(flag.name, this.deltaStep);
}
}
if (query.hash) {
for (const hash of utils.paramToArray(query.hash)) {
await this.reducer._delHash(hash.field, this.deltaStep);
}
}
if (query.index) {
for (const index of utils.paramToArray(query.index)) {
await this.reducer._delIndex(index.field, this.deltaStep);
}
}
this.changes.push([this.deltaStep, 1]);
} catch(e) {
this.changes.push([this.deltaStep, 0]);
throw e;
}
return {};
} finally {
this.saveChanges();//no await
this.lock.ret();
}
}
/*
result = {
inMemory: Boolean,
flag: Array, [{name: 'flag1', check: '(r) => r.id > 10'}, ...]
hash: Array, [{field: 'field1', type: 'string', depth: 11, allowUndef: false}, ...]
index: Array, [{field: 'field1', type: 'string', depth: 11, allowUndef: false}, ...]
}
*/
async getMeta() {
this.checkErrors();
return {
inMemory: this.inMemory,
flag: this.reducer._listFlag(),
hash: this.reducer._listHash(),
index: this.reducer._listIndex(),
};
}
prepareWhere(where) {
if (typeof(where) !== 'string')
throw new Error('query.where must be a string');
return `async(__tr) => {${where.replace(/@@/g, 'return await __tr.').replace(/@/g, 'await __tr.')}}`;
}
/*
query = {
distinct: 'fieldName' || Array,
count: Boolean,
map: '(r) => ({id1: r.id, ...})',
where: `@@index('field1', 10, 20)`,
sort: '(a, b) => a.id - b.id',
limit: 10,
offset: 10,
}
result = Array
*/
async select(query = {}) {
await this.openingLock.get(false);
this.checkErrors();
let ids;//iterator
if (query.where) {
const where = this.prepareWhere(query.where);
const whereFunc = new Function(`return ${where}`)();
ids = await whereFunc(this.reducer);
} else {
ids = this.rowsInterface.getAllIds();
}
let found = [];
let distinct = () => true;
if (query.distinct) {
const distFields = (Array.isArray(query.distinct) ? query.distinct : [query.distinct]);
const dist = new Map();
distinct = (row) => {
let uniq = '';
for (const field of distFields) {
const value = row[field];
uniq += `${(value === undefined ? '___' : '')}${field}:${value}`;
}
if (dist.has(uniq))
return false;
dist.set(uniq, true);
return true;
};
}
if (!query.where && !query.distinct && query.count) {//some optimization
found = [{count: this.rowsInterface.getAllIdsSize()}];
} else {//full running
for (const id of ids) {
const row = await this.rowsInterface.getRow(id);
if (row && distinct(row)) {
found.push(row);
}
}
if (query.count) {
found = [{count: found.length}];
}
}
let result = [];
if (query.map) {
const mapFunc = new Function(`return ${query.map}`)();
for (const row of found) {
result.push(mapFunc(row));
}
} else {
result = found;
}
if (query.sort) {
const sortFunc = new Function(`return ${query.sort}`)();
result.sort(sortFunc);
}
if (query.hasOwnProperty('limit') || query.hasOwnProperty('offset')) {
const offset = query.offset || 0;
const limit = (query.hasOwnProperty('limit') ? query.limit : result.length);
result = result.slice(offset, offset + limit);
}
return utils.cloneDeep(result);
}
/*
query = {
replace: Boolean,
(!) rows: Array,
}
result = {
(!) inserted: Number,
(!) replaced: Number,
}
*/
async insert(query = {}) {
await this.openingLock.get(false);
this.checkErrors();
await this.lock.get();
try {
if (!Array.isArray(query.rows)) {
throw new Error('query.rows must be an array');
}
const newRows = utils.cloneDeep(query.rows);
const replace = query.replace;
//autoIncrement correction
for (const newRow of newRows) {
if (typeof(newRow.id) === 'number' && newRow.id >= this.autoIncrement)
this.autoIncrement = newRow.id + 1;
}
const oldRows = [];
const newRowsStr = [];
//checks
for (const newRow of newRows) {
if (newRow.hasOwnProperty('___meta'))
throw new Error(`Use of field with name '___meta' is forbidden`);
if (newRow.id === undefined) {
newRow.id = this.autoIncrement;
this.autoIncrement++;
}
const t = typeof(newRow.id);
if (t !== 'number' && t !== 'string') {
throw new Error(`Row id bad type, 'number' or 'string' expected, got ${t}`);
}
const oldRow = await this.rowsInterface.getRow(newRow.id);
if (!replace && oldRow) {
throw new Error(`Record id:${newRow.id} already exists`);
}
oldRows.push((oldRow ? oldRow : {}));
newRowsStr.push(JSON.stringify(newRow));//because of stringify errors
}
const result = {inserted: 0, replaced: 0};
this.deltaStep++;
try {
//reducer
this.reducer._update(oldRows, newRows, this.deltaStep);
//insert
for (let i = 0; i < newRows.length; i++) {
const newRow = newRows[i];
const newRowStr = newRowsStr[i];
const oldRow = oldRows[i];
this.rowsInterface.setRow(newRow.id, newRow, newRowStr, this.deltaStep);
if (oldRow.id !== undefined)
result.replaced++;
else
result.inserted++;
}
this.changes.push([this.deltaStep, 1]);
} catch(e) {
this.changes.push([this.deltaStep, 0]);
throw e;
}
await this.waitForSaveChanges();
return result;
} finally {
this.saveChanges();//no await
this.lock.ret();
}
}
/*
query = {
(!) mod: '(r) => r.count++',
where: `@@index('field1', 10, 20)`,
sort: '(a, b) => a.id - b.id',
limit: 10,
offset: 10,
}
result = {
(!) updated: Number,
}
*/
async update(query = {}) {
await this.openingLock.get(false);
this.checkErrors();
await this.lock.get();
try {
if (typeof(query.mod) !== 'string') {
throw new Error('query.mod must be a string');
}
const modFunc = new Function(`return ${query.mod}`)();
//where
let ids;//iterator
if (query.where) {
const where = this.prepareWhere(query.where);
const whereFunc = new Function(`return ${where}`)();
ids = await whereFunc(this.reducer);
} else {
ids = this.rowsInterface.getAllIds();
}
//oldRows
let oldRows = [];
for (const id of ids) {
const oldRow = await this.rowsInterface.getRow(id);
if (oldRow) {
oldRows.push(oldRow);
}
}
if (query.sort) {
const sortFunc = new Function(`return ${query.sort}`)();
oldRows.sort(sortFunc);
}
let newRows = utils.cloneDeep(oldRows);
if (query.hasOwnProperty('limit') || query.hasOwnProperty('offset')) {
const offset = query.offset || 0;
const limit = (query.hasOwnProperty('limit') ? query.limit : newRows.length);
newRows = newRows.slice(offset, offset + limit);
oldRows = oldRows.slice(offset, offset + limit);
}
//mod & checks
const context = {};
const newRowsStr = [];
for (const newRow of newRows) {
modFunc(newRow, context);
const t = typeof(newRow.id);
if (t !== 'number' && t !== 'string') {
throw new Error(`Row id bad type, 'number' or 'string' expected, got ${t}`);
}
//autoIncrement correction
if (t === 'number' && newRow.id >= this.autoIncrement)
this.autoIncrement = newRow.id + 1;
if (newRow.hasOwnProperty('___meta'))
throw new Error(`Use of field with name '___meta' is forbidden`);
newRowsStr.push(JSON.stringify(newRow));//because of stringify errors
}
this.deltaStep++;
const result = {updated: 0};
try {
//reducer
this.reducer._update(oldRows, newRows, this.deltaStep);
//replace
for (let i = 0; i < newRows.length; i++) {
const newRow = newRows[i];
const newRowStr = newRowsStr[i];
// oldRow.id === newRow.id always here, so
this.rowsInterface.setRow(newRow.id, newRow, newRowStr, this.deltaStep);
result.updated++;
}
this.changes.push([this.deltaStep, 1]);
} catch(e) {
this.changes.push([this.deltaStep, 0]);
throw e;
}
await this.waitForSaveChanges();
return result;
} finally {
this.saveChanges();//no await
this.lock.ret();
}
}
/*
query = {
where: `@@index('field1', 10, 20)`,
sort: '(a, b) => a.id - b.id',
limit: 10,
offset: 10,
}
result = {
(!) deleted: Number,
}
*/
async delete(query = {}) {
await this.openingLock.get(false);
this.checkErrors();
await this.lock.get();
try {
//where
let ids;//iterator
if (query.where) {
const where = this.prepareWhere(query.where);
const whereFunc = new Function(`return ${where}`)();
ids = await whereFunc(this.reducer);
} else {
ids = this.rowsInterface.getAllIds();
}
//oldRows
let oldRows = [];
let newRows = [];
for (const id of ids) {
const oldRow = await this.rowsInterface.getRow(id);
if (oldRow) {
oldRows.push(oldRow);
newRows.push({});
}
}
if (query.sort) {
const sortFunc = new Function(`return ${query.sort}`)();
oldRows.sort(sortFunc);
}
if (query.hasOwnProperty('limit') || query.hasOwnProperty('offset')) {
const offset = query.offset || 0;
const limit = (query.hasOwnProperty('limit') ? query.limit : newRows.length);
newRows = newRows.slice(offset, offset + limit);
oldRows = oldRows.slice(offset, offset + limit);
}
this.deltaStep++;
const result = {deleted: 0};
try {
//reducer
this.reducer._update(oldRows, newRows, this.deltaStep);
//delete
for (let i = 0; i < oldRows.length; i++) {
const oldRow = oldRows[i];
this.rowsInterface.deleteRow(oldRow.id, this.deltaStep);
result.deleted++;
}
this.changes.push([this.deltaStep, 1]);
} catch(e) {
this.changes.push([this.deltaStep, 0]);
throw e;
}
await this.waitForSaveChanges();
return result;
} finally {
this.saveChanges();//no await
this.lock.ret();
}
}
async saveState(state) {
await fs.writeFile(`${this.tablePath}/state`, state);
}
async saveChanges() {
this.needSaveChanges = true;
if (this.savingChanges)
return;
if (this.inMemory) {
this.changes = [];
return;
}
try {
this.checkErrors();
} catch(e) {
return;
}
this.savingChanges = true;
try {
await utils.sleep(0);
while (this.needSaveChanges) {
this.needSaveChanges = false;
await this.saveState('0');
while (this.changes.length) {
const len = this.changes.length;
let i = 0;
while (i < len) {
const [deltaStep, isOk] = this.changes[i];
i++;
if (isOk) {
await this.tableRowsFile.saveDelta(deltaStep);
await this.reducer._saveDelta(deltaStep);
} else {
await this.tableRowsFile.cancelDelta(deltaStep);
await this.reducer._cancelDelta(deltaStep);
}
}
this.changes = this.changes.slice(i);
}
await this.saveState('1');
if (this.forceFileClosing) {
await this.tableRowsFile.closeAllFiles();
await this.reducer._closeAllFiles();
}
}
} catch(e) {
console.error(e.message);
this.fileError = e.message;
} finally {
this.savingChanges = false;
}
}
}
module.exports = Table;

View File

@@ -1,22 +0,0 @@
class TableFlag {
constructor(checkCode) {
this.checkCode = checkCode;
this.checkFunc = eval(checkCode);
this.flag = new Set();
}
add(row) {
if (this.checkFunc(row)) {
this.flag.add(row.id);
return true;
}
return false;
}
del(row) {
this.flag.delete(row.id);
}
}
module.exports = TableFlag;

View File

@@ -1,172 +0,0 @@
class TableHash {
//opts.type = 'string' || 'number' || 'number_as_string'
constructor(opts = {}) {
const type = opts.type || 'string';
this.depth = opts.depth || 11;
this.allowUndef = opts.allowUndef || false;
this.unique = opts.unique || false;
this.hash = new Map();
this.isNumber = (type === 'number' || type === 'number_as_string');
this.numberAsString = (type === 'number_as_string');
this.valueAsString = !this.isNumber || this.numberAsString;
}
checkType(v) {
if (typeof(v) != 'number' && this.isNumber)
throw new Error(`Hashed value must be a number, got type:${typeof(v)}, value:${v}`);
if (typeof(v) != 'string' && !this.isNumber)
throw new Error(`Hashed value must be a string, got type:${typeof(v)}, value:${v}`);
}
prepareValue(v) {
let result = v;
if (this.numberAsString) {
result = v.toString().padStart(this.depth, '0');
}
if (this.valueAsString && result.length > this.depth)
result = result.substring(0, this.depth);
return result;
}
add(value, id) {
if (value === undefined && this.allowUndef)
return;
this.checkType(value);
value = this.prepareValue(value);
if (this.hash.has(value)) {
if (this.unique) {
const id_ = this.hash.get(value);
if (id_ !== id) {
throw new Error(`Collision for unique hash detected: value:${value}, id1:${id_}, id2:${id}`);
}
} else {
const ids = this.hash.get(value);
ids.add(id);
}
} else {
if (this.unique) {
this.hash.set(value, id);
} else {
const ids = new Set();
this.hash.set(value, ids);
ids.add(id);
}
}
return value;
}
del(value, id) {
if (value === undefined && this.allowUndef)
return;
this.checkType(value);
value = this.prepareValue(value);
if (this.hash.has(value)) {
if (this.unique) {
const id_ = this.hash.get(value);
if (id_ === id)
this.hash.delete(value);
} else {
const ids = this.hash.get(value);
ids.delete(id);
if (!ids.size) {
this.hash.delete(value);
}
}
}
return value;
}
reduce(value) {
this.checkType(value);
value = this.prepareValue(value);
let result;
if (this.hash.has(value)) {
if (this.unique) {
result = new Set();
result.add(this.hash.get(value));
} else {
result = this.hash.get(value);
}
} else {
result = new Set();
}
return result;
}
min() {
let result = new Set();
let min = null;
let id = null;
for (const value of this.hash.keys()) {
if (value < min || min === null) {
min = value;
id = this.hash.get(min);
}
}
if (id !== null) {
if (this.unique)
result.add(id);
else
result = id;
}
return result;
}
max() {
let result = new Set();
let max = null;
let id = null;
for (const value of this.hash.keys()) {
if (value > max || max === null) {
max = value;
id = this.hash.get(max);
}
}
if (id !== null) {
if (this.unique)
result.add(id);
else
result = id;
}
return result;
}
iter(checkFunc) {
const result = new Set();
for (const [value, ids] of this.hash.entries()) {
const checkResult = checkFunc(value);
if (checkResult === undefined)
break;
if (checkResult) {
if (this.unique) {
result.add(ids);
} else {
for (const id of ids)
result.add(id);
}
}
}
return result;
}
}
module.exports = TableHash;

View File

@@ -1,311 +0,0 @@
const utils = require('./utils');
class TableIndex {
//opts.type = 'string' || 'number' || 'number_as_string'
constructor(opts = {}) {
const type = opts.type || 'string';
this.depth = opts.depth || 11;
this.allowUndef = opts.allowUndef || false;
this.unique = opts.unique || false;
this.hash = new Map();
this.sorted = [[]];
this.delCount = 0;
this.isNumber = (type === 'number' || type === 'number_as_string');
this.numberAsString = (type === 'number_as_string');
this.valueAsString = !this.isNumber || this.numberAsString;
this.cmp = (a, b) => a.localeCompare(b);
if (type === 'number') {
this.cmp = (a, b) => a - b;
} else if (type === 'number_as_string') {
this.cmp = (a, b) => (a < b ? -1 : (a > b ? 1 : 0));
}
}
checkType(v) {
if (typeof(v) != 'number' && this.isNumber)
throw new Error(`Indexed value must be a number, got type:${typeof(v)}, value:${v}`);
if (typeof(v) != 'string' && !this.isNumber)
throw new Error(`Indexed value must be a string, got type:${typeof(v)}, value:${v}`);
}
prepareValue(v) {
let result = v;
if (this.numberAsString) {
result = v.toString().padStart(this.depth, '0');
}
if (this.valueAsString && result.length > this.depth)
result = result.substring(0, this.depth);
return result;
}
add(value, id) {
if (value === undefined && this.allowUndef)
return;
this.checkType(value);
value = this.prepareValue(value);
if (this.hash.has(value)) {
if (this.unique) {
const id_ = this.hash.get(value);
if (id_ !== id) {
throw new Error(`Collision for unique index detected: value:${value}, id1:${id_}, id2:${id}`);
}
} else {
const ids = this.hash.get(value);
ids.add(id);
}
} else {
if (this.unique) {
this.hash.set(value, id);
} else {
const ids = new Set();
this.hash.set(value, ids);
ids.add(id);
}
let s = this.sorted.length - 1;
const d = this.sorted[s];
d.push(value);
let i = d.length - 1;
//вставка
while (i > 0 && this.cmp(d[i], d[i - 1]) < 0) {
const v = d[i];
d[i] = d[i - 1];
d[i - 1] = v;
i--;
}
if (d.length > 10) {
//слияние
while (s > 0 && this.sorted[s].length >= this.sorted[s - 1].length) {
const a = this.sorted.pop();
const b = this.sorted.pop();
const c = [];
let i = 0;
let j = 0;
while (i < a.length || j < b.length) {
if (i < a.length && (j === b.length || this.cmp(a[i], b[j]) <= 0)) {
c.push(a[i]);
i++;
}
if (j < b.length && (i === a.length || this.cmp(b[j], a[i]) <= 0)) {
c.push(b[j]);
j++;
}
}
this.sorted.push(c);
s--;
}
this.sorted.push([]);
}
}
return value;
}
del(value, id, forceClean = false) {
if (value === undefined && this.allowUndef)
return;
this.checkType(value);
value = this.prepareValue(value);
if (this.hash.has(value)) {
if (this.unique) {
const id_ = this.hash.get(value);
if (id_ === id) {
this.hash.delete(value);
this.delCount++;
}
} else {
const ids = this.hash.get(value);
ids.delete(id);
if (!ids.size) {
this.hash.delete(value);
this.delCount++;
}
}
}
if (this.delCount > (this.sorted[0].length >> 2) || forceClean) {
for (let s = 0; s < this.sorted.length; s++) {
const a = this.sorted[s];
const b = [];
for (let i = 0; i < a.length; i++) {
if (this.hash.has(a[i]))
b.push(a[i]);
}
this.sorted[s] = b;
}
this.sorted = this.sorted.filter(a => a.length);
if (!this.sorted.length) {
this.sorted = [[]]
} else {
this.sorted.sort((a, b) => b.length - a.length);
}
this.delCount = 0;
}
return value;
}
reduce(from, to) {
const useFrom = (from !== undefined);
const useTo = (to !== undefined);
if (useFrom) {
this.checkType(from);
from = this.prepareValue(from);
}
if (useTo) {
this.checkType(to);
to = this.prepareValue(to);
}
const result = [];
for (let s = 0; s < this.sorted.length; s++) {
const a = this.sorted[s];
if (!a.length) // на всякий случай
continue;
let leftIndex = 0;
if (useFrom) {
//дихотомия
let left = 0;
let right = a.length - 1;
while (left < right) {
let mid = left + ((right - left) >> 1);
if (this.cmp(from, a[mid]) <= 0)
right = mid;
else
left = mid + 1;
}
leftIndex = right;
if (this.cmp(from, a[right]) > 0)
leftIndex++;
}
let rightIndex = a.length;
if (useTo) {
//дихотомия
let left = 0;
let right = a.length - 1;
while (left < right) {
let mid = right - ((right - left) >> 1);
if (this.cmp(to, a[mid]) >= 0)
left = mid;
else
right = mid - 1;
}
rightIndex = left;
if (this.cmp(to, a[left]) >= 0)
rightIndex++;
}
//console.log(a, leftIndex, rightIndex);
if (this.unique) {
const ids = new Set();
for (let i = leftIndex; i < rightIndex; i++) {
const value = a[i];
if (this.hash.has(value)) {
ids.add(this.hash.get(value));
}
}
result.push(ids);
} else {
for (let i = leftIndex; i < rightIndex; i++) {
const value = a[i];
if (this.hash.has(value)) {
result.push(this.hash.get(value));
}
}
}
}
return utils.unionSet(result);
}
min() {
let result = new Set();
let min = null;
let id = null;
for (let s = 0; s < this.sorted.length; s++) {
const a = this.sorted[s];
if (!a.length) // на всякий случай
continue;
if (a[0] < min || min === null) {
min = a[0];
id = this.hash.get(min);
}
}
if (id !== null) {
if (this.unique)
result.add(id);
else
result = id;
}
return result;
}
max() {
let result = new Set();
let max = null;
let id = null;
for (let s = 0; s < this.sorted.length; s++) {
const a = this.sorted[s];
if (!a.length) // на всякий случай
continue;
const last = a.length - 1;
if (a[last] > max || max === null) {
max = a[last];
id = this.hash.get(max);
}
}
if (id !== null) {
if (this.unique)
result.add(id);
else
result = id;
}
return result;
}
iter(checkFunc) {
const result = new Set();
for (const [value, ids] of this.hash.entries()) {
const checkResult = checkFunc(value);
if (checkResult === undefined)
break;
if (checkResult) {
if (this.unique) {
result.add(ids);
} else {
for (const id of ids)
result.add(id);
}
}
}
return result;
}
}
module.exports = TableIndex;

File diff suppressed because it is too large Load Diff

View File

@@ -1,646 +0,0 @@
const fs = require('fs').promises;
const path = require('path');
const utils = require('./utils');
const maxBlockSize = 1024*1024;//bytes
const minFileDumpSize = 100*1024;//bytes
const maxFileDumpSize = 50*1024*1024;//bytes
const defragAfter = 10;
const defragBlockCountAtOnce = 10;//better >= defragAfter
class TableRowsFile {
constructor(tablePath, cacheSize, compressed) {
this.tablePath = tablePath;
this.loadedBlocksCount = cacheSize || 5;
this.loadedBlocksCount = (this.loadedBlocksCount <= 0 ? 0 : this.loadedBlocksCount);
this.compressed = compressed || 0;
this.blockIndex = new Map();
this.currentBlockIndex = 0;
this.lastSavedBlockIndex = 0;
this.blockList = new Map();
this.blocksNotFinalized = new Map();//indexes of blocks
this.loadedBlocks = [];
this.deltas = new Map();
this.defragCounter = 0;
this.destroyed = false;
this.blockindex0Size = 0;
this.blocklist0Size = 0;
this.fd = {
blockIndex: null,
blockList: null,
blockRows: null,
blockRowsIndex: null,//not a file descriptor
};
}
//--- rows interface
async getRow(id) {
const block = this.blockList.get(this.blockIndex.get(id));
if (block) {
if (!block.rows) {
await this.loadBlock(block);
}
this.unloadBlocksIfNeeded();//no await
return block.rows.get(id);
}
return;
}
setRow(id, row, rowStr, deltaStep) {
const delta = this.getDelta(deltaStep);
if (this.blockIndex.has(id)) {
this.deleteRow(id, deltaStep, delta);
}
const index = this.addToCurrentBlock(id, row, rowStr, deltaStep, delta);
this.blockIndex.set(id, index);
delta.blockIndex.push([id, index]);
}
deleteRow(id, deltaStep, delta) {
if (this.blockIndex.has(id)) {
if (!delta)
delta = this.getDelta(deltaStep);
const block = this.blockList.get(this.blockIndex.get(id));
if (block) {
block.delCount++;
delta.blockList.push([block.index, 1]);
}
this.blockIndex.delete(id);
delta.blockIndex.push([id, 0]);
}
}
getAllIds() {
return this.blockIndex.keys();
}
getAllIdsSize() {
return this.blockIndex.size;
}
//--- rows interface end
getDelta(deltaStep) {
if (this.deltas.has(deltaStep)) {
return this.deltas.get(deltaStep);
} else {
const delta = {
blockIndex: [],
blockList: [],
blockRows: [],
};
this.deltas.set(deltaStep, delta);
return delta;
}
}
createNewBlock() {
this.currentBlockIndex++;
const block = {
index: this.currentBlockIndex,
delCount: 0,
addCount: 0,
size: 0,
rows: new Map(),
rowsLength: 0,
final: false,
};
this.blockList.set(this.currentBlockIndex, block);
this.loadedBlocks.push(this.currentBlockIndex);
this.blocksNotFinalized.set(this.currentBlockIndex, 1);
return block;
}
addToCurrentBlock(id, row, rowStr, deltaStep, delta) {
if (!delta)
delta = this.getDelta(deltaStep);
let block = this.blockList.get(this.currentBlockIndex);
if (!block)
block = this.createNewBlock();
if (block.size > maxBlockSize)
block = this.createNewBlock();
if (!block.rows) {
throw new Error('TableRowsFile: something has gone wrong');
}
block.rows.set(id, row);
block.addCount++;
block.size += rowStr.length;
block.rowsLength = block.rows.size;
delta.blockList.push([block.index, 1]);
delta.blockRows.push([block.index, id, row]);
return block.index;
}
async unloadBlocksIfNeeded() {
this.needUnload = true;
if (this.unloadingBlocks)
return;
this.unloadingBlocks = true;
try {
while (this.needUnload) {
this.needUnload = false;
if (this.destroyed)
return;
await utils.sleep(10);
//check loaded
let missed = new Map();
while (this.loadedBlocks.length >= this.loadedBlocksCount) {
const index = this.loadedBlocks.shift();
if (index >= this.lastSavedBlockIndex) {
missed.set(index, 1);
continue;
}
const block = this.blockList.get(index);
if (block) {
block.rows = null;
//console.log(`unloaded block ${block.index}`);
}
if (this.destroyed)
return;
}
this.loadedBlocks = this.loadedBlocks.concat(Array.from(missed.keys()));
}
} finally {
this.unloadingBlocks = false;
}
}
async loadFile(filePath) {
let buf = await fs.readFile(filePath);
if (!buf.length)
throw new Error(`TableRowsFile: file ${filePath} is empty`);
const flag = buf[0];
if (flag === 50) {//flag '2' ~ finalized && compressed
const packed = Buffer.from(buf.buffer, buf.byteOffset + 1, buf.length - 1);
const data = await utils.inflate(packed);
buf = data.toString();
} else if (flag === 49) {//flag '1' ~ finalized
buf[0] = 32;//' '
buf = buf.toString();
} else {//flag '0' ~ not finalized
buf[0] = 32;//' '
const last = buf.length - 1;
if (buf[last] === 44) {//','
buf[last] = 93;//']'
buf = buf.toString();
} else {//corrupted or empty
buf = buf.toString();
if (this.loadCorrupted) {
const lastComma = buf.lastIndexOf(',');
if (lastComma >= 0)
buf = buf.substring(0, lastComma);
}
buf += ']';
}
}
let result;
try {
result = JSON.parse(buf);
} catch(e) {
throw new Error(`load ${filePath} failed: ${e.message}`);
}
return result;
}
async writeFinal(fileName, data) {
if (!this.compressed) {
await fs.writeFile(fileName, '1' + data);
} else {
let buf = Buffer.from(data);
buf = await utils.deflate(buf, this.compressed);
const fd = await fs.open(fileName, 'w');
await fd.write('2');
await fd.write(buf);
await fd.close();
}
}
async loadBlock(block) {
//console.log(`start load block ${block.index}`);
if (!block.rows) {
const arr = await this.loadFile(this.blockRowsFilePath(block.index));
block.rows = new Map(arr);
this.loadedBlocks.push(block.index);
//console.log(`loaded block ${block.index}`);
}
}
async closeFd(name) {
if (this.fd[name]) {
await this.fd[name].close();
this.fd[name] = null;
}
}
async openFd(name, fileName = '') {
if (this.fd[name])
return;
if (!fileName) {
throw new Error('TableRowsFile: fileName is empty');
}
const exists = await utils.pathExists(fileName);
const fd = await fs.open(fileName, 'a');
if (!exists) {
await fd.write('0[');
}
this.fd[name] = fd;
}
blockRowsFilePath(index) {
if (index < 1000000)
return `${this.tablePath}/${index.toString().padStart(6, '0')}.jem`;
else
return `${this.tablePath}/${index.toString().padStart(12, '0')}.jem`;
}
async finalizeBlocks() {
//console.log(this.blocksNotFinalized.size);
for (const index of this.blocksNotFinalized.keys()) {
if (this.destroyed)
return;
if (index >= this.lastSavedBlockIndex)
continue;
const block = this.blockList.get(index);
if (block) {
if (block.final)
throw new Error('finalizeBlocks: something wrong');
const blockPath = this.blockRowsFilePath(block.index);
//console.log(`start finalize block ${block.index}`);
const arr = await this.loadFile(blockPath);
const rows = new Map(arr);
const finBlockPath = `${blockPath}.tmp`;
const rowsStr = JSON.stringify(Array.from(rows));
await this.writeFinal(finBlockPath, rowsStr);
await fs.rename(finBlockPath, blockPath);
block.size = Buffer.byteLength(rowsStr, 'utf8') + 1;
block.rowsLength = rows.size;//insurance
block.final = true;
await this.fd.blockList.write(JSON.stringify(block) + ',');
//console.log(`finalized block ${block.index}`);
}
this.blocksNotFinalized.delete(index);
}
}
async dumpMaps() {
//dumping blockIndex
const blockindex1Size = (await this.fd.blockIndex.stat()).size;
if ((blockindex1Size > minFileDumpSize && blockindex1Size > this.blockindex0Size) || blockindex1Size > maxFileDumpSize) {
const blockindex0Path = `${this.tablePath}/blockindex.0`;
const blockindex2Path = `${this.tablePath}/blockindex.2`;
await this.writeFinal(blockindex2Path, JSON.stringify(Array.from(this.blockIndex)));
await fs.rename(blockindex2Path, blockindex0Path);
await this.closeFd('blockIndex');
await fs.unlink(`${this.tablePath}/blockindex.1`);
this.blockindex0Size = (await fs.stat(blockindex0Path)).size;
}
//dumping blockList
const blocklist1Size = (await this.fd.blockList.stat()).size;
if ((blocklist1Size > minFileDumpSize && blocklist1Size > this.blocklist0Size) || blocklist1Size > maxFileDumpSize) {
const blocklist0Path = `${this.tablePath}/blocklist.0`;
const blocklist2Path = `${this.tablePath}/blocklist.2`;
await this.writeFinal(blocklist2Path, JSON.stringify(Array.from(this.blockList.values())));
await fs.rename(blocklist2Path, blocklist0Path);
await this.closeFd('blockList');
await fs.unlink(`${this.tablePath}/blocklist.1`);
this.blocklist0Size = (await fs.stat(blocklist0Path)).size;
}
}
async saveDelta(deltaStep) {
const delta = this.getDelta(deltaStep);
//lastSavedBlockIndex
const len = delta.blockRows.length;
if (len) {
this.lastSavedBlockIndex = delta.blockRows[len - 1][0];
}
//check all blocks fragmentation
if (!this.defragCandidates)
this.defragCandidates = [];
if (!this.defragCandidates.length) {
if (this.defragCounter >= defragAfter) {
for (const block of this.blockList.values()) {
if (!block.final)
continue;
if (block.addCount - block.delCount < block.rowsLength/2 || block.size < maxBlockSize/2) {
this.defragCandidates.push(block);
}
}
this.defragCounter = 0;
} else {
this.defragCounter++;
}
}
let defragmented = 0;
while (this.defragCandidates.length) {
if (defragmented >= defragBlockCountAtOnce || this.destroyed)
break;
const block = this.defragCandidates.shift();
if (!block.rows) {
await this.loadBlock(block);
}
//move all active rows from fragmented block to current
for (const [id, row] of block.rows.entries()) {
if (this.blockIndex.get(id) === block.index) {
const newIndex = this.addToCurrentBlock(id, row, JSON.stringify(row), deltaStep, delta);
this.blockIndex.set(id, newIndex);
delta.blockIndex.push([id, newIndex]);
}
}
this.blockList.delete(block.index);
delta.blockList.push([block.index, 0]);
if (!delta.delFiles)
delta.delFiles = [];
delta.delFiles.push(this.blockRowsFilePath(block.index));
defragmented++;
//console.log(`defragmented block ${block.index}, size: ${block.size}, addCount: ${block.addCount}, delCount: ${block.delCount}, rowsLength: ${block.rowsLength}`);
}
//blockIndex delta save
if (!this.fd.blockIndex)
await this.openFd('blockIndex', `${this.tablePath}/blockindex.1`);
let buf = [];
for (const deltaRec of delta.blockIndex) {
buf.push(JSON.stringify(deltaRec));
}
if (buf.length)
await this.fd.blockIndex.write(buf.join(',') + ',');
//blockList delta save
if (!this.fd.blockList)
await this.openFd('blockList', `${this.tablePath}/blocklist.1`);
let lastSaved = 0;
buf = [];
for (const deltaRec of delta.blockList) {
const index = deltaRec[0];
const exists = deltaRec[1];
if (exists) {
if (lastSaved !== index) {//optimization
const block = this.blockList.get(index);
if (block)//might be defragmented already
buf.push(JSON.stringify(block));
lastSaved = index;
}
} else {
buf.push(JSON.stringify({index, deleted: 1}));
}
}
if (buf.length)
await this.fd.blockList.write(buf.join(',') + ',');
//blockRows delta save
buf = [];
for (const deltaRec of delta.blockRows) {
const [index, id, row] = deltaRec;
if (this.fd.blockRowsIndex !== index) {
if (buf.length)
await this.fd.blockRows.write(buf.join(',') + ',');
buf = [];
await this.closeFd('blockRows');
this.fd.blockRowsIndex = null;
}
if (!this.fd.blockRows) {
const blockPath = this.blockRowsFilePath(index);
await this.openFd('blockRows', blockPath);
this.fd.blockRowsIndex = index;
}
buf.push(JSON.stringify([id, row]));
}
if (buf.length)
await this.fd.blockRows.write(buf.join(',') + ',');
//blocks finalization
await this.finalizeBlocks();
this.unloadBlocksIfNeeded();//no await
//dumps if needed
await this.dumpMaps();
//delete files if needed
if (delta.delFiles) {
for (const fileName of delta.delFiles) {
//console.log(`delete ${fileName}`);
if (await utils.pathExists(fileName))
await fs.unlink(fileName);
}
}
this.deltas.delete(deltaStep);
}
async cancelDelta(deltaStep) {
this.deltas.delete(deltaStep);
}
async load() {
let autoIncrement = 0;
const loadBlockIndex = (fileNum, data) => {
if (fileNum === 0) {//dumped data
this.blockIndex = new Map(data);//much faster
for (const id of this.blockIndex.keys()) {
if (typeof(id) === 'number' && id >= autoIncrement)
autoIncrement = id + 1;
}
} else {
for (const rec of data) {
const [id, index] = rec;
if (index > 0) {
this.blockIndex.set(id, index);
if (typeof(id) === 'number' && id >= autoIncrement)
autoIncrement = id + 1;
} else
this.blockIndex.delete(id);
}
}
}
const loadBlockList = (data) => {
for (const rec of data) {
const block = rec;
if (block.deleted) {
this.blockList.delete(block.index);
} else {
block.rows = null;
this.blockList.set(block.index, block);
if (block.index > this.currentBlockIndex)
this.currentBlockIndex = block.index;
}
}
}
this.blockIndex.clear();
for (let i = 0; i < 2; i++) {
const dataPath = `${this.tablePath}/blockindex.${i}`;
if (await utils.pathExists(dataPath)) {
const data = await this.loadFile(dataPath);
loadBlockIndex(i, data);
}
}
const blockindex0Path = `${this.tablePath}/blockindex.0`;
if (await utils.pathExists(blockindex0Path))
this.blockindex0Size = (await fs.stat(blockindex0Path)).size;
this.currentBlockIndex = 0;
this.blockList.clear();
for (let i = 0; i < 2; i++) {
const dataPath = `${this.tablePath}/blocklist.${i}`;
if (await utils.pathExists(dataPath)) {
const data = await this.loadFile(dataPath);
loadBlockList(data);
}
}
const blocklist0Path = `${this.tablePath}/blocklist.0`;
if (await utils.pathExists(blocklist0Path))
this.blocklist0Size = (await fs.stat(blocklist0Path)).size;
this.lastSavedBlockIndex = this.currentBlockIndex;
const currentBlock = this.blockList.get(this.currentBlockIndex);
if (currentBlock)
await this.loadBlock(currentBlock);
this.blocksNotFinalized = new Map();
for (const block of this.blockList.values()) {
if (!block.final)
this.blocksNotFinalized.set(block.index, 1);
}
return autoIncrement;
}
async loadCorrupted() {
this.loadCorrupted = true;
const loadBlockIndex = (fileNum, data) => {
if (fileNum === 0) {//dumped data
this.blockIndex = new Map(data);//much faster
} else {
for (const rec of data) {
const [id, index] = rec;
if (index > 0)
this.blockIndex.set(id, index);
else
this.blockIndex.delete(id);
}
}
}
this.blockIndex.clear();
for (let i = 0; i < 2; i++) {
const dataPath = `${this.tablePath}/blockindex.${i}`;
if (await utils.pathExists(dataPath)) {
try {
const data = await this.loadFile(dataPath);
loadBlockIndex(i, data);
} catch(e) {
console.error(e);
}
}
}
const files = await fs.readdir(this.tablePath, { withFileTypes: true });
this.blockList.clear();
for (const file of files) {
if (file.isFile() && path.extname(file.name) == '.jem') {
const numStr = path.basename(file.name, '.jem');
const index = parseInt(numStr, 10);
if (!isNaN(index)) {
const block = {
index,
delCount: 0,
addCount: 0,
size: 0,
rows: null,
rowsLength: 0,
final: false,
};
this.blockList.set(block.index, block);
//console.log(index);
}
}
}
}
async closeAllFiles() {
await this.closeFd('blockIndex');
await this.closeFd('blockList');
await this.closeFd('blockRows');
}
async destroy() {
await this.closeAllFiles();
this.destroyed = true;
}
}
module.exports = TableRowsFile;

View File

@@ -1,34 +0,0 @@
class TableRowsMem {
constructor() {
this.rows = new Map();
}
//--- rows interface
async getRow(id) {
return this.rows.get(id);
}
setRow(id, row) {
this.rows.set(id, row);
}
deleteRow(id) {
this.rows.delete(id);
}
getAllIds() {
return this.rows.keys();
}
getAllIdsSize() {
return this.rows.size;
}
//--- rows interface end
async destroy() {
//for GC
this.rows = null;
}
}
module.exports = TableRowsMem;

View File

@@ -1,7 +0,0 @@
const JembaDb = require('./JembaDb');
const JembaDbThread = require('./JembaDbThread');
module.exports = {
JembaDb,
JembaDbThread,
};

View File

@@ -1,152 +0,0 @@
const fsCB = require('fs');
const fs = fsCB.promises;
const zlib = require('zlib');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function sleepWithStop(ms, cb = () => {}) {
return new Promise(resolve => {
const timer = setTimeout(resolve, ms);
cb(() => { clearTimeout(timer); resolve(); });
});
}
function unionSet(arrSet) {
if (!arrSet.length)
return new Set();
let max = 0;
let size = arrSet[0].size;
for (let i = 1; i < arrSet.length; i++) {
if (arrSet[i].size > size) {
max = i;
size = arrSet[i].size;
}
}
const result = new Set(arrSet[max]);
for (let i = 0; i < arrSet.length; i++) {
if (i === max)
continue;
for (const elem of arrSet[i]) {
result.add(elem);
}
}
return result;
}
function intersectSet(arrSet) {
if (!arrSet.length)
return new Set();
let min = 0;
let size = arrSet[0].size;
for (let i = 1; i < arrSet.length; i++) {
if (arrSet[i].size < size) {
min = i;
size = arrSet[i].size;
}
}
const result = new Set();
for (const elem of arrSet[min]) {
let inAll = true;
for (let i = 0; i < arrSet.length; i++) {
if (i === min)
continue;
if (!arrSet[i].has(elem)) {
inAll = false;
break;
}
}
if (inAll)
result.add(elem);
}
return result;
}
async function pathExists(path) {
try {
await fs.access(path);
return true;
} catch(e) {
return false;
}
}
async function appendFileToFile(nameFrom, nameTo) {
return new Promise((resolve, reject) => {
const readStream = fsCB.createReadStream(nameFrom);
readStream.on('error', (err) => {
reject(err);
});
const writeStream = fsCB.createWriteStream(nameTo, {flags: 'a'});
writeStream.on('error', (err) => {
reject(err);
});
writeStream.on('close', () => {
resolve();
});
readStream.pipe(writeStream);
});
}
function esc(obj) {
return JSON.stringify(obj).replace(/@/g, '\\x40');
}
function paramToArray(param) {
return (Array.isArray(param) ? param : [param]);
}
function cloneDeep(obj) {
return JSON.parse(JSON.stringify(obj));
}
//async
function deflate(buf, compressionLevel) {
return new Promise((resolve, reject) => {
zlib.deflateRaw(buf, {level: compressionLevel}, (err, b) => {
if (err)
reject(err);
resolve(b);
});
});
}
//async
function inflate(buf) {
return new Promise((resolve, reject) => {
zlib.inflateRaw(buf, (err, b) => {
if (err)
reject(err);
resolve(b);
});
});
}
module.exports = {
sleep,
sleepWithStop,
unionSet,
intersectSet,
pathExists,
appendFileToFile,
esc,
paramToArray,
cloneDeep,
deflate,
inflate,
};

View File

@@ -8,7 +8,6 @@ const http = require('http');
const WebSocket = require ('ws');
const ayncExit = new (require('./core/AsyncExit'))();
ayncExit.init();
let log = null;

View File

@@ -31,7 +31,6 @@ function initRoutes(app, wss, config) {
['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-finish', worker.getStateFinish.bind(worker), [aAll], {}],
];
//to app