Compare commits

...

166 Commits

Author SHA1 Message Date
Book Pauk
7fa891b4fc Merge branch 'release/0.9.11' 2020-12-09 22:31:33 +07:00
Book Pauk
6cb7412cf3 Версия 0.9.11 2020-12-09 22:30:58 +07:00
Book Pauk
157322834b Небольшая поправка 2020-12-09 22:30:19 +07:00
Book Pauk
1a13a0fee1 Работа над конвертером pdf 2020-12-09 22:19:14 +07:00
Book Pauk
37256255bf Добавлена поддержка тегов 'sup' и 'sub' 2020-12-09 20:35:52 +07:00
Book Pauk
75e01c899e Работа над конвертером pdf 2020-12-09 20:08:17 +07:00
Book Pauk
ef0d6eab89 Работа над конвертером Pdf 2020-12-09 19:05:09 +07:00
Book Pauk
5d54b1b0f4 Работа над конвертером pdf 2020-12-09 03:52:24 +07:00
Book Pauk
522f953b4f Работа над конвертером pdf 2020-12-09 03:06:15 +07:00
Book Pauk
15f02c7115 Работа над конвертером pdf 2020-12-09 01:29:58 +07:00
Book Pauk
174c877eee Рефакторинг, плюс небольшие доработки 2020-12-09 01:29:09 +07:00
Book Pauk
fd9ec736d7 Рефакторинг 2020-12-08 19:36:53 +07:00
Book Pauk
2c94025ba3 Поправлен баг 2020-12-08 19:31:00 +07:00
Book Pauk
bfadf35c40 Закончена работа над xmlParser, оттестировано 2020-12-08 18:48:55 +07:00
Book Pauk
f3b69caa12 Работа над модулем xmlParser 2020-12-08 16:17:36 +07:00
Book Pauk
18a83a5b0b Поправки настроек сжатия 2020-12-08 14:26:49 +07:00
Book Pauk
bd9669b782 Поправка цели dev 2020-12-08 14:26:25 +07:00
Book Pauk
e05713aa7f Работа над конвертером pdf 2020-12-08 14:15:17 +07:00
Book Pauk
bc3e1f0a6f Мелкий рефакторинг 2020-12-07 22:13:14 +07:00
Book Pauk
063d01b5ca Перевод pdf-конвертера на использование pdfalto 2020-12-07 22:05:01 +07:00
Book Pauk
81c38d7749 Мелкий рефакторинг 2020-12-07 20:13:32 +07:00
Book Pauk
a29842b084 Поправка readme 2020-12-07 20:12:37 +07:00
Book Pauk
bb5adcdaf6 Рефакторинг 2020-12-07 01:30:10 +07:00
Book Pauk
537e17a219 Merge tag '0.9.10-5' into develop
0.9.10-5
2020-12-05 13:42:45 +07:00
Book Pauk
03ce50153e Merge branch 'release/0.9.10-5' 2020-12-05 13:42:39 +07:00
Book Pauk
15d01ad7fc Коррекция таймаутов очереди ожидания 2020-12-05 13:41:42 +07:00
Book Pauk
e2b29e2c2f Merge tag '0.9.10-4' into develop
0.9.10-4
2020-12-05 13:25:10 +07:00
Book Pauk
ce7ae84e0f Merge branch 'release/0.9.10-4' 2020-12-05 13:25:06 +07:00
Book Pauk
01eb545f15 Улучшение работы с очередью, поправка багов 2020-12-05 13:24:04 +07:00
Book Pauk
706738c7f1 Merge tag '0.9.10-3' into develop
0.9.10-3
2020-12-05 01:40:37 +07:00
Book Pauk
6afa78cde9 Merge branch 'release/0.9.10-3' 2020-12-05 01:40:30 +07:00
Book Pauk
71f5710bba Увеличен лимит количества файлов для распаковки 2020-12-05 01:12:29 +07:00
Book Pauk
0d87043f91 Поправлен неверный вызов reject 2020-12-05 01:11:31 +07:00
Book Pauk
e25375fb7a Поправка багов 2020-12-05 00:31:53 +07:00
Book Pauk
41822999c8 Небольшие поправки 2020-12-05 00:06:54 +07:00
Book Pauk
07444bc7c2 Добавлена подсказка в сообщение об ошибке 2020-12-04 23:25:34 +07:00
Book Pauk
ec48e5b0b7 Мелкая поправка 2020-12-04 20:14:53 +07:00
Book Pauk
e8e2e9297f Merge tag '0.9.10-2' into develop
0.9.10-2
2020-12-04 20:00:40 +07:00
Book Pauk
4f871dd5ca Merge branch 'release/0.9.10-2' 2020-12-04 20:00:35 +07:00
Book Pauk
f5f07a591a Небольшие доработки конвертирования 2020-12-04 20:00:05 +07:00
Book Pauk
4c11e6918f Merge tag '0.9.10-1' into develop
0.9.10-1
2020-12-04 18:38:02 +07:00
Book Pauk
403b9c0508 Merge branch 'release/0.9.10-1' 2020-12-04 18:37:58 +07:00
Book Pauk
ee8ba75371 Переделывание конвертера Djvu, теперь работает быстрее и без промежуточного конвертирования в pdf 2020-12-04 18:24:08 +07:00
Book Pauk
a2773fb180 Поправил readme 2020-12-04 18:23:57 +07:00
Book Pauk
ca36d588fc Merge tag '0.9.10' into develop
0.9.10
2020-12-03 22:05:47 +07:00
Book Pauk
1e65707b7f Merge branch 'release/0.9.10' 2020-12-03 22:05:42 +07:00
Book Pauk
eddf34ce55 Мелкая поправка 2020-12-03 22:04:47 +07:00
Book Pauk
0fb43aa33c Версия 0.9.10 2020-12-03 21:41:23 +07:00
Book Pauk
b273b02da4 Убрал "k" в статусбаре 2020-12-03 21:37:58 +07:00
Book Pauk
0b997f9673 Поправка приветственного сообщения 2020-12-03 21:37:27 +07:00
Book Pauk
bdb2ae57a8 Добавлена частичная поддержка формата Djvu 2020-12-03 21:30:50 +07:00
Book Pauk
b5e563679a Мелкая поправка 2020-12-03 20:00:22 +07:00
Book Pauk
992c104262 Удален ненужный файл 2020-12-03 19:57:27 +07:00
Book Pauk
555154031e Добавлен запрос persistent storage, увеличен размер кэша книг 2020-12-03 19:53:08 +07:00
Book Pauk
acb083e429 Поправлен readme 2020-12-03 19:30:27 +07:00
Book Pauk
4a527d192d Улучшено управление внешними конвертерами 2020-12-03 19:04:34 +07:00
Book Pauk
39c3bf17dd Добавлен пакет pidusage 2020-12-03 18:43:57 +07:00
Book Pauk
afc8c84f41 Мелкие поправки таймаутов 2020-12-03 12:39:16 +07:00
Book Pauk
a085e04c4d Поправки параметров запуска Rar 2020-12-03 10:24:47 +07:00
Book Pauk
2f82b0db34 Добавлена поддержка rar-архивов с помощью внешнего архиватора 2020-12-02 23:56:17 +07:00
Book Pauk
0124c2b17d Поправка цели dev 2020-12-02 23:55:23 +07:00
Book Pauk
d2cfbbc9f3 Поправлен триггер сокрытия диалога 2020-12-02 22:24:11 +07:00
Book Pauk
c59f48822c Префикс "file://" заменен на "disk://", т.к. порождается конфликт с браузерным протоколом file:// 2020-12-02 22:01:36 +07:00
Book Pauk
b2d6584c4a Merge tag '0.9.9-2' into develop
0.9.9-2
2020-11-24 04:53:10 +07:00
Book Pauk
8f7cafb240 Merge branch 'release/0.9.9-2' 2020-11-24 04:52:55 +07:00
Book Pauk
08fd0f15ff Улучшено распознавание параграфов чистого текста 2020-11-24 04:49:18 +07:00
Book Pauk
dbb1bfe587 Поправки распознавания кодировки fb2-файла 2020-11-24 02:09:17 +07:00
Book Pauk
fe4b7a5a85 Улучшено распознавание xml-формата 2020-11-23 23:49:20 +07:00
Book Pauk
d8df5d76e5 Поправка ридми 2020-11-23 21:57:52 +07:00
Book Pauk
b65dcc5ade Добавлен параметр -nodrm 2020-11-23 21:49:19 +07:00
Book Pauk
a5c387a19e Поправка строки запуска сервера 2020-11-23 21:35:45 +07:00
Book Pauk
07c38d9a9f Поправка для журналирования ошибок конвертера 2020-11-23 21:19:04 +07:00
Book Pauk
20ac8a444b Merge tag '0.9.9-1' into develop
0.9.9-1
2020-11-21 13:36:15 +07:00
Book Pauk
7b601c9c7f Merge branch 'release/0.9.9-1' 2020-11-21 13:36:11 +07:00
Book Pauk
8d2f74daa4 Небольшие доделки в связи с оптимизацией загрузки списка недавних 2020-11-21 13:34:58 +07:00
Book Pauk
01e82dca5f Merge tag '0.9.9' into develop
0.9.9
2020-11-21 04:06:54 +07:00
Book Pauk
094bb407ed Merge branch 'release/0.9.9' 2020-11-21 04:06:49 +07:00
Book Pauk
338baa55ec Версия 0.9.9 2020-11-21 04:06:16 +07:00
Book Pauk
d06d20a33e Оптимизация загрузки и хранения списка недавних книг 2020-11-21 04:03:54 +07:00
Book Pauk
d46ba6b92b Мелкий рефакторинг 2020-11-21 01:13:30 +07:00
Book Pauk
ec2639039d Поправка бага 2020-11-20 21:57:54 +07:00
Book Pauk
3a211ded2e Мелкий рефакторинг, добавлено удобочитаемое имя при сохраненнии fb2 2020-11-20 21:43:37 +07:00
Book Pauk
c2131e3654 Доработки парсинга fb2 - распознаем тег sequence 2020-11-20 21:26:16 +07:00
Book Pauk
594fb59395 Убрал лишнее 2020-11-20 20:45:47 +07:00
Book Pauk
f44378ec84 Рефакторинг 2020-11-20 18:25:24 +07:00
Book Pauk
0f6b366f62 Расширил лог 2020-11-20 16:17:54 +07:00
Book Pauk
8d0a5997ee Небольшая поправка 2020-11-20 16:06:23 +07:00
Book Pauk
347cb3417e Поправки названий и справки 2020-11-19 15:20:40 +07:00
Book Pauk
337fcebd10 Merge tag '0.9.8-5' into develop
0.9.8-5
2020-11-15 14:57:27 +07:00
Book Pauk
e057b130e9 Merge branch 'release/0.9.8-5' 2020-11-15 14:57:23 +07:00
Book Pauk
19a0765a1a Рефакторинг, плюс небольшие доделки 2020-11-15 14:55:02 +07:00
Book Pauk
b81cd3240b Merge tag '0.9.8-4' into develop
0.9.8-4
2020-11-15 00:09:10 +07:00
Book Pauk
1e6105b076 Merge branch 'release/0.9.8-4' 2020-11-15 00:09:03 +07:00
Book Pauk
d8d89b3463 Добавлен показ аннотации в начале книги 2020-11-15 00:07:45 +07:00
Book Pauk
459564cb2d Рефакторинг 2020-11-14 23:43:22 +07:00
Book Pauk
084df35184 Мелкий рефакторинг 2020-11-14 23:32:39 +07:00
Book Pauk
81912babeb Merge tag '0.9.8-3' into develop
0.9.8-3
2020-11-14 23:27:14 +07:00
Book Pauk
3943fc7d95 Merge branch 'release/0.9.8-3' 2020-11-14 23:27:09 +07:00
Book Pauk
3999dc930c Рефакторинг, плюс небольшие изменения внешнего вида 2020-11-14 23:24:20 +07:00
Book Pauk
d4dea16456 Убрал неиспользуемые компоненты 2020-11-14 23:23:51 +07:00
Book Pauk
ed38cb33a5 Merge tag '0.9.8-2' into develop
0.9.8-2
2020-11-14 12:01:44 +07:00
Book Pauk
4cf5a0f4c8 Merge branch 'release/0.9.8-2' 2020-11-14 12:01:39 +07:00
Book Pauk
8f0d526af2 Скрыл хоткей для "Библиотека" в режиме omnireader 2020-11-14 12:00:55 +07:00
Book Pauk
6ca3881841 Merge tag '0.9.8-1' into develop
0.9.8-1
2020-11-14 11:49:06 +07:00
Book Pauk
d8e765a04f Merge branch 'release/0.9.8-1' 2020-11-14 11:49:02 +07:00
Book Pauk
40ff572f94 Добавил проксирование для fantasy-worlds.org 2020-11-14 11:48:03 +07:00
Book Pauk
cc4275dc03 Merge tag '0.9.8' into develop
0.9.8
2020-11-14 00:05:14 +07:00
Book Pauk
b387f4a0db Merge branch 'release/0.9.8' 2020-11-14 00:05:07 +07:00
Book Pauk
1a096031c4 Версия 0.9.8 2020-11-14 00:04:30 +07:00
Book Pauk
83a60b4091 Небольшие поправки парсера 2020-11-13 23:59:55 +07:00
Book Pauk
b292407ec2 Работа над ContentsPage 2020-11-13 23:33:36 +07:00
Book Pauk
952c337b76 Работа над ContentsPage 2020-11-13 22:45:34 +07:00
Book Pauk
e947b887fe Работа над ContentsPage 2020-11-13 19:10:15 +07:00
Book Pauk
bd1e5485d7 Работа над ContentsPage 2020-11-13 18:47:11 +07:00
Book Pauk
e095c3318b Начало работы над ContentsPage 2020-11-13 14:23:55 +07:00
Book Pauk
d75a08b519 Merge tag '0.9.7' into develop
0.9.7
2020-11-12 19:44:15 +07:00
Book Pauk
d55a616fe0 Merge branch 'release/0.9.7' 2020-11-12 19:44:08 +07:00
Book Pauk
2146cb3576 Версия 0.9.7 2020-11-12 19:43:33 +07:00
Book Pauk
ae260e74f6 Поправка механизма чистки TempPublicDir 2020-11-12 19:39:57 +07:00
Book Pauk
355410c03c Исправление бага - не изменялись хоткеи в настройках 2020-11-12 19:13:24 +07:00
Book Pauk
718ad51fac Merge tag '0.9.6-1' into develop
0.9.6-1
2020-11-07 20:57:02 +07:00
Book Pauk
4242a8679f Merge branch 'release/0.9.6-1' 2020-11-07 20:56:57 +07:00
Book Pauk
4ff9ff699b Добавил ссылку на flibs.in в предустановленные закладки 2020-11-07 20:56:07 +07:00
Book Pauk
7a76673274 Merge tag '0.9.6' into develop
0.9.6
2020-11-06 18:00:31 +07:00
Book Pauk
bd03ca5136 Merge branch 'release/0.9.6' 2020-11-06 18:00:22 +07:00
Book Pauk
b3e1e4b909 Версия 0.9.6 2020-11-06 17:59:54 +07:00
Book Pauk
4bb22183df Добавлен диалог с сообщением о новой читалке 2020-11-06 17:57:16 +07:00
Book Pauk
e72f8f4245 Мелкий рефакторинг 2020-11-06 17:36:05 +07:00
Book Pauk
5b52f48bce Поправка для автоматического отображения справки 2020-11-06 16:50:17 +07:00
Book Pauk
f07a157a2a Поправка бага при resize 2020-11-06 01:08:22 +07:00
Book Pauk
ca40854106 Рефакторинг, версия 0.9.6 2020-11-05 22:21:52 +07:00
Book Pauk
d6a8209b31 Работа над ExternalLibs 2020-11-05 21:52:34 +07:00
Book Pauk
731e1f1f15 Мелкая поправка 2020-11-05 20:18:23 +07:00
Book Pauk
b4a2a8fb98 Merge tag '0.9.5-2' into develop
0.9.5-2
2020-11-05 16:58:53 +07:00
Book Pauk
5a3e4ee5ca Merge branch 'release/0.9.5-2' 2020-11-05 16:58:37 +07:00
Book Pauk
ab2cf0aeec Работа над ExternalLibs 2020-11-05 16:56:27 +07:00
Book Pauk
9de6a02b30 Поправка безопасности 2020-11-05 16:42:00 +07:00
Book Pauk
9fb7892bfe Работа над ExternalLibs 2020-11-05 16:40:33 +07:00
Book Pauk
546f4556f6 Мелкий рефакторинг 2020-11-05 15:43:15 +07:00
Book Pauk
471de104bc Работа над ExternalLibs 2020-11-05 15:36:58 +07:00
Book Pauk
d30be1536d Работа над BookmarkSettings 2020-11-05 15:06:36 +07:00
Book Pauk
6c0678ed61 Рефакторинг 2020-11-05 14:08:15 +07:00
Book Pauk
4883b8a190 Работа над ExternalLibs 2020-11-05 03:21:54 +07:00
Book Pauk
14742ed4ad Работа над BookmarkSettings 2020-11-04 22:56:28 +07:00
Book Pauk
1d8bd56862 Поправки багов 2020-11-04 22:23:39 +07:00
Book Pauk
94b8f9fe1c Работа над BookmarkSettings 2020-11-04 19:06:11 +07:00
Book Pauk
27412211a5 Работа над BookmarkSettings 2020-11-04 18:53:14 +07:00
Book Pauk
f8c4960079 Работа над BookmarkSettings 2020-11-04 14:43:06 +07:00
Book Pauk
b2e0bcf995 Улучшение безопасности отображения списка 2020-11-04 14:42:35 +07:00
Book Pauk
fcf6639d38 Небольшие поправки обработки клавиш 2020-11-04 14:41:38 +07:00
Book Pauk
d540cb91a9 Добавлен пакет vue-sanitize 2020-11-04 14:41:11 +07:00
Book Pauk
f69cc6f1b1 Работа над BookmarkSettings 2020-11-03 23:04:06 +07:00
Book Pauk
607f2ff407 Работа над BookmarkSettings 2020-11-03 22:24:32 +07:00
Book Pauk
ba6bf8c091 Работа над BookmarkSettings 2020-11-03 17:03:57 +07:00
Book Pauk
7e4c938dfd Поправлен небольшой баг отображения окна 2020-11-03 14:42:02 +07:00
Book Pauk
7f36d55320 Мелкая поправка 2020-11-03 14:41:32 +07:00
Book Pauk
d9634a134c Обновление caniuse -lite 2020-11-03 00:35:44 +07:00
Book Pauk
4f8868d4b1 Merge tag '0.9.5-1' into develop
0.9.5-1
2020-11-03 00:29:22 +07:00
Book Pauk
956546585c Merge branch 'release/0.9.5-1' 2020-11-03 00:29:12 +07:00
Book Pauk
3ca0a92442 Временно отключил кнопку "Настроить закладки" 2020-11-03 00:28:15 +07:00
Book Pauk
213f7e48c9 Работа над BookmarkSettings 2020-11-03 00:26:25 +07:00
Book Pauk
8b66fd522d Добавлено восстановление дефолтных настроек в объекте settings при их отсутствии 2020-11-03 00:00:31 +07:00
Book Pauk
fdf5009999 Мелкая поправка 2020-11-02 23:51:41 +07:00
Book Pauk
bbdba0ef16 Поправки по результату тестирования 2020-11-02 23:45:59 +07:00
Book Pauk
a63602df7a Поправки багов 2020-11-02 14:33:15 +07:00
Book Pauk
587120f984 Merge tag '0.9.5' into develop
0.9.5
2020-11-01 19:55:53 +07:00
54 changed files with 2965 additions and 833 deletions

View File

@@ -66,7 +66,7 @@ class Reader {
await utils.sleep(refreshPause); await utils.sleep(refreshPause);
i++; i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания'); throw new Error('Слишком долгое время ожидания');
} }
//проверка воркера //проверка воркера
@@ -181,9 +181,8 @@ class Reader {
maxUploadFileSize = 10*1024*1024; maxUploadFileSize = 10*1024*1024;
if (file.size > maxUploadFileSize) if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`); throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData(); let formData = new FormData();
formData.append('file', file); formData.append('file', file, file.name);
const options = { const options = {
headers: { headers: {

View File

@@ -128,6 +128,10 @@ class App extends Vue {
this.setAppTitle(); this.setAppTitle();
(async() => { (async() => {
//запросим persistent storage
if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist();
}
await this.routerReady(); await this.routerReady();
this.redirectIfNeeded(); this.redirectIfNeeded();
})(); })();
@@ -248,7 +252,7 @@ body, html, #app {
} }
.dborder { .dborder {
border: 2px solid yellow !important; border: 2px solid magenta !important;
} }
.icon-rotate { .icon-rotate {

View File

@@ -0,0 +1,349 @@
<template>
<Window ref="window" width="600px" height="95%" @close="close">
<template slot="header">
Настроить закладки
</template>
<div class="col column fit">
<div class="row items-center top-panel bg-grey-3">
<q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip>
</q-btn>
<q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search">
<template v-slot:append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch"/>
</template>
</q-input>
</div>
<div class="col row">
<div class="left-panel column items-center no-wrap bg-grey-3">
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
</q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip>
</q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip>
</q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip>
</q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip>
</q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip>
</q-btn>
<div class="space"/>
</div>
<div class="col fit tree">
<div v-show="nodes.length" class="checkbox-tick-all">
<q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" />
</div>
<q-tree
class="q-my-xs"
:nodes="nodes"
node-key="key"
tick-strategy="leaf"
:selected.sync="selected"
:ticked.sync="ticked"
:expanded.sync="expanded"
selected-color="black"
:filter="search"
no-nodes-label="Закладок пока нет"
no-results-label="Ничего не найдено"
>
<template v-slot:default-header="p">
<div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div>
</template>
</q-tree>
</div>
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import Window from '../../share/Window.vue';
import * as lu from '../linkUtils';
import rstore from '../../../store/modules/reader';
const BookmarkSettingsProps = Vue.extend({
props: {
libs: Object,
addBookmarkVisible: Boolean,
}
});
export default @Component({
components: {
Window,
},
watch: {
ticked: function() {
this.checkAllTicked();
},
}
})
class BookmarkSettings extends BookmarkSettingsProps {
search = '';
selected = '';
ticked = [];
expanded = [];
tickAll = false;
created() {
this.afterInit = true;
}
mounted() {
}
init() {
this.$refs.window.init();
}
get nodes() {
const result = [];
const expanded = [];
this.links = {};
this.libs.groups.forEach(group => {
const rkey = `r-${group.r}`;
const g = {label: group.r, key: rkey, children: []};
this.links[rkey] = {l: group.r, c: ''};
group.list.forEach(link => {
const key = link.l;
g.children.push({
label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l),
key
});
this.links[key] = link;
if (link.l == this.libs.startLink && expanded.indexOf(rkey) < 0) {
expanded.push(rkey);
}
});
result.push(g);
});
if (this.afterInit) {
this.$nextTick(() => {
this.expanded = expanded;
});
this.afterInit = false;
}
return result;
}
makeTickAll() {
if (this.tickAll) {
const newTicked = [];
for (const key of Object.keys(this.links)) {
if (key.indexOf('r-') != 0)
newTicked.push(key);
}
this.ticked = newTicked;
} else {
this.ticked = [];
}
}
checkAllTicked() {
const ticked = new Set(this.ticked);
let newTickAll = !!(this.nodes.length);
for (const key of Object.keys(this.links)) {
if (key.indexOf('r-') != 0 && !ticked.has(key))
newTickAll = false;
}
this.tickAll = newTickAll;
}
resetSearch() {
this.search = '';
this.$refs.search.focus();
}
openSelected() {
if (!this.selected)
return;
if (this.selected.indexOf('r-') === 0) {//rootLink
this.$emit('do-action', {action: 'setRootLink', data: this.links[this.selected].l});
} else {//selectedLink
this.$emit('do-action', {action: 'setSelectedLink', data: this.links[this.selected].l});
}
this.close();
}
editBookmark() {
this.$emit('do-action', {action: 'editBookmark', data: {link: this.links[this.selected].l, desc: this.links[this.selected].c}});
}
addBookmark() {
this.$emit('do-action', {action: 'addBookmark'});
}
async delBookmark() {
const newLibs = _.cloneDeep(this.libs);
if (await this.$root.stdDialog.confirm(`Подтвердите удаление ${this.ticked.length} закладок:`, ' ')) {
const ticked = new Set(this.ticked);
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
const g = newLibs.groups[i];
for (let j = g.list.length - 1; j >= 0; j--) {
if (ticked.has(g.list[j].l)) {
delete g.list[j];
}
}
g.list = g.list.filter(v => v);
if (!g.list.length)
delete newLibs.groups[i];
else {
const item = lu.getListItemByLink(g.list, g.s);
if (!item)
g.s = g.list[0].l;
}
}
newLibs.groups = newLibs.groups.filter(v => v);
this.ticked = [];
this.selected = '';
this.$emit('do-action', {action: 'setLibs', data: newLibs});
}
}
moveBookmark(down = false) {
const newLibs = _.cloneDeep(this.libs);
const ticked = new Set(this.ticked);
let moved = false;
let prevFull = false;
if (!down) {
for (let i = 0; i < newLibs.groups.length; i++) {
const g = newLibs.groups[i];
let count = 0;
for (let j = 0; j < g.list.length; j++) {
if (ticked.has(g.list[j].l)) {
if (j > 0 && !ticked.has(g.list[j - 1].l)) {
[g.list[j], g.list[j - 1]] = [g.list[j - 1], g.list[j]];
moved = true;
}
count++;
}
}
if (count == g.list.length && !prevFull && i > 0) {
const gs = newLibs.groups;
[gs[i], gs[i - 1]] = [gs[i - 1], gs[i]];
moved = true;
} else
prevFull = (count == g.list.length);
}
} else {
for (let i = newLibs.groups.length - 1; i >= 0; i--) {
const g = newLibs.groups[i];
let count = 0;
for (let j = g.list.length - 1; j >= 0; j--) {
if (ticked.has(g.list[j].l)) {
if (j < g.list.length - 1 && !ticked.has(g.list[j + 1].l)) {
[g.list[j], g.list[j + 1]] = [g.list[j + 1], g.list[j]];
moved = true;
}
count++;
}
}
if (count == g.list.length && !prevFull && i < newLibs.groups.length - 1) {
const gs = newLibs.groups;
[gs[i], gs[i + 1]] = [gs[i + 1], gs[i]];
moved = true;
} else
prevFull = (count == g.list.length);
}
}
if (moved)
this.$emit('do-action', {action: 'setLibs', data: newLibs});
}
async setDefaultBookmarks() {
const result = await this.$root.stdDialog.prompt(`Введите 'да' для сброса всех закладок в предустановленные значения:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.$emit('do-action', {action: 'setLibs', data: _.cloneDeep(
Object.assign({helpShowed: true}, rstore.libsDefaults)
)});
}
}
close() {
this.afterInit = false;
this.$emit('close');
}
keyHook(event) {
if (this.addBookmarkVisible)
return false;
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
return true;
}
return false;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.top-panel {
height: 50px;
border-bottom: 1px solid gray;
padding: 0 10px 0 12px;
}
.left-panel {
width: 60px;
height: 100%;
border-right: 1px solid gray;
overflow-x: hidden;
overflow-y: auto;
}
.tree {
padding: 0px 10px 10px 10px;
overflow-x: auto;
overflow-y: auto;
}
.selected {
text-shadow: 0 0 20px yellow, 0 0 15px yellow, 0 0 10px yellow, 0 0 10px yellow, 0 0 5px yellow;
}
.checkbox-tick-all {
border-bottom: 1px solid #bbbbbb;
margin-bottom: 7px;
padding: 5px 5px 2px 16px;
}
.space {
min-height: 1px;
width: 1px;
}
</style>

View File

@@ -9,6 +9,18 @@
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/> <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<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)">
<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">
<q-icon name="la la-question-circle" size="16px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
</span>
</template> </template>
<div v-show="ready" class="col column" style="min-width: 600px"> <div v-show="ready" class="col column" style="min-width: 600px">
@@ -23,12 +35,12 @@
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px"> <q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
</q-btn> </q-btn>
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px" disabled> <q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки (пока недоступно)</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
</q-btn> </q-btn>
</template> </template>
<template v-slot:selected> <template v-slot:selected>
<div style="overflow: hidden; white-space: nowrap;">{{ removeProtocol(rootLink) }}</div> <div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
</template> </template>
</q-select> </q-select>
@@ -44,23 +56,30 @@
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown" @focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(libs.startLink)" size="12px"> <q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
</q-btn> </q-btn>
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px"> <q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
</q-btn> </q-btn>
</template> </template>
<template v-slot:append>
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
</q-btn>
</template>
</q-input> </q-input>
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl">Открыть <q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
</q-btn> </q-btn>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div class="col fit" style="position: relative;"> <div class="col fit" ref="frameBox" style="position: relative;">
<iframe v-if="frameVisible" class="fit" ref="frame" :src="frameSrc" frameborder="0"></iframe> <div ref="frameWrap" class="overflow-hidden">
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
</div>
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div> <div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
</div> </div>
@@ -68,7 +87,8 @@
<template slot="header"> <template slot="header">
<div class="row items-center"> <div class="row items-center">
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon> <q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
Добавить закладку <div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
<div v-else>Добавить закладку</div>
</div> </div>
</template> </template>
@@ -96,7 +116,30 @@
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn> <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
</template> </template>
</Dialog> </Dialog>
<Dialog ref="options" v-model="optionsVisible">
<template slot="header">
<div class="row items-center">
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
Опции
</div> </div>
</template>
<div class="q-mx-md column">
<q-checkbox v-model="closeAfterSubmit" size="36px" label="Закрыть окно при отправке ссылки в читалку" />
<q-checkbox v-model="openInFrameOnEnter" size="36px" label="Открывать ссылку во фрейме при нажатии 'Enter'" />
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
</div>
<template slot="footer">
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
</template>
</Dialog>
</div>
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
@do-action="doAction" @close="closeBookmarkSettings">
</BookmarkSettings>
</Window> </Window>
</template> </template>
@@ -108,58 +151,102 @@ import _ from 'lodash';
import Window from '../share/Window.vue'; import Window from '../share/Window.vue';
import Dialog from '../share/Dialog.vue'; import Dialog from '../share/Dialog.vue';
import BookmarkSettings from './BookmarkSettings/BookmarkSettings.vue';
import rstore from '../../store/modules/reader'; import rstore from '../../store/modules/reader';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import * as lu from './linkUtils';
const proxySubst = { const proxySubst = {
'http://flibusta.is': 'http://b.liberama.top:23480', 'http://flibusta.is': 'http://b.liberama.top:23480',
'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
}; };
export default @Component({ export default @Component({
components: { components: {
Window, Window,
Dialog Dialog,
BookmarkSettings
}, },
watch: { watch: {
libs: function() { libs: function() {
this.loadLibs(); this.loadLibs();
}, },
rootLink: function() {
this.updateSelectedLink();
this.updateStartLink();
},
selectedLink: function() {
this.updateStartLink();
},
defaultRootLink: function() { defaultRootLink: function() {
this.updateBookmarkLink(); this.updateBookmarkLink();
},
bookUrl: function(newValue) {
const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true);
if (value != subst) {
this.$nextTick(() => {
this.bookUrl = subst;
});
} }
},
bookmarkLink: function(newValue) {
const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true);
if (value != subst) {
this.$nextTick(() => {
this.bookmarkLink = subst;
});
}
},
closeAfterSubmit: function(newValue) {
this.commitProp('closeAfterSubmit', newValue);
},
openInFrameOnEnter: function(newValue) {
this.commitProp('openInFrameOnEnter', newValue);
},
openInFrameOnAdd: function(newValue) {
this.commitProp('openInFrameOnAdd', newValue);
},
} }
}) })
class ExternalLibs extends Vue { class ExternalLibs extends Vue {
ready = false; ready = false;
frameVisible = false; frameVisible = false;
startLink = '';
rootLink = ''; rootLink = '';
selectedLink = ''; selectedLink = '';
frameSrc = ''; frameSrc = '';
bookUrl = ''; bookUrl = '';
libs = {}; libs = {};
fullScreenActive = false; fullScreenActive = false;
addBookmarkVisible = false;
transparentLayoutVisible = false; transparentLayoutVisible = false;
addBookmarkVisible = false;
optionsVisible = false;
addBookmarkMode = '';
bookmarkLink = ''; bookmarkLink = '';
bookmarkDesc = ''; bookmarkDesc = '';
defaultRootLink = ''; defaultRootLink = '';
bookmarkSettingsActive = false;
closeAfterSubmit = false;
openInFrameOnEnter = false;
openInFrameOnAdd = false;
frameScale = 1;
created() { created() {
this.oldStartLink = '';
this.justOpened = true;
this.$root.addKeyHook(this.keyHook); this.$root.addKeyHook(this.keyHook);
this.$root.$on('resize', async() => {
await utils.sleep(200);
this.frameResize();
});
document.addEventListener('fullscreenchange', () => { document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null); this.fullScreenActive = (document.fullscreenElement !== null);
}); });
this.debouncedGoToLink = _.debounce((link) => {
this.goToLink(link);
}, 100, {'maxWait':200});
//this.commit = this.$store.commit; //this.commit = this.$store.commit;
//this.commit('reader/setLibs', rstore.libsDefaults); //this.commit('reader/setLibs', rstore.libsDefaults);
} }
@@ -252,8 +339,6 @@ class ExternalLibs extends Vue {
} else if (d.type == 'libs') { } else if (d.type == 'libs') {
this.ready = true; this.ready = true;
this.libs = _.cloneDeep(d.data); this.libs = _.cloneDeep(d.data);
if (!this.frameSrc)
this.goToLink(this.libs.startLink);
} else if (d.type == 'notify') { } else if (d.type == 'notify') {
this.$root.notify.success(d.data, '', {position: 'bottom-right'}); this.$root.notify.success(d.data, '', {position: 'bottom-right'});
} }
@@ -278,11 +363,44 @@ class ExternalLibs extends Vue {
this.sendMessage({type: 'libs', data: libs}); this.sendMessage({type: 'libs', data: libs});
} }
commitProp(prop, value) {
let libs = _.cloneDeep(this.libs);
libs[prop] = value;
this.commitLibs(libs);
}
loadLibs() { loadLibs() {
const libs = this.libs; const libs = this.libs;
this.startLink = (libs.comment ? libs.comment + ' ': '') + this.removeProtocol(libs.startLink);
this.rootLink = this.getOrigin(libs.startLink); if (!libs.helpShowed) {
this.updateSelectedLink(); this.showHelp();
(async() => {
await utils.sleep(1000);
this.commitProp('helpShowed', true);
})();
}
this.selectedLink = libs.startLink;
this.closeAfterSubmit = libs.closeAfterSubmit || false;
this.openInFrameOnEnter = libs.openInFrameOnEnter || false;
this.openInFrameOnAdd = libs.openInFrameOnAdd || false;
this.frameScale = 1;
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
if (index >= 0)
this.frameScale = this.libs.groups[index].frameScale || 1;
this.updateStartLink();
}
doAction(event) {
switch (event.action) {
case 'setLibs': this.commitLibs(event.data); break;
case 'setRootLink': this.rootLink = event.data; this.rootLinkInput(); break;
case 'setSelectedLink': this.selectedLink = event.data; this.selectedLinkInput(); break;
case 'editBookmark': this.addBookmark('edit', event.data.link, event.data.desc); break;
case 'addBookmark': this.addBookmark('add'); break;
}
} }
get mode() { get mode() {
@@ -290,34 +408,62 @@ class ExternalLibs extends Vue {
} }
get header() { get header() {
let result = (this.ready ? 'Библиотека' : 'Загрузка...'); let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
if (this.ready && this.startLink) { if (this.ready && this.selectedLink) {
result += ` | ${this.startLink}`; result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
} }
this.$root.$emit('set-app-title', result); this.$root.$emit('set-app-title', result);
return result; return result;
} }
updateSelectedLink() { get rootLinkWithoutProtocol() {
if (!this.ready) return lu.removeProtocol(this.rootLink);
return;
const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
if (index >= 0)
this.selectedLink = this.libs.groups[index].s;
} }
updateStartLink() { updateSelectedLinkByRoot() {
if (!this.ready) if (!this.ready)
return; return;
const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink);
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
if (index >= 0)
this.selectedLink = this.libs.groups[index].s;
else
this.selectedLink = '';
}
updateStartLink(force) {
if (!this.ready)
return;
let index = -1;
try {
this.rootLink = lu.getOrigin(this.selectedLink);
index = lu.getRootIndexByUrl(this.libs.groups, this.rootLink);
} catch(e) {
//
}
if (index >= 0) { if (index >= 0) {
let libs = _.cloneDeep(this.libs); let libs = _.cloneDeep(this.libs);
const com = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
if (libs.groups[index].s != this.selectedLink ||
libs.startLink != this.selectedLink ||
libs.comment != com) {
libs.groups[index].s = this.selectedLink; libs.groups[index].s = this.selectedLink;
libs.startLink = this.selectedLink; libs.startLink = this.selectedLink;
libs.comment = this.getCommentByLink(libs.groups[index].list, this.selectedLink); libs.comment = com;
this.goToLink(this.selectedLink);
this.commitLibs(libs); this.commitLibs(libs);
} }
if (force || this.oldStartLink != libs.startLink) {
this.oldStartLink = libs.startLink;
this.debouncedGoToLink(this.selectedLink);
}
} else {
this.rootLink = '';
this.selectedLink = '';
this.debouncedGoToLink(this.selectedLink);
}
} }
get rootLinkOptions() { get rootLinkOptions() {
@@ -326,7 +472,7 @@ class ExternalLibs extends Vue {
return result; return result;
this.libs.groups.forEach(group => { this.libs.groups.forEach(group => {
result.push({label: this.removeProtocol(group.r), value: group.r}); result.push({label: lu.removeProtocol(group.r), value: group.r});
}); });
return result; return result;
@@ -336,7 +482,7 @@ class ExternalLibs extends Vue {
let result = []; let result = [];
rstore.libsDefaults.groups.forEach(group => { rstore.libsDefaults.groups.forEach(group => {
result.push({label: this.removeProtocol(group.r), value: group.r}); result.push({label: lu.removeProtocol(group.r), value: group.r});
}); });
return result; return result;
@@ -347,10 +493,10 @@ class ExternalLibs extends Vue {
if (!this.ready) if (!this.ready)
return result; return result;
const index = this.getRootIndexByUrl(this.libs.groups, this.rootLink); const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
if (index >= 0) { if (index >= 0) {
this.libs.groups[index].list.forEach(link => { this.libs.groups[index].list.forEach(link => {
result.push({label: (link.c ? link.c + ' ': '') + this.removeOrigin(link.l), value: link.l}); result.push({label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l), value: link.l});
}); });
} }
@@ -359,66 +505,69 @@ class ExternalLibs extends Vue {
openBookUrlInFrame() { openBookUrlInFrame() {
if (this.bookUrl) { if (this.bookUrl) {
this.goToLink(this.addProtocol(this.bookUrl)); this.goToLink(lu.addProtocol(this.bookUrl));
} }
} }
goToLink(link) { goToLink(link) {
if (!this.ready) if (!this.ready || !link)
return; return;
if (!link) {
this.frameVisible = false;
return;
}
this.frameSrc = this.makeProxySubst(link); this.frameSrc = this.makeProxySubst(link);
this.frameVisible = false; this.frameVisible = false;
this.$nextTick(() => { this.$nextTick(() => {
this.frameVisible = true; this.frameVisible = true;
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.frame) {
this.$refs.frame.contentWindow.focus(); this.$refs.frame.contentWindow.focus();
this.frameResize();
}
}); });
}); });
} }
addProtocol(url) { frameResize() {
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0)) this.$refs.frameWrap.style = 'width: 1px; height: 1px;';
return 'http://' + url; this.$nextTick(() => {
return url; if (this.$refs.frame) {
const w = this.$refs.frameBox.offsetWidth;
const h = this.$refs.frameBox.offsetHeight;
const normalSize = `width: ${w}px; height: ${h}px;`;
this.$refs.frameWrap.style = normalSize;
if (this.frameScale != 1) {
const s = this.frameScale;
this.$refs.frame.style = `width: ${w/s}px; height: ${h/s}px; transform: scale(${s}); transform-origin: 0 0;`;
} else {
this.$refs.frame.style = normalSize;
}
}
});
} }
removeProtocol(url) { changeScale(delta) {
return url.replace(/(^\w+:|^)\/\//, ''); if ((this.frameScale > 0.1 && delta <= 0) || (this.frameScale < 5 && delta >= 0)) {
this.frameScale = _.round(this.frameScale + delta, 1);
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
if (index >= 0) {
let libs = _.cloneDeep(this.libs);
libs.groups[index].frameScale = this.frameScale;
this.commitLibs(libs);
} }
getOrigin(url) { this.frameResize();
const parsed = new URL(url); this.$root.notify.success(`Масштаб изменен: ${(this.frameScale*100).toFixed(0)}%`, '', {position: 'bottom-right'});
return parsed.origin;
} }
removeOrigin(url) {
const parsed = new URL(url);
const result = url.substring(parsed.origin.length);
return (result ? result : '/');
}
getRootIndexByUrl(groups, url) {
if (!this.ready)
return -1;
const origin = this.getOrigin(url);
for (let i = 0; i < groups.length; i++) {
if (groups[i].r == origin)
return i;
}
return -1;
}
getListItemByLink(list, link) {
for (const item of list) {
if (item.l == link)
return item;
}
return null;
} }
getCommentByLink(list, link) { getCommentByLink(list, link) {
const item = this.getListItemByLink(list, link); const item = lu.getListItemByLink(list, link);
return (item ? item.c : ''); return (item ? item.c : '');
} }
@@ -426,7 +575,7 @@ class ExternalLibs extends Vue {
for (const [key, value] of Object.entries(proxySubst)) { for (const [key, value] of Object.entries(proxySubst)) {
if (reverse && value == url.substring(0, value.length)) { if (reverse && value == url.substring(0, value.length)) {
return key + url.substring(value.length); return key + url.substring(value.length);
} else if (key == url.substring(0, key.length)) { } else if (!reverse && key == url.substring(0, key.length)) {
return value + url.substring(key.length); return value + url.substring(key.length);
} }
} }
@@ -440,29 +589,37 @@ class ExternalLibs extends Vue {
} }
rootLinkInput() { rootLinkInput() {
this.updateSelectedLink(); this.updateSelectedLinkByRoot();
this.updateStartLink(); this.updateStartLink(true);
} }
selectedLinkInput() { selectedLinkInput() {
this.updateStartLink(); this.updateStartLink(true);
} }
submitUrl() { submitUrl() {
if (this.bookUrl) { if (this.bookUrl) {
this.sendMessage({type: 'submitUrl', data: { this.sendMessage({type: 'submitUrl', data: {
url: this.makeProxySubst(this.addProtocol(this.bookUrl), true), url: this.bookUrl,
force: true force: true
}}); }});
this.bookUrl = ''; this.bookUrl = '';
if (this.libs.closeAfterSubmit) if (this.closeAfterSubmit)
this.close(); this.close();
} }
} }
addBookmark() { addBookmark(mode = 'add', link = '', desc = '') {
this.bookmarkLink = (this.bookUrl ? this.makeProxySubst(this.addProtocol(this.bookUrl), true) : '');
if (mode == 'edit') {
this.editBookmarkLink = this.bookmarkLink = link;
this.editBookmarkDesc = this.bookmarkDesc = desc;
} else {
this.bookmarkLink = this.bookUrl;
this.bookmarkDesc = ''; this.bookmarkDesc = '';
}
this.addBookmarkMode = mode;
this.addBookmarkVisible = true; this.addBookmarkVisible = true;
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.bookmarkLink.focus(); this.$refs.bookmarkLink.focus();
@@ -471,7 +628,7 @@ class ExternalLibs extends Vue {
} }
updateBookmarkLink() { updateBookmarkLink() {
const index = this.getRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink); const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
if (index >= 0) { if (index >= 0) {
this.bookmarkLink = rstore.libsDefaults.groups[index].s; this.bookmarkLink = rstore.libsDefaults.groups[index].s;
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink); this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
@@ -494,8 +651,9 @@ class ExternalLibs extends Vue {
bookmarkDescKeyDown(event) { bookmarkDescKeyDown(event) {
if (event.key == 'Enter') { if (event.key == 'Enter') {
this.okAddBookmark(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
this.okAddBookmark();
} }
} }
@@ -503,55 +661,82 @@ class ExternalLibs extends Vue {
if (!this.bookmarkLink) if (!this.bookmarkLink)
return; return;
const link = this.addProtocol(this.bookmarkLink); const link = (this.addBookmarkMode == 'edit' ? lu.addProtocol(this.editBookmarkLink) : lu.addProtocol(this.bookmarkLink));
let index = -1; let index = -1;
try { try {
index = this.getRootIndexByUrl(this.libs.groups, link); index = lu.getRootIndexByUrl(this.libs.groups, link);
} catch (e) { } catch (e) {
await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка'); await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
return; return;
} }
//есть группа в закладках
if (index >= 0) {
const item = this.getListItemByLink(this.libs.groups[index].list, link);
if (!item || item.c != this.bookmarkDesc) {
//добавляем
let libs = _.cloneDeep(this.libs); let libs = _.cloneDeep(this.libs);
//добавление
//есть группа в закладках
if (index >= 0) {
const item = lu.getListItemByLink(libs.groups[index].list, link);
//редактирование
if (item && this.addBookmarkMode == 'edit') {
if (item) {
//редактируем
item.l = link;
item.c = this.bookmarkDesc;
this.commitLibs(libs);
} else {
await this.$root.stdDialog.alert('Не удалось отредактировать закладку', 'Ошибка');
}
} else if (!item) {
//добавляем
if (libs.groups[index].list.length >= 100) { if (libs.groups[index].list.length >= 100) {
await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка'); await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
return; return;
} }
libs.groups[index].list.push({l: link, c: this.bookmarkDesc}); libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
if (this.openInFrameOnAdd) {
libs.startLink = link;
libs.comment = this.bookmarkDesc;
}
this.commitLibs(libs); this.commitLibs(libs);
} else if (item.c != this.bookmarkDesc) {
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
item.c = this.bookmarkDesc;
this.commitLibs(libs);
} else
return;
} else {
await this.$root.stdDialog.alert('Такая закладка уже существует', ' ');
return;
} }
} else {//нет группы в закладках } else {//нет группы в закладках
let libs = _.cloneDeep(this.libs);
if (libs.groups.length >= 100) { if (libs.groups.length >= 100) {
await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка'); await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
return; return;
} }
//добавляем сначала группу //добавляем сначала группу
libs.groups.push({r: this.getOrigin(link), s: link, list: []}); libs.groups.push({r: lu.getOrigin(link), s: link, list: []});
index = this.getRootIndexByUrl(libs.groups, link); index = lu.getSafeRootIndexByUrl(libs.groups, link);
if (index >= 0) if (index >= 0)
libs.groups[index].list.push({l: link, c: this.bookmarkDesc}); libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
if (this.openInFrameOnAdd) {
libs.startLink = link;
libs.comment = this.bookmarkDesc;
}
this.commitLibs(libs); this.commitLibs(libs);
} }
this.addBookmarkVisible = false; this.addBookmarkVisible = false;
} }
bookmarkSettings() {
}
fullScreenToggle() { fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive; this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) { if (this.fullScreenActive) {
@@ -579,19 +764,65 @@ class ExternalLibs extends Vue {
bookUrlKeyDown(event) { bookUrlKeyDown(event) {
if (event.key == 'Enter') { if (event.key == 'Enter') {
if (!this.openInFrameOnEnter) {
this.submitUrl(); this.submitUrl();
} else {
if (this.bookUrl)
this.goToLink(this.bookUrl);
}
event.preventDefault(); event.preventDefault();
} }
} }
keyHook() { bookmarkSettings() {
this.bookmarkSettingsActive = true;
this.$nextTick(() => {
this.$refs.bookmarkSettings.init();
});
}
closeBookmarkSettings() {
this.bookmarkSettingsActive = false;
}
showHelp() {
this.$root.stdDialog.alert(`
<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
к сожалению, в нем открываются не все страницы.</p>
<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
к третьим лицам.
</p>
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
</p>
<p>Приятного пользования ;-)
</p>
`, 'Справка', {iconName: 'la la-info-circle'});
}
keyHook(event) {
if (this.$root.rootRoute() == '/external-libs') { if (this.$root.rootRoute() == '/external-libs') {
if (this.$refs.dialogAddBookmark.active) if (this.$root.stdDialog.active)
return false;
if (this.bookmarkSettingsActive && this.$refs.bookmarkSettings.keyHook(event))
return true;
if (this.addBookmarkVisible || this.optionsVisible)
return false; return false;
if (event.type == 'keydown' && event.key == 'F4') { if (event.type == 'keydown' && event.key == 'F4') {
this.addBookmark() this.addBookmark();
return; return true;
} }
if (event.type == 'keydown' && event.key == 'Escape' && if (event.type == 'keydown' && event.key == 'Escape' &&
@@ -599,12 +830,11 @@ class ExternalLibs extends Vue {
(document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu) (document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu)
) { ) {
this.close(); this.close();
}
return true; return true;
} }
}
return false; return false;
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -0,0 +1,48 @@
export function addProtocol(url) {
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0))
return 'http://' + url;
return url;
}
export function removeProtocol(url) {
return url.replace(/(^\w+:|^)\/\//, '');
}
export function getOrigin(url) {
const parsed = new URL(url);
return parsed.origin;
}
export function removeOrigin(url) {
const parsed = new URL(url);
const result = url.substring(parsed.origin.length);
return (result ? result : '/');
}
export function getRootIndexByUrl(groups, url) {
const origin = getOrigin(url);
for (let i = 0; i < groups.length; i++) {
if (groups[i].r == origin)
return i;
}
return -1;
}
export function getSafeRootIndexByUrl(groups, url) {
let index = -1;
try {
index = getRootIndexByUrl(groups, url);
} catch(e) {
//
}
return index;
}
export function getListItemByLink(list, link) {
for (const item of list) {
if (item.l == link)
return item;
}
return null;
}

View File

@@ -0,0 +1,247 @@
<template>
<Window width="600px" ref="window" @close="close">
<template slot="header">
Оглавление/закладки
</template>
<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="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="contents" icon="la la-list" label="Оглавление" />
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
</q-tabs>
</div>
<div class="q-mb-sm"/>
<div class="tab-panel" v-show="selectedTab == 'contents'">
<div>
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<div class="row item q-px-sm no-wrap">
<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"/>
</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"/>
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">{{ item.perc }}%</div>
</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 subitem q-px-sm no-wrap">
<div class="col row clickable" @click="setBookPos(subitem.offset)">
<div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<div class="column justify-center">{{ subitem.perc }}%</div>
</div>
</div>
</div>
</div>
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
Оглавление отсутствует
</div>
</div>
</div>
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
<div class="column justify-center items-center" style="height: 100px">
Раздел находится в разработке
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
//import _ from 'lodash';
import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils';
export default @Component({
components: {
Window,
},
watch: {
},
})
class ContentsPage extends Vue {
selectedTab = 'contents';
contents = [];
created() {
}
async init(currentBook, parsed) {
this.$refs.window.init();
//закладки
//далее формаирование оглавления
if (this.parsed == parsed)
return;
this.parsed = parsed;
this.contents = [];
await this.$nextTick();
const pc = parsed.contents;
const newpc = [];
//преобразуем все, кроме первого, разделы body в title-subtitle
let curSubtitles = [];
let prevBodyIndex = -1;
for (let i = 0; i < pc.length; i++) {
const cont = pc[i];
if (prevBodyIndex != cont.bodyIndex)
curSubtitles = [];
prevBodyIndex = cont.bodyIndex;
if (cont.bodyIndex > 1) {
if (cont.inset < 1) {
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
} else {
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
}
} else {
newpc.push(cont);
}
}
const prepareLabel = (title, bolder = false) => {
let titleParts = title.split('<p>');
const textParts = titleParts.filter(v => v).map(v => `<div>${utils.removeHtmlTags(v)}</div>`);
if (bolder && textParts.length > 1)
textParts[0] = `<b>${textParts[0]}</b>`;
return textParts.join('');
}
const getIndentStyle = inset => `width: ${inset*20}px`;
const getLabelStyle = (inset) => {
const fontSizes = ['110%', '100%', '90%', '85%'];
inset = (inset > 3 ? 3 : inset);
return `font-size: ${fontSizes[inset]}`;
};
//формируем newContents
let i = 0;
const newContents = [];
newpc.forEach((cont) => {
const label = prepareLabel(cont.title, true);
const indentStyle = getIndentStyle(cont.inset);
const labelStyle = getLabelStyle(cont.inset);
let j = 0;
const list = [];
cont.subtitles.forEach((sub) => {
const l = prepareLabel(sub.title);
const s = getIndentStyle(sub.inset + 1);
const ls = getLabelStyle(cont.inset + 1);
const p = parsed.para[sub.paraIndex];
list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s, labelStyle: ls};
j++;
});
const p = parsed.para[cont.paraIndex];
newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
i++;
});
this.contents = newContents;
}
async expandClick(key) {
const item = this.contents[key];
const expanded = !item.expanded;
if (!expanded) {
const subitems = this.$refs[`subitem${key}`][0];
subitems.style.height = '0';
await utils.sleep(200);
}
this.$set(this.contents, key, Object.assign({}, item, {expanded}));
if (expanded) {
await this.$nextTick();
const subitems = this.$refs[`subitem${key}`][0];
subitems.style.height = subitems.scrollHeight + 'px';
}
}
async setBookPos(newValue) {
this.$emit('book-pos-changed', {bookPos: newValue});
this.close();
}
close() {
this.$emit('do-action', {action: 'contents'});
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.tab-panel {
overflow-x: hidden;
overflow-y: auto;
font-size: 90%;
padding: 0 10px 0px 10px;
}
.clickable {
cursor: pointer;
padding: 10px 0 10px 0;
}
.item, .subitem {
border-bottom: 1px solid #e0e0e0;
}
.item:hover, .subitem:hover {
background-color: #f0f0f0;
}
.expand-button, .no-expand-button {
width: 40px;
}
.subitems-transition {
height: 0;
transition: height 0.2s linear;
overflow: hidden;
}
.icon {
transition: transform 0.2s;
}
.expanded-icon {
transform: rotate(90deg);
}
</style>

View File

@@ -7,8 +7,8 @@
<span class="greeting"><b>{{ title }}</b></span> <span class="greeting"><b>{{ title }}</b></span>
<div class="q-my-sm"></div> <div class="q-my-sm"></div>
<span class="greeting">Добро пожаловать!</span> <span class="greeting">Добро пожаловать!</span>
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span> <span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz<span v-if="isExternalConverter">, rar</span></b></span>
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span> <span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
</div> </div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden"> <div class="col-auto column justify-start items-center no-wrap overflow-hidden">

View File

@@ -48,6 +48,10 @@
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
</button> </button>
<div class="space"></div> <div class="space"></div>
<button ref="contents" v-show="showToolButton['contents']" class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')" v-ripple>
<q-icon name="la la-list" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['contents'] }}</q-tooltip>
</button>
<button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple> <button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
<q-icon name="la la-sitemap" size="32px"/> <q-icon name="la la-sitemap" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
@@ -89,71 +93,15 @@
@stop-text-search="stopTextSearch"> @stop-text-search="stopTextSearch">
</SearchPage> </SearchPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage> <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
<LibsPage v-show="libsActive" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage> <LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage> <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage> <SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage> <HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage> <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage> <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<ContentsPage v-show="contentsActive" ref="contentsPage" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
<Dialog ref="dialog1" v-model="whatsNewVisible"> <ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
<template slot="header">
Что нового:
</template>
<div style="line-height: 20px" v-html="whatsNewContent"></div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
</span>
</Dialog>
<Dialog ref="dialog2" v-model="donationVisible">
<template slot="header">
Здравствуйте, уважаемые читатели!
</template>
<div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div>
<span slot="footer">
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog>
</div> </div>
</div> </div>
</template> </template>
@@ -178,13 +126,14 @@ import SettingsPage from './SettingsPage/SettingsPage.vue';
import HelpPage from './HelpPage/HelpPage.vue'; import HelpPage from './HelpPage/HelpPage.vue';
import ClickMapPage from './ClickMapPage/ClickMapPage.vue'; import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
import ServerStorage from './ServerStorage/ServerStorage.vue'; import ServerStorage from './ServerStorage/ServerStorage.vue';
import Dialog from '../share/Dialog.vue'; import ContentsPage from './ContentsPage/ContentsPage.vue';
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager'; import bookManager from './share/bookManager';
import rstore from '../../store/modules/reader'; import rstore from '../../store/modules/reader';
import readerApi from '../../api/reader'; import readerApi from '../../api/reader';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import {versionHistory} from './versionHistory';
export default @Component({ export default @Component({
components: { components: {
@@ -201,7 +150,9 @@ export default @Component({
HelpPage, HelpPage,
ClickMapPage, ClickMapPage,
ServerStorage, ServerStorage,
Dialog, ContentsPage,
ReaderDialogs,
}, },
watch: { watch: {
bookPos: function(newValue) { bookPos: function(newValue) {
@@ -258,6 +209,7 @@ class Reader extends Vue {
settingsActive = false; settingsActive = false;
helpActive = false; helpActive = false;
clickMapActive = false; clickMapActive = false;
contentsActive = false;
bookPos = null; bookPos = null;
allowUrlParamBookPos = false; allowUrlParamBookPos = false;
@@ -298,11 +250,11 @@ class Reader extends Vue {
await this.$nextTick(); await this.$nextTick();
this.paramPosIgnore = false; this.paramPosIgnore = false;
} }
}, 500, {'maxWait':5000}); }, 500, {maxWait: 5000});
this.scrollingSetRecentBook = _.debounce((newValue) => { this.scrollingSetRecentBook = _.debounce((newValue) => {
this.debouncedSetRecentBook(newValue); this.debouncedSetRecentBook(newValue);
}, 15000, {'maxWait':20000}); }, 15000, {maxWait: 20000});
document.addEventListener('fullscreenchange', () => { document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null); this.fullScreenActive = (document.fullscreenElement !== null);
@@ -331,10 +283,15 @@ class Reader extends Vue {
this.checkActivateDonateHelpPage(); this.checkActivateDonateHelpPage();
this.loading = false; this.loading = false;
//проверим состояние Settings, и обновим, если надо
const newSettings = rstore.addDefaultsToSettings(this.settings);
if (newSettings) {
this.commit('reader/setSettings', newSettings);
}
this.updateRoute(); this.updateRoute();
await this.showWhatsNew(); await this.$refs.dialogs.init();
await this.showDonation();
})(); })();
} }
@@ -345,8 +302,6 @@ class Reader extends Vue {
this.showClickMapPage = settings.showClickMapPage; this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl; this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showToolButton = settings.showToolButton; this.showToolButton = settings.showToolButton;
this.enableSitesFilter = settings.enableSitesFilter; this.enableSitesFilter = settings.enableSitesFilter;
@@ -410,64 +365,6 @@ class Reader extends Vue {
} }
} }
async showWhatsNew() {
await utils.sleep(2000);
const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) {
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
this.whatsNewVisible = true;
}
}
async showDonation() {
await utils.sleep(3000);
const today = utils.formatDate(new Date(), 'coDate');
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
this.donationVisible = true;
}
}
donationDialogDisable() {
this.donationVisible = false;
if (this.showDonationDialog2020) {
const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
this.commit('reader/setSettings', newSettings);
}
}
donationDialogRemind() {
this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
}
openDonate() {
this.donationVisible = false;
this.donateToggle();
}
async copyLink(link) {
const result = await utils.copyTextToClipboard(link);
if (result)
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
else
this.$root.notify.error('Копирование не удалось');
}
openVersionHistory() {
this.whatsNewVisible = false;
this.versionHistoryToggle();
}
whatsNewDisable() {
this.whatsNewVisible = false;
const whatsNew = versionHistory[0];
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
}
get routeParamPos() { get routeParamPos() {
let result = undefined; let result = undefined;
const q = this.$route.query; const q = this.$route.query;
@@ -571,14 +468,6 @@ class Reader extends Vue {
return this.$store.state.reader.settings; return this.$store.state.reader.settings;
} }
get whatsNewContentHash() {
return this.$store.state.reader.whatsNewContentHash;
}
get donationRemindDate() {
return this.$store.state.reader.donationRemindDate;
}
addAction(pos) { addAction(pos) {
let a = this.actionList; let a = this.actionList;
if (!a.length || a[a.length - 1] != pos) { if (!a.length || a[a.length - 1] != pos) {
@@ -611,6 +500,7 @@ class Reader extends Vue {
this.stopScrolling(); this.stopScrolling();
this.stopSearch(); this.stopSearch();
this.helpActive = false; this.helpActive = false;
this.contentsActive = false;
} }
loaderToggle() { loaderToggle() {
@@ -703,12 +593,6 @@ class Reader extends Vue {
} }
} }
refreshBookSplitToPara() {
if (this.mostRecentBook()) {
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
}
}
recentBooksClose() { recentBooksClose() {
this.recentBooksActive = false; this.recentBooksActive = false;
} }
@@ -724,6 +608,21 @@ class Reader extends Vue {
} }
} }
contentsPageToggle() {
this.contentsActive = !this.contentsActive;
const page = this.$refs.page;
if (this.contentsActive && this.activePage == 'TextPage' && page.parsed) {
this.closeAllWindows();
this.contentsActive = true;
this.$nextTick(() => {
this.$refs.contentsPage.init(this.mostRecentBook(), page.parsed);
});
} else {
this.contentsActive = false;
}
}
libsClose() { libsClose() {
if (this.libsActive) if (this.libsActive)
this.libsToogle(); this.libsToogle();
@@ -783,9 +682,14 @@ class Reader extends Vue {
} }
} }
refreshBook() { refreshBook(mode) {
if (this.mostRecentBook()) { const mrb = this.mostRecentBook();
this.loadBook({url: this.mostRecentBook().url, force: true}); if (mrb) {
if (mode && mode == 'split') {
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, skipCheck: true, isText: true, force: true});
} else {
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
}
} }
} }
@@ -828,6 +732,7 @@ class Reader extends Vue {
case 'copyText': case 'copyText':
case 'splitToPara': case 'splitToPara':
case 'refresh': case 'refresh':
case 'contents':
case 'libs': case 'libs':
case 'recentBooks': case 'recentBooks':
case 'offlineMode': case 'offlineMode':
@@ -856,6 +761,7 @@ class Reader extends Vue {
case 'scrolling': case 'scrolling':
case 'search': case 'search':
case 'copyText': case 'copyText':
case 'contents':
classResult = classDisabled; classResult = classDisabled;
break; break;
case 'splitToPara': case 'splitToPara':
@@ -939,8 +845,12 @@ class Reader extends Vue {
let url = encodeURI(decodeURI(opts.url)); let url = encodeURI(decodeURI(opts.url));
//TODO: убрать конвертирование 'file://' после 06.2021
if (url.length == 71 && url.indexOf('file://') == 0)
url = url.replace(/^file/, 'disk');
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) && if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('file://') != 0)) (url.indexOf('disk://') != 0))
url = 'http://' + url; url = 'http://' + url;
// уже просматривается сейчас // уже просматривается сейчас
@@ -971,6 +881,7 @@ class Reader extends Vue {
wasOpened = (wasOpened ? wasOpened : {}); wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos); const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen); const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
let book = null; let book = null;
@@ -1017,7 +928,8 @@ class Reader extends Vue {
url, url,
skipCheck: (opts.skipCheck ? true : false), skipCheck: (opts.skipCheck ? true : false),
isText: (opts.isText ? true : false), isText: (opts.isText ? true : false),
enableSitesFilter: this.enableSitesFilter enableSitesFilter: this.enableSitesFilter,
uploadFileName
}, },
(state) => { (state) => {
progress.setState(state); progress.setState(state);
@@ -1033,7 +945,7 @@ class Reader extends Vue {
}); });
// добавляем в историю // добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook)); await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
this.mostRecentBook(); this.mostRecentBook();
this.addAction(bookPos); this.addAction(bookPos);
this.updateRoute(true); this.updateRoute(true);
@@ -1070,7 +982,7 @@ class Reader extends Vue {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
await this.loadBook({url}); await this.loadBook({url, uploadFileName: opts.file.name, force: true});
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
@@ -1142,11 +1054,14 @@ class Reader extends Vue {
this.copyTextToggle(); this.copyTextToggle();
break; break;
case 'splitToPara': case 'splitToPara':
this.refreshBookSplitToPara(); this.refreshBook('split');
break; break;
case 'refresh': case 'refresh':
this.refreshBook(); this.refreshBook();
break; break;
case 'contents':
this.contentsPageToggle();
break;
case 'libs': case 'libs':
this.libsToogle(); this.libsToogle();
break; break;
@@ -1222,9 +1137,12 @@ class Reader extends Vue {
keyHook(event) { keyHook(event) {
let result = false; let result = false;
if (this.$root.rootRoute() == '/reader') { if (this.$root.rootRoute() == '/reader') {
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active) if (this.$root.stdDialog.active)
return result; return result;
if (!result)
result = this.$refs.dialogs.keyHook(event);
if (!result && this.helpActive) if (!result && this.helpActive)
result = this.$refs.helpPage.keyHook(event); result = this.$refs.helpPage.keyHook(event);
@@ -1243,6 +1161,9 @@ class Reader extends Vue {
if (!result && this.copyTextActive) if (!result && this.copyTextActive)
result = this.$refs.copyTextPage.keyHook(event); result = this.$refs.copyTextPage.keyHook(event);
if (!result && this.contentsActive)
result = this.$refs.contentsPage.keyHook(event);
if (!result && this.$refs.page && this.$refs.page.keyHook) if (!result && this.$refs.page && this.$refs.page.keyHook)
result = this.$refs.page.keyHook(event); result = this.$refs.page.keyHook(event);
@@ -1333,16 +1254,4 @@ class Reader extends Vue {
.clear { .clear {
color: rgba(0,0,0,0); color: rgba(0,0,0,0);
} }
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.copy-icon {
cursor: pointer;
font-size: 120%;
color: blue;
}
</style> </style>

View File

@@ -0,0 +1,250 @@
<template>
<div>
<Dialog ref="dialog1" v-model="whatsNewVisible">
<template slot="header">
Что нового:
</template>
<div style="line-height: 20px" v-html="whatsNewContent"></div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
</span>
</Dialog>
<Dialog ref="dialog2" v-model="donationVisible">
<template slot="header">
Здравствуйте, уважаемые читатели!
</template>
<div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div>
<span slot="footer">
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog>
<Dialog ref="dialog3" v-model="liberamaTopVisible">
<template slot="header">
Здравствуйте, уважаемые читатели!
</template>
<div style="word-break: normal">
Создан новый ресурс:<br><br>
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
<br><br>
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
<ul>
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
</ul>
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
Спасибо, что вы с нами!
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div>
<span slot="footer">
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
</span>
</Dialog>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import Dialog from '../../share/Dialog.vue';
import * as utils from '../../../share/utils';
import {versionHistory} from '../versionHistory';
export default @Component({
components: {
Dialog
},
watch: {
settings: function() {
this.loadSettings();
},
},
})
class ReaderDialogs extends Vue {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
liberamaTopVisible = false;
created() {
this.commit = this.$store.commit;
this.loadSettings();
}
mounted() {
}
async init() {
await this.showWhatsNew();
await this.showDonation();
await this.showLiberamaTop();
}
loadSettings() {
const settings = this.settings;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
}
async showWhatsNew() {
const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) {
await utils.sleep(2000);
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
this.whatsNewVisible = true;
}
}
async showDonation() {
const today = utils.formatDate(new Date(), 'coDate');
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
await utils.sleep(3000);
this.donationVisible = true;
}
}
donationDialogDisable() {
this.donationVisible = false;
if (this.showDonationDialog2020) {
this.commit('reader/setSettings', { showDonationDialog2020: false });
}
}
donationDialogRemind() {
this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
}
openDonate() {
this.donationVisible = false;
this.liberamaTopVisible = false;
this.$emit('donate-toggle');
}
async copyLink(link) {
const result = await utils.copyTextToClipboard(link);
if (result)
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
else
this.$root.notify.error('Копирование не удалось');
}
openVersionHistory() {
this.whatsNewVisible = false;
this.$emit('version-history-toggle');
}
whatsNewDisable() {
this.whatsNewVisible = false;
const whatsNew = versionHistory[0];
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
}
get mode() {
return this.$store.state.config.mode;
}
get settings() {
return this.$store.state.reader.settings;
}
get whatsNewContentHash() {
return this.$store.state.reader.whatsNewContentHash;
}
get donationRemindDate() {
return this.$store.state.reader.donationRemindDate;
}
async showLiberamaTop() {
const today = utils.formatDate(new Date(), 'coDate');
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
await utils.sleep(3000);
this.liberamaTopVisible = true;
}
}
liberamaTopDialogDisable() {
this.liberamaTopVisible = false;
if (this.showLiberamaTopDialog2020) {
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
}
}
keyHook() {
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
return true;
return false;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.copy-icon {
cursor: pointer;
font-size: 120%;
color: blue;
}
</style>

View File

@@ -59,7 +59,7 @@
<q-td key="links" :props="props" class="td-mp" auto-width> <q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%"> <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 v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a> <a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
</div> </div>
</q-td> </q-td>
@@ -87,7 +87,7 @@
import Vue from 'vue'; import Vue from 'vue';
import Component from 'vue-class-component'; import Component from 'vue-class-component';
import path from 'path'; import path from 'path';
import _ from 'lodash'; //import _ from 'lodash';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
@@ -112,6 +112,7 @@ class RecentBooksPage extends Vue {
pagination = {}; pagination = {};
created() { created() {
this.firstInit = true;
this.pagination = {rowsPerPage: 0}; this.pagination = {rowsPerPage: 0};
this.columns = [ this.columns = [
@@ -167,26 +168,11 @@ class RecentBooksPage extends Vue {
this.initing = true; this.initing = true;
if (!bookManager.loaded) { if (this.firstInit) {//для отзывчивости
await this.updateTableData(10);
//для отзывчивости
await utils.sleep(100);
let i = 0;
let j = 5;
while (i < 500 && !bookManager.loaded) {
if (i % j == 0) {
bookManager.sortedRecentCached = null;
await this.updateTableData(20); await this.updateTableData(20);
j *= 2; this.firstInit = false;
}
await utils.sleep(100);
i++;
}
} else {
//для отзывчивости
await utils.sleep(100);
} }
await utils.sleep(50);
await this.updateTableData(); await this.updateTableData();
this.initing = false; this.initing = false;
})(); })();
@@ -222,30 +208,11 @@ class RecentBooksPage extends Vue {
textLen = ` ${Math.round(book.textLength/1000)}k`; textLen = ` ${Math.round(book.textLength/1000)}k`;
} }
const fb2 = (book.fb2 ? book.fb2 : {}); const bt = utils.getBookTitle(book.fb2);
let title = fb2.bookTitle; let title = bt.bookTitle;
if (title) title = (title ? `"${title}"`: '');
title = `"${title}"`; const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
else
title = '';
let author = '';
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
author = authorNames.join(', ');
} else {//TODO: убрать в будущем
author = _.compact([
fb2.lastName,
fb2.firstName,
fb2.middleName
]).join(' ');
}
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
result.push({ result.push({
num, num,
@@ -256,9 +223,10 @@ class RecentBooksPage extends Vue {
author, author,
title: `${title}${perc}${textLen}`, title: `${title}${perc}${textLen}`,
}, },
descString: `${author}${title}${perc}${textLen}`, descString: `${author}${title}${perc}${textLen}`,//для сортировки
url: book.url, url: book.url,
path: book.path, path: book.path,
fullTitle: bt.fullTitle,
key: book.key, key: book.key,
}); });
} }
@@ -291,13 +259,18 @@ class RecentBooksPage extends Vue {
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`; return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
} }
async downloadBook(fb2path) { async downloadBook(fb2path, fullTitle) {
try { try {
await readerApi.checkCachedBook(fb2path); await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download; const d = this.$refs.download;
d.href = fb2path; d.href = fb2path;
try {
const fn = utils.makeValidFilename(fullTitle);
d.download = fn.substring(0, 100) + '.fb2';
} catch(e) {
d.download = path.basename(fb2path).substr(0, 10) + '.fb2'; d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
}
d.click(); d.click();
} catch (e) { } catch (e) {
@@ -308,14 +281,6 @@ class RecentBooksPage extends Vue {
} }
} }
openOriginal(url) {
window.open(url, '_blank');
}
openFb2(path) {
window.open(path, '_blank');
}
async handleDel(key) { async handleDel(key) {
await bookManager.delRecentBook({key}); await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent //this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
@@ -331,7 +296,7 @@ class RecentBooksPage extends Vue {
isUrl(url) { isUrl(url) {
if (url) if (url)
return (url.indexOf('file://') != 0); return (url.indexOf('disk://') != 0);
else else
return false; return false;
} }

View File

@@ -216,8 +216,15 @@ class ServerStorage extends Vue {
} }
error(message) { error(message) {
if (this.showServerStorageMessages && !this.offlineModeActive) if (this.showServerStorageMessages && !this.offlineModeActive) {
this.$root.notify.error(message); this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
const hint = (this.errorMessageCounter < 2 ? '' :
'<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
'<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
);
this.$root.notify.error(message + hint);
}
} }
async loadSettings(force = false, doNotifySuccess = true) { async loadSettings(force = false, doNotifySuccess = true) {
@@ -505,12 +512,12 @@ class ServerStorage extends Vue {
const md = newRecentMod.data; const md = newRecentMod.data;
if (md.key && result[md.key]) if (md.key && result[md.key])
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, true); result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
if (!bookManager.loaded) { /*if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией'); this.warning('Ожидание загрузки списка книг перед синхронизацией');
while (!bookManager.loaded) await utils.sleep(100); while (!bookManager.loaded) await utils.sleep(100);
} }*/
if (newRecent.rev != this.cachedRecent.rev) if (newRecent.rev != this.cachedRecent.rev)
await this.setCachedRecent(newRecent); await this.setCachedRecent(newRecent);
@@ -569,7 +576,7 @@ class ServerStorage extends Vue {
let applyMod = this.cachedRecentMod.data; let applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key]) if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true); newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}}; newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
needSaveRecentPatch = true; needSaveRecentPatch = true;
@@ -580,8 +587,8 @@ class ServerStorage extends Vue {
let newRecent = {}; let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) { if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent //ждем весь bm.recent
while (!bookManager.loaded) /*while (!bookManager.loaded)
await utils.sleep(100); await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)}; newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}}; newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};

View File

@@ -108,8 +108,10 @@ export default @Component({
}, },
vertShift: function(newValue) { vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName); const font = (this.webFontName ? this.webFontName : this.fontName);
if (this.fontShifts[font] != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue}); this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue; this.fontVertShift = newValue;
}
}, },
fontName: function(newValue) { fontName: function(newValue) {
const font = (this.webFontName ? this.webFontName : newValue); const font = (this.webFontName ? this.webFontName : newValue);
@@ -185,10 +187,18 @@ class SettingsPage extends Vue {
settingsChanged() { settingsChanged() {
if (_.isEqual(this.form, this.settings)) if (_.isEqual(this.form, this.settings))
return; return;
this.form = Object.assign({}, this.settings); this.form = Object.assign({}, this.settings);
if (!this.unwatch)
this.unwatch = {};
for (let prop in rstore.settingDefaults) { for (let prop in rstore.settingDefaults) {
if (this.unwatch && this.unwatch[prop])
this.unwatch[prop]();
this[prop] = this.form[prop]; this[prop] = this.form[prop];
this.$watch(prop, (newValue) => {
this.unwatch[prop] = this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue}); this.form = Object.assign({}, this.form, {[prop]: newValue});
}); });
} }
@@ -397,7 +407,7 @@ class SettingsPage extends Vue {
return; return;
try { try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.currentProfile}' необратимо.` + const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` + `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', { `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; }, inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },

View File

@@ -104,8 +104,12 @@ class UserHotKeys extends UserHotKeysProps {
this.updateTableData(); this.updateTableData();
} }
get mode() {
return this.$store.state.config.mode;
}
updateTableData() { updateTableData() {
let result = rstore.hotKeys.map(hk => hk.name); let result = rstore.hotKeys.map(hk => hk.name).filter(name => (this.mode == 'liberama.top' || name != 'libs'));
const search = this.search.toLowerCase(); const search = this.search.toLowerCase();
const codesIncludeSearch = (action) => { const codesIncludeSearch = (action) => {

View File

@@ -22,7 +22,7 @@
<q-select v-model="currentProfile" :options="currentProfileOptions" <q-select v-model="currentProfile" :options="currentProfileOptions"
style="width: 275px" style="width: 275px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options outlined dense emit-value map-options display-value-sanitize options-sanitize
/> />
</div> </div>
</div> </div>

View File

@@ -77,9 +77,15 @@ export default class DrawHelper {
let j = 0; let j = 0;
//формируем строку //формируем строку
for (const part of line.parts) { for (const part of line.parts) {
let tOpen = (part.style.bold ? '<b>' : ''); let tOpen = '';
tOpen += (part.style.bold ? '<b>' : '');
tOpen += (part.style.italic ? '<i>' : ''); tOpen += (part.style.italic ? '<i>' : '');
let tClose = (part.style.italic ? '</i>' : ''); tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
let tClose = '';
tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : '');
tClose += (part.style.bold ? '</b>' : ''); tClose += (part.style.bold ? '</b>' : '');
let text = ''; let text = '';
@@ -159,7 +165,7 @@ export default class DrawHelper {
const fh = h - 2*pad; const fh = h - 2*pad;
const fh2 = fh/2; const fh2 = fh/2;
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`; const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
const w1 = this.measureTextFont(t1, font) + fh2; const w1 = this.measureTextFont(t1, font) + fh2;
const read = (bookPos + 1)/textLength; const read = (bookPos + 1)/textLength;
const t2 = `${(read*100).toFixed(2)}%`; const t2 = `${(read*100).toFixed(2)}%`;

View File

@@ -40,7 +40,7 @@ import Component from 'vue-class-component';
import {loadCSS} from 'fg-loadcss'; import {loadCSS} from 'fg-loadcss';
import _ from 'lodash'; import _ from 'lodash';
import {sleep} from '../../../share/utils'; import * as utils from '../../../share/utils';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
import DrawHelper from './DrawHelper'; import DrawHelper from './DrawHelper';
import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
@@ -134,7 +134,7 @@ class TextPage extends Vue {
this.$root.$on('resize', async() => { this.$root.$on('resize', async() => {
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
await sleep(500); await utils.sleep(500);
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
}); });
} }
@@ -285,7 +285,7 @@ class TextPage extends Vue {
let close = null; let close = null;
(async() => { (async() => {
await sleep(500); await utils.sleep(500);
if (this.fontsLoading) if (this.fontsLoading)
close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>'); close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
})(); })();
@@ -342,7 +342,7 @@ class TextPage extends Vue {
let i = 0; let i = 0;
const t = this.parsed.testText; const t = this.parsed.testText;
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth) while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
await sleep(100); await utils.sleep(100);
if (this.parsed === parsed) { if (this.parsed === parsed) {
this.parsed.testWidth = this.drawHelper.measureText(t, {}); this.parsed.testWidth = this.drawHelper.measureText(t, {});
@@ -366,7 +366,6 @@ class TextPage extends Vue {
this.updateLayout(); this.updateLayout();
this.book = null; this.book = null;
this.meta = null; this.meta = null;
this.fb2 = null;
this.parsed = null; this.parsed = null;
this.linesUp = null; this.linesUp = null;
@@ -383,7 +382,7 @@ class TextPage extends Vue {
try { try {
//подождем ленивый парсинг //подождем ленивый парсинг
this.stopLazyParse = true; this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10); while (this.doingLazyParse) await utils.sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook); const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) { if (!isParsed) {
@@ -392,21 +391,9 @@ class TextPage extends Vue {
this.book = await bookManager.getBook(this.lastBook); this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book); this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2; const bt = utils.getBookTitle(this.meta.fb2);
let authorNames = []; this.title = bt.fullTitle;
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title); this.$root.$emit('set-app-title', this.title);
@@ -493,7 +480,7 @@ class TextPage extends Vue {
let wait = (timeout + 201)/100; let wait = (timeout + 201)/100;
while (wait > 0 && !this[stopPropertyName]) { while (wait > 0 && !this[stopPropertyName]) {
wait--; wait--;
await sleep(100); await utils.sleep(100);
} }
resolve(); resolve();
})().catch(reject); }); })().catch(reject); });
@@ -509,7 +496,7 @@ class TextPage extends Vue {
} }
//ждем анимацию //ждем анимацию
while (this.inAnimation) await sleep(10); while (this.inAnimation) await utils.sleep(10);
this.stopScrolling = false; this.stopScrolling = false;
this.doingScrolling = true; this.doingScrolling = true;
@@ -520,7 +507,7 @@ class TextPage extends Vue {
this.page1 = this.page2; this.page1 = this.page2;
this.toggleLayout = true; this.toggleLayout = true;
await this.$nextTick(); await this.$nextTick();
await sleep(50); await utils.sleep(50);
this.cachedPos = -1; this.cachedPos = -1;
this.draw(); this.draw();
@@ -557,7 +544,7 @@ class TextPage extends Vue {
page.style.transform = 'none'; page.style.transform = 'none';
page.offsetHeight; page.offsetHeight;
while (this.doingScrolling) await sleep(10); while (this.doingScrolling) await utils.sleep(10);
} }
draw() { draw() {
@@ -766,7 +753,7 @@ class TextPage extends Vue {
for (let i = 0; i < this.parsed.para.length; i++) { for (let i = 0; i < this.parsed.para.length; i++) {
j++; j++;
if (j > 1) { if (j > 1) {
await sleep(1); await utils.sleep(1);
j = 0; j = 0;
} }
if (this.stopLazyParse) if (this.stopLazyParse)
@@ -788,7 +775,7 @@ class TextPage extends Vue {
async refreshTime() { async refreshTime() {
if (!this.timeRefreshing) { if (!this.timeRefreshing) {
this.timeRefreshing = true; this.timeRefreshing = true;
await sleep(60*1000); await utils.sleep(60*1000);
if (this.book && this.parsed.textLength) { if (this.book && this.parsed.textLength) {
this.debouncedDrawStatusBar(); this.debouncedDrawStatusBar();
@@ -904,9 +891,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) { if (!this.settingsChanging) {
this.settingsChanging = true; this.settingsChanging = true;
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100); const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize}); this.commit('reader/setSettings', {fontSize: newSize});
this.commit('reader/setSettings', newSettings); await utils.sleep(50);
await sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -915,9 +901,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) { if (!this.settingsChanging) {
this.settingsChanging = true; this.settingsChanging = true;
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5); const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize}); this.commit('reader/setSettings', {fontSize: newSize});
this.commit('reader/setSettings', newSettings); await utils.sleep(50);
await sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -926,9 +911,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) { if (!this.settingsChanging) {
this.settingsChanging = true; this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1); const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay}); this.commit('reader/setSettings', {scrollingDelay: newDelay});
this.commit('reader/setSettings', newSettings); await utils.sleep(50);
await sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -937,9 +921,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) { if (!this.settingsChanging) {
this.settingsChanging = true; this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000); const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay}); this.commit('reader/setSettings', {scrollingDelay: newDelay});
this.commit('reader/setSettings', newSettings); await utils.sleep(50);
await sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -953,7 +936,7 @@ class TextPage extends Vue {
let delay = 400; let delay = 400;
while (this.repDoing) { while (this.repDoing) {
this.handleClick(pointX, pointY); this.handleClick(pointX, pointY);
await sleep(delay); await utils.sleep(delay);
if (delay > 15) if (delay > 15)
delay *= 0.8; delay *= 0.8;
} }
@@ -1070,7 +1053,7 @@ class TextPage extends Vue {
onStatusBarClick() { onStatusBarClick() {
const url = this.meta.url; const url = this.meta.url;
if (url && url.indexOf('file://') != 0) { if (url && url.indexOf('disk://') != 0) {
window.open(url, '_blank'); window.open(url, '_blank');
} else { } else {
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'}); this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});

View File

@@ -1,6 +1,6 @@
import he from 'he'; import he from 'he';
import sax from '../../../../server/core/sax'; import sax from '../../../../server/core/sax';
import {sleep} from '../../../share/utils'; import * as utils from '../../../share/utils';
const maxImageLineCount = 100; const maxImageLineCount = 100;
@@ -46,11 +46,21 @@ export default class BookParser {
let isFirstSection = true; let isFirstSection = true;
let isFirstTitlePara = false; let isFirstTitlePara = false;
//изображения
this.binary = {}; this.binary = {};
let binaryId = ''; let binaryId = '';
let binaryType = ''; let binaryType = '';
let dimPromises = []; let dimPromises = [];
//оглавление
this.contents = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
let paraIndex = -1; let paraIndex = -1;
let paraOffset = 0; let paraOffset = 0;
let para = []; /*array of let para = []; /*array of
@@ -80,7 +90,7 @@ export default class BookParser {
i.onerror = reject; i.onerror = reject;
i.src = `data:${binaryType};base64,${data}`; i.src = `data:${binaryType};base64,${data}`;
await sleep(30*1000); await utils.sleep(30*1000);
if (!resolved) if (!resolved)
reject('Не удалось получить размер изображения'); reject('Не удалось получить размер изображения');
})().catch(reject); }); })().catch(reject); });
@@ -102,7 +112,7 @@ export default class BookParser {
i.onerror = reject; i.onerror = reject;
i.src = src; i.src = src;
await sleep(30*1000); await utils.sleep(30*1000);
if (!resolved) if (!resolved)
reject('Не удалось получить размер изображения'); reject('Не удалось получить размер изображения');
})().catch(reject); }); })().catch(reject); });
@@ -118,6 +128,12 @@ export default class BookParser {
addIndex: (addIndex ? addIndex : 0), addIndex: (addIndex ? addIndex : 0),
}; };
if (inSubtitle) {
curSubtitle.title += '<p>';
} else if (inTitle) {
curTitle.title += '<p>';
}
para[paraIndex] = p; para[paraIndex] = p;
paraOffset += p.length; paraOffset += p.length;
}; };
@@ -129,6 +145,7 @@ export default class BookParser {
return; return;
} }
const prevParaIndex = paraIndex;
let p = para[paraIndex]; let p = para[paraIndex];
paraOffset -= p.length; paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим //добавление пустых (addEmptyParagraphs) параграфов перед текущим
@@ -143,6 +160,11 @@ export default class BookParser {
p.offset = paraOffset; p.offset = paraOffset;
para[paraIndex] = p; para[paraIndex] = p;
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
//уберем начальный пробел //уберем начальный пробел
p.length = 0; p.length = 0;
p.text = p.text.substr(1); p.text = p.text.substr(1);
@@ -151,6 +173,13 @@ export default class BookParser {
p.length += len; p.length += len;
p.text += text; p.text += text;
if (inSubtitle) {
curSubtitle.title += text;
} else if (inTitle) {
curTitle.title += text;
}
para[paraIndex] = p; para[paraIndex] = p;
paraOffset += p.length; paraOffset += p.length;
}; };
@@ -160,7 +189,7 @@ export default class BookParser {
return; return;
tag = elemName; tag = elemName;
path += '/' + elemName; path += '/' + tag;
if (tag == 'binary') { if (tag == 'binary') {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
@@ -187,17 +216,55 @@ export default class BookParser {
} }
} }
if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) { if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author) if (!fb2.author)
fb2.author = []; fb2.author = [];
fb2.author.push({}); fb2.author.push({});
} }
const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
if (!fb2.sequence)
fb2.sequence = [];
if (!isPublishSequence || !fb2.sequence.length) {
const attrs = sax.getAttrsSync(tail);
const seq = {};
if (attrs.name && attrs.name.value) {
seq.name = attrs.name.value;
}
if (attrs.number && attrs.number.value) {
seq.number = attrs.number.value;
}
fb2.sequence.push(seq);
}
}
if (path.indexOf('/fictionbook/body') == 0) { if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') { if (tag == 'body') {
if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => {
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
});
if (ann.length)
newParagraph(' ', 1);
}
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
const bt = utils.getBookTitle(fb2);
if (bt.sequence) {
newParagraph(bt.sequence, bt.sequence.length);
newParagraph(' ', 1);
}
}
if (!isFirstBody) if (!isFirstBody)
newParagraph(' ', 1); newParagraph(' ', 1);
isFirstBody = false; isFirstBody = false;
bodyIndex++;
} }
if (tag == 'title') { if (tag == 'title') {
@@ -205,15 +272,20 @@ export default class BookParser {
isFirstTitlePara = true; isFirstTitlePara = true;
bold = true; bold = true;
center = true; center = true;
inTitle = true;
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
this.contents.push(curTitle);
} }
if (tag == 'section') { if (tag == 'section') {
if (!isFirstSection) if (!isFirstSection)
newParagraph(' ', 1); newParagraph(' ', 1);
isFirstSection = false; isFirstSection = false;
sectionLevel++;
} }
if (tag == 'emphasis' || tag == 'strong') { if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`<${tag}>`, 0); growParagraph(`<${tag}>`, 0);
} }
@@ -231,9 +303,13 @@ export default class BookParser {
isFirstTitlePara = true; isFirstTitlePara = true;
bold = true; bold = true;
center = true; center = true;
inSubtitle = true;
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
curTitle.subtitles.push(curSubtitle);
} }
if (tag == 'epigraph') { if (tag == 'epigraph' || tag == 'annotation') {
italic = true; italic = true;
space += 1; space += 1;
} }
@@ -260,9 +336,14 @@ export default class BookParser {
isFirstTitlePara = false; isFirstTitlePara = false;
bold = false; bold = false;
center = false; center = false;
inTitle = false;
} }
if (tag == 'emphasis' || tag == 'strong') { if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0); growParagraph(`</${tag}>`, 0);
} }
@@ -274,11 +355,14 @@ export default class BookParser {
isFirstTitlePara = false; isFirstTitlePara = false;
bold = false; bold = false;
center = false; center = false;
inSubtitle = false;
} }
if (tag == 'epigraph') { if (tag == 'epigraph' || tag == 'annotation') {
italic = false; italic = false;
space -= 1; space -= 1;
if (tag == 'annotation')
newParagraph(' ', 1);
} }
if (tag == 'stanza') { if (tag == 'stanza') {
@@ -372,7 +456,7 @@ export default class BookParser {
}; };
const onProgress = async(prog) => { const onProgress = async(prog) => {
await sleep(1); await utils.sleep(1);
callback(prog); callback(prog);
}; };
@@ -394,7 +478,7 @@ export default class BookParser {
this.textLength = paraOffset; this.textLength = paraOffset;
callback(100); callback(100);
await sleep(10); await utils.sleep(10);
return {fb2}; return {fb2};
} }
@@ -423,7 +507,7 @@ export default class BookParser {
splitToStyle(s) { splitToStyle(s) {
let result = [];/*array of { let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number}, style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
image: {local: Boolean, inline: Boolean, id: String}, image: {local: Boolean, inline: Boolean, id: String},
text: String, text: String,
}*/ }*/
@@ -446,6 +530,12 @@ export default class BookParser {
case 'emphasis': case 'emphasis':
style.italic = true; style.italic = true;
break; break;
case 'sup':
style.sup = true;
break;
case 'sub':
style.sub = true;
break;
case 'center': case 'center':
style.center = true; style.center = true;
break; break;
@@ -496,6 +586,12 @@ export default class BookParser {
case 'emphasis': case 'emphasis':
style.italic = false; style.italic = false;
break; break;
case 'sup':
style.sup = false;
break;
case 'sub':
style.sub = false;
break;
case 'center': case 'center':
style.center = false; style.center = false;
break; break;

View File

@@ -4,19 +4,25 @@ import _ from 'lodash';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import BookParser from './BookParser'; import BookParser from './BookParser';
const maxDataSize = 300*1024*1024;//compressed bytes const maxDataSize = 500*1024*1024;//compressed bytes
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({ const bmMetaStore = localForage.createInstance({
name: 'bmMetaStore' name: 'bmMetaStore'
}); });
//локальный кэш самих книг, ограничение maxDataSize
const bmDataStore = localForage.createInstance({ const bmDataStore = localForage.createInstance({
name: 'bmDataStore' name: 'bmDataStore'
}); });
const bmRecentStore = localForage.createInstance({ //список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore' name: 'bmRecentStore'
}); });
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
class BookManager { class BookManager {
async init(settings) { async init(settings) {
@@ -25,9 +31,47 @@ class BookManager {
this.eventListeners = []; this.eventListeners = [];
this.books = {}; this.books = {};
this.recent = {};
this.saveRecent = _.debounce(() => {
bmRecentStoreNew.setItem('recent', this.recent);
}, 300, {maxWait: 800});
this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300});
//загрузка bmRecentStore
this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
if (this.recentRev) {
this.recent = await bmRecentStoreNew.getItem('recent');
if (!this.recent)
this.recent = {}; this.recent = {};
this.recentLast = await bmRecentStore.getItem('recent-last'); this.recentItem = await bmRecentStoreNew.getItem('recent-item');
if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem;
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
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) { if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast; this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`); const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
@@ -36,14 +80,39 @@ class BookManager {
} }
} }
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; this.recentChanged = true;
this.loadStored();//no await this.loadStored();//no await
} }
//Долгая асинхронная загрузка из хранилища. //Ленивая асинхронная загрузка bmMetaStore
//Хранение в отдельных записях дает относительно
//нормальное поведение при нескольких вкладках с читалкой в браузере.
async loadStored() { async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
await utils.sleep(2000); await utils.sleep(2000);
@@ -70,32 +139,7 @@ class BookManager {
} }
} }
let key = null;
len = await bmRecentStore.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStore.key(i);
if (key) {
let r = await bmRecentStore.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStore.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 this.cleanBooks(); await this.cleanBooks();
await this.cleanRecentBooks();
this.recentChanged = true;
this.loaded = true; this.loaded = true;
this.emit('load-stored-finish'); this.emit('load-stored-finish');
} }
@@ -125,7 +169,7 @@ class BookManager {
} }
async deflateWithProgress(data, callback) { async deflateWithProgress(data, callback) {
const chunkSize = 128*1024; const chunkSize = 512*1024;
const deflator = new utils.pako.Deflate({level: 5}); const deflator = new utils.pako.Deflate({level: 5});
let chunkTotal = 1 + Math.floor(data.length/chunkSize); let chunkTotal = 1 + Math.floor(data.length/chunkSize);
@@ -159,7 +203,7 @@ class BookManager {
} }
async inflateWithProgress(data, callback) { async inflateWithProgress(data, callback) {
const chunkSize = 64*1024; const chunkSize = 512*1024;
const inflator = new utils.pako.Inflate({to: 'string'}); const inflator = new utils.pako.Inflate({to: 'string'});
let chunkTotal = 1 + Math.floor(data.length/chunkSize); let chunkTotal = 1 + Math.floor(data.length/chunkSize);
@@ -238,7 +282,7 @@ class BookManager {
let book = this.books[meta.key]; let book = this.books[meta.key];
if (!book && !this.loaded) { if (!book && !this.loaded) {
book = await bmDataStore.getItem(`bmMeta-${meta.key}`); book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (book) if (book)
this.books[meta.key] = book; this.books[meta.key] = book;
} }
@@ -254,7 +298,7 @@ class BookManager {
result = this.books[meta.key]; result = this.books[meta.key];
if (!result) { if (!result) {
result = await bmDataStore.getItem(`bmMeta-${meta.key}`); result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (result) if (result)
this.books[meta.key] = result; this.books[meta.key] = result;
} }
@@ -328,6 +372,43 @@ class BookManager {
} }
//-- recent -------------------------------------------------------------- //-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev');
if (rev != this.recentRev && !skipCheck) {
const newRecent = await bmRecentStoreNew.getItem('recent');
Object.assign(this.recent, newRecent);
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
this.recentRev = rev;
}
const prevKey = (this.recentItem ? this.recentItem.key : '');
if (item) {
this.recent[item.key] = item;
this.recentItem = item;
} else {
this.recentItem = null;
}
this.saveRecentItem();
if (!item || prevKey != item.key) {
this.saveRecent();
}
this.recentChanged = true;
if (item) {
this.emit('recent-changed', item.key);
} else {
this.emit('recent-changed');
}
}
async recentSetLastKey(key) {
this.recentLastKey = key;
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
}
async setRecentBook(value) { async setRecentBook(value) {
const result = this.metaOnly(value); const result = this.metaOnly(value);
result.touchTime = Date.now(); result.touchTime = Date.now();
@@ -341,38 +422,25 @@ class BookManager {
result.bookPosSeen = this.recent[result.key].bookPosSeen; result.bookPosSeen = this.recent[result.key].bookPosSeen;
} }
this.recent[result.key] = result; await this.recentSetLastKey(result.key);
await this.recentSetItem(result);
await bmRecentStore.setItem(result.key, result);
this.recentLast = result;
await bmRecentStore.setItem('recent-last', this.recentLast);
this.recentChanged = true;
this.emit('recent-changed', result.key);
return result; return result;
} }
async getRecentBook(value) { async getRecentBook(value) {
let result = this.recent[value.key]; return this.recent[value.key];
if (!result) {
result = await bmRecentStore.getItem(value.key);
if (result)
this.recent[value.key] = result;
}
return result;
} }
async delRecentBook(value) { async delRecentBook(value) {
this.recent[value.key].deleted = 1; const item = this.recent[value.key];
await bmRecentStore.setItem(value.key, this.recent[value.key]); item.deleted = 1;
if (this.recentLast.key == value.key) { if (this.recentLastKey == value.key) {
this.recentLast = null; await this.recentSetLastKey(null);
await bmRecentStore.setItem('recent-last', this.recentLast);
} }
await this.recentSetItem(item);
this.emit('recent-deleted', value.key); this.emit('recent-deleted', value.key);
this.emit('recent-changed', value.key);
} }
async cleanRecentBooks() { async cleanRecentBooks() {
@@ -380,24 +448,49 @@ class BookManager {
let isDel = false; let isDel = false;
for (let i = 1000; i < sorted.length; i++) { for (let i = 1000; i < sorted.length; i++) {
await bmRecentStore.removeItem(sorted[i].key);
delete this.recent[sorted[i].key]; delete this.recent[sorted[i].key];
await bmRecentStore.removeItem(sorted[i].key);
isDel = true; isDel = true;
} }
this.sortedRecentCached = null; this.sortedRecentCached = null;
if (isDel) if (isDel)
this.emit('recent-changed'); await this.recentSetItem();
return isDel; return isDel;
} }
mostRecentBook() { async convertFileToDiskPrefix() {
if (this.recentLast) { let isConverted = false;
return this.recentLast;
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;
} }
const oldRecentLast = this.recentLast;
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];
}
const oldKey = this.recentLastKey;
let max = 0; let max = 0;
let result = null; let result = null;
@@ -408,10 +501,11 @@ class BookManager {
result = book; result = book;
} }
} }
this.recentLast = result;
bmRecentStore.setItem('recent-last', this.recentLast);//no await
if (this.recentLast !== oldRecentLast) const newRecentLastKey = (result ? result.key : null);
this.recentSetLastKey(newRecentLastKey);//no await
if (newRecentLastKey !== oldKey)
this.emit('recent-changed'); this.emit('recent-changed');
return result; return result;
@@ -442,24 +536,12 @@ class BookManager {
delete mergedRecent[i]; delete mergedRecent[i];
} }
//"ленивое" обновление хранилища
(async() => {
for (const rec of Object.values(mergedRecent)) {
if (rec.key) {
await bmRecentStore.setItem(rec.key, rec);
await utils.sleep(1);
}
}
})();
this.recent = mergedRecent; this.recent = mergedRecent;
this.recentLast = null; await this.recentSetLastKey(null);
await bmRecentStore.setItem('recent-last', this.recentLast); await this.recentSetItem(null, true);
this.recentChanged = true;
this.emit('set-recent'); this.emit('set-recent');
this.emit('recent-changed');
} }
addEventListener(listener) { addEventListener(listener) {

View File

@@ -1,4 +1,73 @@
export const versionHistory = [ export const versionHistory = [
{
showUntil: '2020-12-08',
header: '0.9.11 (2020-12-09)',
content:
`
<ul>
<li>оптимизации, улучшения работы конвертеров</li>
</ul>
`
},
{
showUntil: '2020-12-10',
header: '0.9.10 (2020-12-03)',
content:
`
<ul>
<li>добавлена частичная поддержка формата Djvu</li>
<li>добавлена поддержка Rar-архивов</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-11-20',
header: '0.9.9 (2020-11-21)',
content:
`
<ul>
<li>оптимизации, исправления багов</li>
</ul>
`
},
{
showUntil: '2020-11-12',
header: '0.9.8 (2020-11-13)',
content:
`
<ul>
<li>добавлено окно "Оглавление/закладки"</li>
</ul>
`
},
{
showUntil: '2020-11-11',
header: '0.9.7 (2020-11-12)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content:
`
<ul>
<li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{ {
showUntil: '2020-10-31', showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)', header: '0.9.5 (2020-11-01)',

View File

@@ -1,5 +1,5 @@
<template> <template>
<q-dialog v-model="active"> <q-dialog v-model="active" no-route-dismiss>
<div class="column bg-white no-wrap"> <div class="column bg-white no-wrap">
<div class="header row"> <div class="header row">
<div class="caption col row items-center q-ml-md"> <div class="caption col row items-center q-ml-md">
@@ -32,7 +32,7 @@ const DialogProps = Vue.extend({
props: { props: {
value: Boolean, value: Boolean,
} }
}) });
export default @Component({ export default @Component({
}) })

View File

@@ -1,12 +1,12 @@
<template> <template>
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide"> <q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
<slot></slot> <slot></slot>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'alert'" class="bg-white no-wrap"> <div v-show="type == 'alert'" class="bg-white no-wrap">
<div class="header row"> <div class="header row">
<div class="caption col row items-center q-ml-md"> <div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
@@ -29,7 +29,7 @@
<div v-show="type == 'confirm'" class="bg-white no-wrap"> <div v-show="type == 'confirm'" class="bg-white no-wrap">
<div class="header row"> <div class="header row">
<div class="caption col row items-center q-ml-md"> <div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
@@ -53,7 +53,7 @@
<div v-show="type == 'prompt'" class="bg-white no-wrap"> <div v-show="type == 'prompt'" class="bg-white no-wrap">
<div class="header row"> <div class="header row">
<div class="caption col row items-center q-ml-md"> <div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
@@ -79,7 +79,7 @@
<div v-show="type == 'hotKey'" class="bg-white no-wrap"> <div v-show="type == 'hotKey'" class="bg-white no-wrap">
<div class="header row"> <div class="header row">
<div class="caption col row items-center q-ml-md"> <div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
@@ -127,6 +127,7 @@ class StdDialog extends Vue {
inputValue = ''; inputValue = '';
error = ''; error = '';
iconColor = ''; iconColor = '';
iconName = '';
hotKeyCode = ''; hotKeyCode = '';
created() { created() {
@@ -144,12 +145,18 @@ class StdDialog extends Vue {
this.inputValidator = null; this.inputValidator = null;
this.inputValue = ''; this.inputValue = '';
this.error = ''; this.error = '';
this.showed = false;
this.iconColor = 'text-warning'; this.iconColor = 'text-warning';
if (opts && opts.color) { if (opts && opts.color) {
this.iconColor = `text-${opts.color}`; this.iconColor = `text-${opts.color}`;
} }
this.iconName = 'las la-exclamation-circle';
if (opts && opts.iconName) {
this.iconName = opts.iconName;
}
this.hotKeyCode = ''; this.hotKeyCode = '';
if (opts && opts.hotKeyCode) { if (opts && opts.hotKeyCode) {
this.hotKeyCode = opts.hotKeyCode; this.hotKeyCode = opts.hotKeyCode;
@@ -161,6 +168,7 @@ class StdDialog extends Vue {
this.hideTrigger(); this.hideTrigger();
this.hideTrigger = null; this.hideTrigger = null;
} }
this.showed = false;
} }
onShow() { onShow() {
@@ -170,6 +178,7 @@ class StdDialog extends Vue {
this.validate(this.inputValue); this.validate(this.inputValue);
this.$refs.input.focus(); this.$refs.input.focus();
} }
this.showed = true;
} }
validate(value) { validate(value) {
@@ -276,7 +285,7 @@ class StdDialog extends Vue {
} }
keyHook(event) { keyHook(event) {
if (this.active) { if (this.active && this.showed) {
let handled = false; let handled = false;
if (this.type == 'hotKey') { if (this.type == 'hotKey') {
if (event.type == 'keydown') { if (event.type == 'keydown') {

View File

@@ -32,6 +32,9 @@ export default @Component({
class Window extends Vue { class Window extends Vue {
init() { init() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.main.style.top = 0;
this.$refs.main.style.left = 0;
this.$refs.windowBox.style.height = this.height; this.$refs.windowBox.style.height = this.height;
this.$refs.windowBox.style.width = this.width; this.$refs.windowBox.style.width = this.width;
if (this.maxWidth) if (this.maxWidth)

View File

@@ -4,6 +4,9 @@ import router from './router';
import store from './store'; import store from './store';
import './quasar'; import './quasar';
import vueSanitize from 'vue-sanitize';
Vue.use(vueSanitize);
import App from './components/App.vue'; import App from './components/App.vue';
//Vue.config.productionTip = false; //Vue.config.productionTip = false;
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);

View File

@@ -21,7 +21,7 @@ import {QSlider} from 'quasar/src/components/slider';
import {QTabs, QTab} from 'quasar/src/components/tabs'; import {QTabs, QTab} from 'quasar/src/components/tabs';
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels'; //import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
import {QSeparator} from 'quasar/src/components/separator'; import {QSeparator} from 'quasar/src/components/separator';
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item'; //import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
import {QTooltip} from 'quasar/src/components/tooltip'; import {QTooltip} from 'quasar/src/components/tooltip';
import {QSpinner} from 'quasar/src/components/spinner'; import {QSpinner} from 'quasar/src/components/spinner';
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table'; import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
@@ -31,6 +31,8 @@ import {QColor} from 'quasar/src/components/color';
import {QPopupProxy} from 'quasar/src/components/popup-proxy'; import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog'; import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip'; import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = { const components = {
//QLayout, //QLayout,
@@ -47,7 +49,7 @@ const components = {
QTabs, QTab, QTabs, QTab,
//QTabPanels, QTabPanel, //QTabPanels, QTabPanel,
QSeparator, QSeparator,
QList, QItem, QItemSection, QItemLabel, //QList, QItem, QItemSection, QItemLabel,
QTooltip, QTooltip,
QSpinner, QSpinner,
QTable, QTh, QTr, QTd, QTable, QTh, QTr, QTd,
@@ -57,6 +59,8 @@ const components = {
QPopupProxy, QPopupProxy,
QDialog, QDialog,
QChip, QChip,
QTree,
//QExpansionItem,
}; };
//directives //directives

View File

@@ -103,47 +103,107 @@ export function fromBase64(data) {
)); ));
} }
export function getObjDiff(oldObj, newObj) { export function getObjDiff(oldObj, newObj, opts = {}) {
const {
exclude = [],
excludeAdd = [],
excludeDel = [],
} = opts;
const ex = new Set(exclude);
const exAdd = new Set(excludeAdd);
const exDel = new Set(excludeDel);
const makeObjDiff = (oldObj, newObj, keyPath) => {
const result = {__isDiff: true, change: {}, add: {}, del: []}; const result = {__isDiff: true, change: {}, add: {}, del: []};
keyPath = `${keyPath}${keyPath ? '/' : ''}`;
for (const key of Object.keys(oldObj)) { for (const key of Object.keys(oldObj)) {
const kp = `${keyPath}${key}`;
if (newObj.hasOwnProperty(key)) { if (newObj.hasOwnProperty(key)) {
if (ex.has(kp))
continue;
if (!_.isEqual(oldObj[key], newObj[key])) { if (!_.isEqual(oldObj[key], newObj[key])) {
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) { if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
result.change[key] = getObjDiff(oldObj[key], newObj[key]); result.change[key] = makeObjDiff(oldObj[key], newObj[key], kp);
} else { } else {
result.change[key] = _.cloneDeep(newObj[key]); result.change[key] = _.cloneDeep(newObj[key]);
} }
} }
} else { } else {
if (exDel.has(kp))
continue;
result.del.push(key); result.del.push(key);
} }
} }
for (const key of Object.keys(newObj)) { for (const key of Object.keys(newObj)) {
const kp = `${keyPath}${key}`;
if (exAdd.has(kp))
continue;
if (!oldObj.hasOwnProperty(key)) { if (!oldObj.hasOwnProperty(key)) {
result.add[key] = _.cloneDeep(newObj[key]); result.add[key] = _.cloneDeep(newObj[key]);
} }
} }
return result; return result;
}
return makeObjDiff(oldObj, newObj, '');
} }
export function isObjDiff(diff) { export function isObjDiff(diff) {
return (_.isObject(diff) && diff.__isDiff); return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
} }
export function isEmptyObjDiff(diff) { export function isEmptyObjDiff(diff) {
return (!_.isObject(diff) || !diff.__isDiff || return (!isObjDiff(diff) ||
(!Object.keys(diff.change).length && !(Object.keys(diff.change).length ||
!Object.keys(diff.add).length && Object.keys(diff.add).length ||
!diff.del.length diff.del.length
) )
); );
} }
export function applyObjDiff(obj, diff, isAddChanged) { export function isEmptyObjDiffDeep(diff, opts = {}) {
const result = _.cloneDeep(obj); if (!isObjDiff(diff))
return true;
const {
isApplyChange = true,
isApplyAdd = true,
isApplyDel = true,
} = opts;
let notEmptyDeep = false;
const change = diff.change;
for (const key of Object.keys(change)) {
if (_.isObject(change[key]))
notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
else if (isApplyChange)
notEmptyDeep = true;
}
return !(
notEmptyDeep ||
(isApplyAdd && Object.keys(diff.add).length) ||
(isApplyDel && diff.del.length)
);
}
export function applyObjDiff(obj, diff, opts = {}) {
const {
isAddChanged = false,
isApplyChange = true,
isApplyAdd = true,
isApplyDel = true,
} = opts;
let result = _.cloneDeep(obj);
if (!diff.__isDiff) if (!diff.__isDiff)
return result; return result;
@@ -151,8 +211,9 @@ export function applyObjDiff(obj, diff, isAddChanged) {
for (const key of Object.keys(change)) { for (const key of Object.keys(change)) {
if (result.hasOwnProperty(key)) { if (result.hasOwnProperty(key)) {
if (_.isObject(change[key])) { if (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], isAddChanged); result[key] = applyObjDiff(result[key], change[key], opts);
} else { } else {
if (isApplyChange)
result[key] = _.cloneDeep(change[key]); result[key] = _.cloneDeep(change[key]);
} }
} else if (isAddChanged) { } else if (isAddChanged) {
@@ -160,13 +221,19 @@ export function applyObjDiff(obj, diff, isAddChanged) {
} }
} }
if (isApplyAdd) {
for (const key of Object.keys(diff.add)) { for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[key]); result[key] = _.cloneDeep(diff.add[key]);
} }
}
if (isApplyDel && diff.del.length) {
for (const key of diff.del) { for (const key of diff.del) {
delete result[key]; delete result[key];
} }
if (_.isArray(result))
result = result.filter(v => v);
}
return result; return result;
} }
@@ -237,3 +304,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
} }
return result; return result;
} }
export function removeHtmlTags(s) {
return s.replace(/(<([^>]+)>)/ig, '');
}
export function makeValidFilename(filename, repl = '_') {
let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
f = f.trim();
while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
f = f.substring(0, f.length - 1);
}
if (f)
return f;
else
throw new Error('Invalid filename');
}
export function getBookTitle(fb2) {
fb2 = (fb2 ? fb2 : {});
const result = {};
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
result.author = authorNames.join(', ');
}
if (fb2.sequence) {
const seqs = fb2.sequence.map(s => _.compact([
s.name,
(s.number ? `#${s.number}` : null),
]).join(' '));
result.sequence = seqs.join(', ');
if (result.sequence)
result.sequenceTitle = `(${result.sequence})`;
}
result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
result.fullTitle = _.compact([
result.author,
result.bookTitle
]).join(' - ');
return result;
}

View File

@@ -1,3 +1,5 @@
import * as utils from '../../share/utils';
const readerActions = { const readerActions = {
'help': 'Вызвать cправку', 'help': 'Вызвать cправку',
'loader': 'На страницу загрузки', 'loader': 'На страницу загрузки',
@@ -13,7 +15,8 @@ const readerActions = {
'splitToPara': 'Обновить с разбиением на параграфы', 'splitToPara': 'Обновить с разбиением на параграфы',
'refresh': 'Принудительно обновить книгу', 'refresh': 'Принудительно обновить книгу',
'offlineMode': 'Автономный режим (без интернета)', 'offlineMode': 'Автономный режим (без интернета)',
'libs': 'Библиотека', 'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние', 'recentBooks': 'Открыть недавние',
'switchToolbar': 'Показать/скрыть панель управления', 'switchToolbar': 'Показать/скрыть панель управления',
'donate': '', 'donate': '',
@@ -40,6 +43,7 @@ const toolButtons = [
{name: 'copyText', show: false}, {name: 'copyText', show: false},
{name: 'splitToPara', show: false}, {name: 'splitToPara', show: false},
{name: 'refresh', show: true}, {name: 'refresh', show: true},
{name: 'contents', show: true},
{name: 'libs', show: true}, {name: 'libs', show: true},
{name: 'recentBooks', show: true}, {name: 'recentBooks', show: true},
{name: 'offlineMode', show: false}, {name: 'offlineMode', show: false},
@@ -59,9 +63,10 @@ const hotKeys = [
{name: 'copyText', codes: ['Ctrl+C']}, {name: 'copyText', codes: ['Ctrl+C']},
{name: 'splitToPara', codes: ['Shift+R']}, {name: 'splitToPara', codes: ['Shift+R']},
{name: 'refresh', codes: ['R']}, {name: 'refresh', codes: ['R']},
{name: 'offlineMode', codes: ['O']}, {name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']}, {name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']}, {name: 'recentBooks', codes: ['X']},
{name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']}, {name: 'switchToolbar', codes: ['Tab', 'Q']},
{name: 'bookBegin', codes: ['Home']}, {name: 'bookBegin', codes: ['Home']},
@@ -249,6 +254,7 @@ const settingDefaults = {
showServerStorageMessages: true, showServerStorageMessages: true,
showWhatsNewDialog: true, showWhatsNewDialog: true,
showDonationDialog2020: true, showDonationDialog2020: true,
showLiberamaTopDialog2020: true,
enableSitesFilter: true, enableSitesFilter: true,
fontShifts: {}, fontShifts: {},
@@ -256,29 +262,6 @@ const settingDefaults = {
userHotKeys: {}, userHotKeys: {},
}; };
const libsDefaults = {
startLink: 'http://flibusta.is',
comment: 'Флибуста | Книжное братство',
closeAfterSubmit: false,
groups: [
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
]},
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},
]
};
for (const font of fonts) for (const font of fonts)
settingDefaults.fontShifts[font.name] = font.fontVertShift; settingDefaults.fontShifts[font.name] = font.fontVertShift;
for (const font of webFonts) for (const font of webFonts)
@@ -288,6 +271,47 @@ for (const button of toolButtons)
for (const hotKey of hotKeys) for (const hotKey of hotKeys)
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes; settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
const excludeDiffHotKeys = [];
for (const hotKey of hotKeys)
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
}
return false;
}
const libsDefaults = {
startLink: 'http://flibusta.is',
comment: 'Флибуста | Книжное братство',
closeAfterSubmit: false,
openInFrameOnEnter: false,
openInFrameOnAdd: false,
groups: [
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: '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: 'Миры Фэнтези'},
]},
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
]},
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},
]
};
// initial state // initial state
const state = { const state = {
toolBarActive: true, toolBarActive: true,
@@ -341,7 +365,13 @@ const mutations = {
state.currentProfile = value; state.currentProfile = value;
}, },
setSettings(state, value) { setSettings(state, value) {
state.settings = Object.assign({}, state.settings, value); const newSettings = Object.assign({}, state.settings, value);
const added = addDefaultsToSettings(newSettings);
if (added) {
state.settings = added;
} else {
state.settings = newSettings;
}
}, },
setSettingsRev(state, value) { setSettingsRev(state, value) {
state.settingsRev = Object.assign({}, state.settingsRev, value); state.settingsRev = Object.assign({}, state.settingsRev, value);
@@ -361,6 +391,7 @@ export default {
fonts, fonts,
webFonts, webFonts,
settingDefaults, settingDefaults,
addDefaultsToSettings,
libsDefaults, libsDefaults,
namespaced: true, namespaced: true,

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
sudo -H -u www-data /home/beta.liberama/liberama sudo -H -u www-data bash -c "cd /var/www; /home/beta.liberama/liberama"

View File

@@ -126,3 +126,18 @@ server {
proxy_set_header Referer ""; proxy_set_header Referer "";
} }
} }
server {
listen 23580;
server_name fw_proxy;
valid_referers liberama.top b.liberama.top;
if ($invalid_referer) {
return 403;
}
location / {
proxy_pass http://fantasy-worlds.org;
}
}

View File

@@ -27,15 +27,31 @@ sudo chown www-data.www-data /home/liberama
### external converter `calibre`, download from https://download.calibre-ebook.com/ ### external converter `calibre`, download from https://download.calibre-ebook.com/
``` ```
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz" wget "https://download.calibre-ebook.com/5.5.0/calibre-5.5.0-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre sudo -u www-data mkdir -p /home/liberama/data/calibre
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
```
### external converter `pdfalto`, github https://github.com/kermitt2/pdfalto
```
git clone https://github.com/kermitt2/pdfalto
cd pdfalto
git submodule update --init --recursive
cmake ./
добавить в начало CMakeLists.txt строчку: set(CMAKE_EXE_LINKER_FLAGS "-no-pie")
make
sudo -u www-data mkdir -p /home/liberama/data/pdfalto
sudo -u www-data cp pdfalto /home/liberama/data/pdfalto
``` ```
### external converters ### external converters
``` ```
sudo apt install rar
sudo apt install libreoffice sudo apt install libreoffice
sudo apt install poppler-utils sudo apt install djvulibre-bin
sudo apt install libtiff-tools
sudo apt install graphicsmagick-imagemagick-compat
``` ```
### nginx, server config ### nginx, server config

View File

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

View File

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

190
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.9.5", "version": "0.9.11",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -36,12 +36,6 @@
"pkg-up": "^2.0.0" "pkg-up": "^2.0.0"
} }
}, },
"caniuse-lite": {
"version": "1.0.30001062",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.446", "version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
@@ -321,12 +315,6 @@
"pkg-up": "^2.0.0" "pkg-up": "^2.0.0"
} }
}, },
"caniuse-lite": {
"version": "1.0.30001062",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.446", "version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
@@ -1603,12 +1591,6 @@
"pkg-up": "^2.0.0" "pkg-up": "^2.0.0"
} }
}, },
"caniuse-lite": {
"version": "1.0.30001062",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.446", "version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
@@ -4017,9 +3999,9 @@
} }
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30000989", "version": "1.0.30001154",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001154.tgz",
"integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==", "integrity": "sha512-y9DvdSti8NnYB9Be92ddMZQrcOe04kcQtcxtBx4NkB04+qZ+JUWotnXBJTmxlKudhxNTQ3RRknMwNU2YQl/Org==",
"dev": true "dev": true
}, },
"caseless": { "caseless": {
@@ -4373,6 +4355,11 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"colorette": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz",
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw=="
},
"combined-stream": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -4759,12 +4746,6 @@
"pkg-up": "^2.0.0" "pkg-up": "^2.0.0"
} }
}, },
"caniuse-lite": {
"version": "1.0.30001062",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.446", "version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
@@ -8696,6 +8677,11 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true "dev": true
}, },
"klona": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz",
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA=="
},
"last-call-webpack-plugin": { "last-call-webpack-plugin": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
@@ -8790,6 +8776,30 @@
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }
}, },
"line-column": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz",
"integrity": "sha1-0lryk2tvSEkXKzEuR5LR2Ye8NKI=",
"requires": {
"isarray": "^1.0.0",
"isobject": "^2.0.0"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isobject": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
"integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
"requires": {
"isarray": "1.0.0"
}
}
}
},
"loader-runner": { "loader-runner": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@@ -9372,6 +9382,11 @@
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
}, },
"nanoid": {
"version": "3.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
"integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w=="
},
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -10010,6 +10025,11 @@
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
"dev": true "dev": true
}, },
"parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE="
},
"parseurl": { "parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -10104,6 +10124,21 @@
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
"dev": true "dev": true
}, },
"pidusage": {
"version": "2.0.21",
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz",
"integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==",
"requires": {
"safe-buffer": "^5.2.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"pify": { "pify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@@ -13306,6 +13341,97 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sanitize-html": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.1.1.tgz",
"integrity": "sha512-yb+I+K25cn6PzsRJb+kGdlOBC3SkvEwJrr/Xl+6YR42oDZDeu61yWeEMSEi0hCKlRSYlWWE4tUnF2Ds+c/M1Vg==",
"requires": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^4.1.0",
"is-plain-object": "^5.0.0",
"klona": "^2.0.3",
"parse-srcset": "^1.0.2",
"postcss": "^8.0.2"
},
"dependencies": {
"dom-serializer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.1.0.tgz",
"integrity": "sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA=="
},
"domhandler": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz",
"integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==",
"requires": {
"domelementtype": "^2.0.1"
}
},
"domutils": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.2.tgz",
"integrity": "sha512-NKbgaM8ZJOecTZsIzW5gSuplsX2IWW2mIK7xVr8hTQF2v1CJWTmLZ1HOCh5sH+IzVPAGE5IucooOkvwBRAdowA==",
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.0.1",
"domhandler": "^3.3.0"
}
},
"entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"htmlparser2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz",
"integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0",
"domutils": "^2.0.0",
"entities": "^2.0.0"
}
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"postcss": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.1.4.tgz",
"integrity": "sha512-LfqcwgMq9LOd8pX7K2+r2HPitlIGC5p6PoZhVELlqhh2YGDVcXKpkCseqan73Hrdik6nBd2OvoDPUaP/oMj9hQ==",
"requires": {
"colorette": "^1.2.1",
"line-column": "^1.0.2",
"nanoid": "^3.1.15",
"source-map": "^0.6.1"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"sax": { "sax": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -15193,6 +15319,14 @@
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",
"integrity": "sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA==" "integrity": "sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA=="
}, },
"vue-sanitize": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/vue-sanitize/-/vue-sanitize-0.2.1.tgz",
"integrity": "sha512-MhrfAaAqIQ8JXWfdlhK6f+H2WnZ7KQwkOgLNeUyfACI2bq/3HPZ2zeUQDathnLdsXMXAa8pJehOgPYtlaG9Jew==",
"requires": {
"sanitize-html": "^2.1.1"
}
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.9.5", "version": "0.9.11",
"author": "Book Pauk <bookpauk@gmail.com>", "author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0", "license": "CC0-1.0",
"repository": "bookpauk/liberama", "repository": "bookpauk/liberama",
@@ -8,7 +8,7 @@
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
"scripts": { "scripts": {
"dev": "nodemon --inspect --exec 'node server'", "dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
"build:client": "webpack --config build/webpack.prod.config.js", "build:client": "webpack --config build/webpack.prod.config.js",
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .", "build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .", "build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
@@ -72,6 +72,7 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"pako": "^1.0.11", "pako": "^1.0.11",
"path-browserify": "^1.0.0", "path-browserify": "^1.0.0",
"pidusage": "^2.0.21",
"quasar": "^1.14.3", "quasar": "^1.14.3",
"safe-buffer": "^5.2.0", "safe-buffer": "^5.2.0",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
@@ -81,6 +82,7 @@
"unbzip2-stream": "^1.3.3", "unbzip2-stream": "^1.3.3",
"vue": "github:bookpauk/vue", "vue": "github:bookpauk/vue",
"vue-router": "^3.1.6", "vue-router": "^3.1.6",
"vue-sanitize": "^0.2.1",
"vuex": "^3.1.2", "vuex": "^3.1.2",
"vuex-persistedstate": "^2.7.1", "vuex-persistedstate": "^2.7.1",
"webdav": "^2.10.2", "webdav": "^2.10.2",

View File

@@ -18,7 +18,7 @@ module.exports = {
loggingEnabled: true, loggingEnabled: true,
maxUploadFileSize: 50*1024*1024,//50Мб maxUploadFileSize: 50*1024*1024,//50Мб
maxTempPublicDirSize: 512*1024*1024,//512Мб maxTempPublicDirSize: 512*1024*1024,//512Мб + 20% квота если проблема с remoteWebDavStorage
maxUploadPublicDirSize: 200*1024*1024,//100Мб maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false, useExternalBookConverter: false,

View File

@@ -22,6 +22,7 @@ class ReaderController extends BaseController {
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true), enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false), skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
isText: (request.hasOwnProperty('isText') ? request.isText : false), isText: (request.hasOwnProperty('isText') ? request.isText : false),
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
}); });
const state = this.workerState.getState(workerId); const state = this.workerState.getState(workerId);
return (state ? state : {}); return (state ? state : {});

View File

@@ -136,8 +136,9 @@ class WebSocketController {
break; break;
i++; i++;
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера if (i > 3*60*1000/refreshPause) {//3 мин ждем телодвижений воркера
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws); this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
break;
} }
i = (prevProgress != state.progress || prevState != state.state ? 1 : i); i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
} }

View File

@@ -15,6 +15,13 @@ class FileDecompressor {
constructor(limitFileSize = 0) { constructor(limitFileSize = 0) {
this.detector = new FileDetector(); this.detector = new FileDetector();
this.limitFileSize = limitFileSize; this.limitFileSize = limitFileSize;
this.rarPath = '/usr/bin/rar';
this.rarExists = false;
(async() => {
if (await fs.pathExists(this.rarPath))
this.rarExists = true;
})();
} }
async decompressNested(filename, outputDir) { async decompressNested(filename, outputDir) {
@@ -30,7 +37,11 @@ class FileDecompressor {
files: [] files: []
}; };
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) { if (!fileType || !(
fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz'
|| fileType.ext == 'tar' || (this.rarExists && fileType.ext == 'rar')
)
) {
return result; return result;
} }
@@ -94,6 +105,11 @@ class FileDecompressor {
async decompress(fileExt, filename, outputDir) { async decompress(fileExt, filename, outputDir) {
let files = []; let files = [];
if (fileExt == 'rar' && this.rarExists) {
files = await this.unRar(filename, outputDir);
return files;
}
switch (fileExt) { switch (fileExt) {
case 'zip': case 'zip':
files = await this.unZip(filename, outputDir); files = await this.unZip(filename, outputDir);
@@ -119,17 +135,16 @@ class FileDecompressor {
try { try {
return await zip.unpack(filename, outputDir, { return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize, limitFileSize: this.limitFileSize,
limitFileCount: 1000, limitFileCount: 10000,
decodeEntryNameCallback: (nameRaw) => { decodeEntryNameCallback: (nameRaw) => {
return utils.bufferRemoveZeroes(nameRaw); return utils.bufferRemoveZeroes(nameRaw);
} }
} });
);
} catch (e) { } catch (e) {
fs.emptyDir(outputDir); fs.emptyDir(outputDir);
return await zip.unpack(filename, outputDir, { return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize, limitFileSize: this.limitFileSize,
limitFileCount: 1000, limitFileCount: 10000,
decodeEntryNameCallback: (nameRaw) => { decodeEntryNameCallback: (nameRaw) => {
nameRaw = utils.bufferRemoveZeroes(nameRaw); nameRaw = utils.bufferRemoveZeroes(nameRaw);
const enc = textUtils.getEncodingLite(nameRaw); const enc = textUtils.getEncodingLite(nameRaw);
@@ -156,7 +171,7 @@ class FileDecompressor {
if (this.limitFileSize) { if (this.limitFileSize) {
if ((await fs.stat(filename)).size > this.limitFileSize) { if ((await fs.stat(filename)).size > this.limitFileSize) {
reject('Файл слишком большой'); reject(new Error('Файл слишком большой'));
return; return;
} }
} }
@@ -224,6 +239,38 @@ class FileDecompressor {
})().catch(reject); }); })().catch(reject); });
} }
async unRar(filename, outputDir) {
try {
const args = ['x', '-p-', '-y', filename, `${outputDir}`];
const result = await utils.spawnProcess(this.rarPath, {
killAfter: 60,
args
});
if (result.code == 0) {
const files = [];
await utils.findFiles(async(file) => {
const stat = await fs.stat(file);
files.push({path: path.relative(outputDir, file), size: stat.size});
}, outputDir);
return files;
} else {
const error = `${result.code}|FORLOG|, exec: ${this.rarPath}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
throw new Error(`Архиватор Rar завершился с ошибкой: ${error}`);
}
} catch(e) {
if (e.status == 'killed') {
throw new Error('Слишком долгое ожидание архиватора Rar');
} else if (e.status == 'error') {
throw new Error(e.error);
} else {
throw new Error(e);
}
}
}
async gzipBuffer(buf) { async gzipBuffer(buf) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
zlib.gzip(buf, {level: 1}, (err, result) => { zlib.gzip(buf, {level: 1}, (err, result) => {

View File

@@ -54,7 +54,7 @@ class FileDetector {
fromBuffer(buffer, callback) { fromBuffer(buffer, callback) {
let result = null; let result = null;
//console.log(buffer);
const invalidSignaturesList = this.validateSigantures(); const invalidSignaturesList = this.validateSigantures();
if (invalidSignaturesList.length) { if (invalidSignaturesList.length) {
return callback(invalidSignaturesList); return callback(invalidSignaturesList);

View File

@@ -676,7 +676,9 @@
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" }, { "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" }, { "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" }, { "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" } { "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" },
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00220031002e0030002200" },
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00270031002e0030002700" }
] ]
} }
] ]

View File

@@ -28,7 +28,7 @@ class LimitedQueue {
get(onPlaceChange) { get(onPlaceChange) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.destroyed) if (this.destroyed)
reject('destroyed'); reject(new Error('destroyed'));
const take = () => { const take = () => {
if (this.freed <= 0) if (this.freed <= 0)
@@ -73,7 +73,7 @@ class LimitedQueue {
if (onPlaceChange) if (onPlaceChange)
onPlaceChange(this.listeners.length); onPlaceChange(this.listeners.length);
} else { } else {
reject('Превышен размер очереди ожидания'); reject(new Error('Превышен размер очереди ожидания'));
} }
} }
}); });

View File

@@ -1 +0,0 @@
test

View File

@@ -5,8 +5,9 @@ const he = require('he');
const LimitedQueue = require('../../LimitedQueue'); const LimitedQueue = require('../../LimitedQueue');
const textUtils = require('./textUtils'); const textUtils = require('./textUtils');
const utils = require('../../utils'); const utils = require('../../utils');
const xmlParser = require('../../xmlParser');
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек const queue = new LimitedQueue(3, 20, 2*60*1000);//2 минуты ожидание подвижек
class ConvertBase { class ConvertBase {
constructor(config) { constructor(config) {
@@ -14,7 +15,6 @@ class ConvertBase {
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`; this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
this.sofficePath = '/usr/bin/soffice'; this.sofficePath = '/usr/bin/soffice';
this.pdfToHtmlPath = '/usr/bin/pdftohtml';
} }
async run(data, opts) {// eslint-disable-line no-unused-vars async run(data, opts) {// eslint-disable-line no-unused-vars
@@ -27,9 +27,6 @@ class ConvertBase {
if (!await fs.pathExists(this.sofficePath)) if (!await fs.pathExists(this.sofficePath))
throw new Error('Внешний конвертер LibreOffice не найден'); throw new Error('Внешний конвертер LibreOffice не найден');
if (!await fs.pathExists(this.pdfToHtmlPath))
throw new Error('Внешний конвертер pdftohtml не найден');
} }
async execConverter(path, args, onData, abort) { async execConverter(path, args, onData, abort) {
@@ -42,20 +39,35 @@ class ConvertBase {
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.'); throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
} }
abort = (abort ? abort : () => false);
const myAbort = () => {
return q.abort() || abort();
}
try { try {
if (myAbort())
throw new Error('abort');
const result = await utils.spawnProcess(path, { const result = await utils.spawnProcess(path, {
killAfter: 600, killAfter: 3600,//1 час
args, args,
onData: (data) => { onData: (data) => {
if (queue.freed > 0)
q.resetTimeout(); q.resetTimeout();
onData(data); onData(data);
}, },
abort //будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
onUsage: (stats) => {
if (queue.freed > 0 && stats.cpu >= 10) {
q.resetTimeout();
onData('.');
}
},
onUsageInterval: 10,
abort: myAbort
}); });
if (result.code != 0) { if (result.code != 0) {
let error = result.code; const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
if (this.config.branch == 'development')
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`); throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
} }
} catch(e) { } catch(e) {
@@ -91,61 +103,14 @@ class ConvertBase {
} }
formatFb2(fb2) { formatFb2(fb2) {
let out = '<?xml version="1.0" encoding="utf-8"?>'; const out = xmlParser.formatXml({
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">'; FictionBook: {
out += this.formatFb2Node(fb2); _attrs: {xmlns: 'http://www.gribuser.ru/xml/fictionbook/2.0', 'xmlns:l': 'http://www.w3.org/1999/xlink'},
out += '</FictionBook>'; _a: [fb2],
return out;
} }
}, 'utf-8', this.repSpaces);
formatFb2Node(node, name) { return out.replace(/<p>\s*?<\/p>/g, '<empty-line/>');
let out = '';
if (Array.isArray(node)) {
for (const n of node) {
out += this.formatFb2Node(n);
}
} else if (typeof node == 'string') {
if (name)
out += `<${name}>${this.repSpaces(node)}</${name}>`;
else
out += this.repSpaces(node);
} else {
if (node._n)
name = node._n;
let attrs = '';
if (node._attrs) {
for (let attrName in node._attrs) {
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
}
}
let tOpen = '';
let tBody = '';
let tClose = '';
if (name)
tOpen += `<${name}${attrs}>`;
if (node.hasOwnProperty('_t'))
tBody += this.repSpaces(node._t);
for (let nodeName in node) {
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
continue;
const n = node[nodeName];
tBody += this.formatFb2Node(n, nodeName);
}
if (name)
tClose += `</${name}>`;
if (attrs == '' && name == 'p' && tBody.trim() == '')
out += '<empty-line/>'
else
out += `${tOpen}${tBody}${tClose}`;
}
return out;
} }
} }

View File

@@ -0,0 +1,119 @@
const fs = require('fs-extra');
const path = require('path');
const utils = require('../../utils');
const ConvertBase = require('./ConvertBase');
class ConvertDjvu extends ConvertBase {
check(data, opts) {
const {inputFiles} = opts;
return this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'djvu';
}
async run(data, opts) {
if (!this.check(data, opts))
return false;
const {inputFiles, callback, abort, uploadFileName} = opts;
const ddjvuPath = '/usr/bin/ddjvu';
if (!await fs.pathExists(ddjvuPath))
throw new Error('Внешний конвертер ddjvu не найден');
const tiffsplitPath = '/usr/bin/tiffsplit';
if (!await fs.pathExists(tiffsplitPath))
throw new Error('Внешний конвертер tiffsplitPath не найден');
const mogrifyPath = '/usr/bin/mogrify';
if (!await fs.pathExists(mogrifyPath))
throw new Error('Внешний конвертер mogrifyPath не найден');
const dir = `${inputFiles.filesDir}/`;
const inpFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
const tifFile = `${inpFile}.tif`;
//конвертируем в tiff
let perc = 0;
await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
perc = (perc < 100 ? perc + 1 : 40);
callback(perc);
}, abort);
const tifFileSize = (await fs.stat(tifFile)).size;
let limitSize = 3*this.config.maxUploadFileSize;
if (tifFileSize > limitSize) {
throw new Error(`Файл для конвертирования слишком большой|FORLOG| ${tifFileSize} > ${limitSize}`);
}
//разбиваем на файлы
await this.execConverter(tiffsplitPath, [tifFile, dir], null, abort);
await fs.remove(tifFile);
//конвертируем в jpg
await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
perc = (perc < 100 ? perc + 1 : 40);
callback(perc);
}, abort);
//читаем изображения
limitSize = 2*this.config.maxUploadFileSize;
let imagesSize = 0;
const loadImage = async(image) => {
image.data = (await fs.readFile(image.file)).toString('base64');
image.name = path.basename(image.file);
imagesSize += image.data.length;
if (imagesSize > limitSize) {
throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`);
}
}
let files = [];
await utils.findFiles(async(file) => {
if (path.extname(file) == '.jpg')
files.push({name: file, base: path.basename(file)});
}, dir);
files.sort((a, b) => a.base.localeCompare(b.base));
let images = [];
let loading = [];
files.forEach(f => {
const image = {file: f.name};
images.push(image);
loading.push(loadImage(image));
});
await Promise.all(loading);
//формируем fb2
let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo};
let pars = [];
let body = {_n: 'body', section: {_a: [pars]}};
let binary = [];
let fb2 = [desc, body, binary];
let title = '';
if (uploadFileName)
title = uploadFileName;
titleInfo['book-title'] = title;
for (const image of images) {
const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': 'image/jpeg'}, _t: image.data};
binary.push(img);
pars.push({_n: 'p', _t: ''});
pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
}
return this.formatFb2(fb2);
}
}
module.exports = ConvertDjvu;

View File

@@ -1,5 +1,6 @@
const ConvertBase = require('./ConvertBase'); const ConvertBase = require('./ConvertBase');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const textUtils = require('./textUtils');
class ConvertFb2 extends ConvertBase { class ConvertFb2 extends ConvertBase {
check(data, opts) { check(data, opts) {
@@ -9,26 +10,46 @@ class ConvertFb2 extends ConvertBase {
} }
async run(data, opts) { async run(data, opts) {
if (!this.check(data, opts)) let newData = data;
//Корректируем кодировку, 16-битные кодировки должны стать utf-8
const encoding = textUtils.getEncoding(newData);
if (encoding.indexOf('UTF-16') == 0) {
newData = Buffer.from(iconv.decode(newData, encoding));
}
if (!this.check(newData, opts))
return false; return false;
return this.checkEncoding(data); return this.checkEncoding(newData);
} }
checkEncoding(data) { checkEncoding(data) {
let result = data; let result = data;
const left = data.indexOf('<?xml version="1.0"'); let q = '"';
let left = data.indexOf('<?xml version="1.0"');
if (left < 0) {
left = data.indexOf('<?xml version=\'1.0\'');
q = '\'';
}
if (left >= 0) { if (left >= 0) {
const right = data.indexOf('?>', left); const right = data.indexOf('?>', left);
if (right >= 0) { if (right >= 0) {
const head = data.slice(left, right + 2).toString(); const head = data.slice(left, right + 2).toString();
const m = head.match(/encoding="(.*?)"/); const m = head.match(/encoding=['"](.*?)['"]/);
if (m) { if (m) {
let encoding = m[1].toLowerCase(); let encoding = m[1].toLowerCase();
if (encoding != 'utf-8') { if (encoding != 'utf-8') {
result = iconv.decode(data, encoding); //encoding может не соответсвовать реальной кодировке файла, поэтому:
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"')); let calcEncoding = textUtils.getEncoding(data);
if (calcEncoding.indexOf('ISO-8859') >= 0) {
calcEncoding = encoding;
}
result = iconv.decode(data, calcEncoding);
result = Buffer.from(result.toString().replace(m[0], `encoding=${q}utf-8${q}`));
} }
} }
} }

View File

@@ -2,7 +2,7 @@ const fs = require('fs-extra');
const ConvertHtml = require('./ConvertHtml'); const ConvertHtml = require('./ConvertHtml');
class ConvertDocX extends ConvertHtml { class ConvertFb3 extends ConvertHtml {
async check(data, opts) { async check(data, opts) {
const {inputFiles} = opts; const {inputFiles} = opts;
if (this.config.useExternalBookConverter && if (this.config.useExternalBookConverter &&
@@ -39,13 +39,14 @@ class ConvertDocX extends ConvertHtml {
const title = this.getTitle(text) const title = this.getTitle(text)
.replace(/<\/?p>/g, '') .replace(/<\/?p>/g, '')
; ;
text = `<title>${title}</title>` + text text = `<fb2-title>${title}</fb2-title>` + text
.replace(/<title>/g, '<br><b>') .replace(/<title>/g, '<br><b>')
.replace(/<\/title>/g, '</b><br>') .replace(/<\/title>/g, '</b><br>')
.replace(/<subtitle>/g, '<br><br><subtitle>') .replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
.replace(/<\/subtitle>/g, '</fb2-subtitle>')
; ;
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true}); return await super.run(Buffer.from(text), {skipCheck: true});
} }
} }
module.exports = ConvertDocX; module.exports = ConvertFb3;

View File

@@ -34,7 +34,6 @@ class ConvertHtml extends ConvertBase {
} else { } else {
isText = opts.isText; isText = opts.isText;
} }
let {cutTitle} = opts;
let titleInfo = {}; let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo}; let desc = {_n: 'description', 'title-info': titleInfo};
@@ -44,12 +43,17 @@ class ConvertHtml extends ConvertBase {
let fb2 = [desc, body, binary]; let fb2 = [desc, body, binary];
let title = ''; let title = '';
let author = '';
let inTitle = false; let inTitle = false;
let inSectionTitle = false;
let inAuthor = false;
let inSubTitle = false; let inSubTitle = false;
let inImage = false; let inImage = false;
let image = {}; let image = {};
let bold = false; let bold = false;
let italic = false; let italic = false;
let superscript = false;
let subscript = false;
let begining = true; let begining = true;
let spaceCounter = []; let spaceCounter = [];
@@ -62,7 +66,7 @@ class ConvertHtml extends ConvertBase {
}; };
const growParagraph = (text) => { const growParagraph = (text) => {
if (!pars.length) if (!pars.length || pars[pars.length - 1]._n != 'p')
newParagraph(); newParagraph();
const l = pars.length; const l = pars.length;
@@ -94,12 +98,16 @@ class ConvertHtml extends ConvertBase {
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
text = this.escapeEntities(text); text = this.escapeEntities(text);
if (!cutCounter && !(cutTitle && inTitle)) { if (!(cutCounter || inTitle || inSectionTitle || inSubTitle)) {
let tOpen = ''; let tOpen = '';
tOpen += (inSubTitle ? '<subtitle>' : ''); tOpen += (inSubTitle ? '<subtitle>' : '');
tOpen += (bold ? '<strong>' : ''); tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : ''); tOpen += (italic ? '<emphasis>' : '');
tOpen += (superscript ? '<sup>' : '');
tOpen += (subscript ? '<sub>' : '');
let tClose = '' let tClose = ''
tClose += (subscript ? '</sub>' : '');
tClose += (superscript ? '</sup>' : '');
tClose += (italic ? '</emphasis>' : ''); tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : ''); tClose += (bold ? '</strong>' : '');
tClose += (inSubTitle ? '</subtitle>' : ''); tClose += (inSubTitle ? '</subtitle>' : '');
@@ -110,12 +118,22 @@ class ConvertHtml extends ConvertBase {
if (inTitle && !title) if (inTitle && !title)
title = text; title = text;
if (inAuthor && !author)
author = text;
if (inSectionTitle) {
pars.unshift({_n: 'title', _t: text});
}
if (inSubTitle) {
pars.push({_n: 'subtitle', _t: text});
}
if (inImage) { if (inImage) {
image._t = text; image._t = text;
binary.push(image); binary.push(image);
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''}); pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
newParagraph();
} }
}; };
@@ -140,15 +158,27 @@ class ConvertHtml extends ConvertBase {
bold = true; bold = true;
break; break;
} }
if (tag == 'sup')
superscript = true;
if (tag == 'sub')
subscript = true;
} }
if (tag == 'title' || tag == 'cut-title') { if (tag == 'title' || tag == 'fb2-title') {
inTitle = true; inTitle = true;
if (tag == 'cut-title')
cutTitle = true;
} }
if (tag == 'subtitle') { if (tag == 'fb2-author') {
inAuthor = true;
}
if (tag == 'fb2-section-title') {
inSectionTitle = true;
}
if (tag == 'fb2-subtitle') {
inSubTitle = true; inSubTitle = true;
} }
@@ -179,12 +209,26 @@ class ConvertHtml extends ConvertBase {
bold = false; bold = false;
break; break;
} }
if (tag == 'sup')
superscript = false;
if (tag == 'sub')
subscript = false;
} }
if (tag == 'title' || tag == 'cut-title') if (tag == 'title' || tag == 'fb2-title')
inTitle = false; inTitle = false;
if (tag == 'subtitle') if (tag == 'fb2-author') {
inAuthor = false;
}
if (tag == 'fb2-section-title') {
inSectionTitle = false;
}
if (tag == 'fb2-subtitle')
inSubTitle = false; inSubTitle = false;
if (tag == 'fb2-image') if (tag == 'fb2-image')
@@ -195,10 +239,17 @@ class ConvertHtml extends ConvertBase {
sax.parseSync(buf, { sax.parseSync(buf, {
onStartNode, onEndNode, onTextNode, onStartNode, onEndNode, onTextNode,
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image']) innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
}); });
titleInfo['book-title'] = title; titleInfo['book-title'] = title;
if (author)
titleInfo.author = {'last-name': author};
body.section._a[0] = pars;
//console.log(JSON.stringify(fb2, null, 2));
//подозрение на чистый текст, надо разбить на параграфы //подозрение на чистый текст, надо разбить на параграфы
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) { if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
let total = 0; let total = 0;
@@ -218,7 +269,7 @@ class ConvertHtml extends ConvertBase {
let i = 0; let i = 0;
//если разброс не слишком большой, выделяем параграфы //если разброс не слишком большой, выделяем параграфы
if (d < 10 && spaceCounter.length) { if (d < 20 && spaceCounter.length) {
total /= 20; total /= 20;
i = spaceCounter.length - 1; i = spaceCounter.length - 1;
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--; while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
@@ -228,56 +279,49 @@ class ConvertHtml extends ConvertBase {
if (parIndent > 2) parIndent--; if (parIndent > 2) parIndent--;
let newPars = []; let newPars = [];
let curPar = {};
const newPar = () => { const newPar = () => {
newPars.push({_n: 'p', _t: ''}); curPar = {_n: 'p', _t: ''};
newPars.push(curPar);
}; };
const growPar = (text) => {
if (!newPars.length)
newPar();
const l = newPars.length;
newPars[l - 1]._t += text;
}
i = 0;
for (const par of pars) { for (const par of pars) {
if (par._n != 'p') { if (par._n != 'p') {
newPars.push(par); newPars.push(par);
continue; continue;
} }
if (i > 0)
newPar(); newPar();
i++;
let j = 0;
const lines = par._t.split('\n'); const lines = par._t.split('\n');
for (let line of lines) { for (let j = 0; j < lines.length; j++) {
line = repCrLfTab(line); const line = repCrLfTab(lines[j]);
let l = 0; let l = 0;
while (l < line.length && line[l] == ' ') { while (l < line.length && line[l] == ' ') {
l++; l++;
} }
if (l >= parIndent || line == '') { if (j > 0 &&
if (j > 0) (l >= parIndent ||
(j < lines.length - 1 && line == '')
)
) {
newPar(); newPar();
j++;
} }
growPar(line.trim() + ' ');
curPar._t += line.trim() + ' ';
} }
} }
body.section._a[0] = newPars; body.section._a[0] = newPars;
} else {
body.section._a[0] = pars;
} }
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги //убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
bold = false; bold = false;
italic = false; italic = false;
superscript = false;
subscript = false;
inSubTitle = false; inSubTitle = false;
pars = body.section._a[0]; pars = body.section._a[0];
for (let i = 0; i < pars.length; i++) { for (let i = 0; i < pars.length; i++) {
@@ -297,7 +341,11 @@ class ConvertHtml extends ConvertBase {
tOpen += (inSubTitle ? '<subtitle>' : ''); tOpen += (inSubTitle ? '<subtitle>' : '');
tOpen += (bold ? '<strong>' : ''); tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : ''); tOpen += (italic ? '<emphasis>' : '');
tOpen += (superscript ? '<sup>' : '');
tOpen += (subscript ? '<sub>' : '');
let tClose = '' let tClose = ''
tClose += (subscript ? '</sub>' : '');
tClose += (superscript ? '</sup>' : '');
tClose += (italic ? '</emphasis>' : ''); tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : ''); tClose += (bold ? '</strong>' : '');
tClose += (inSubTitle ? '</subtitle>' : ''); tClose += (inSubTitle ? '</subtitle>' : '');
@@ -313,6 +361,10 @@ class ConvertHtml extends ConvertBase {
bold = true; bold = true;
if (tag == 'emphasis') if (tag == 'emphasis')
italic = true; italic = true;
if (tag == 'sup')
superscript = true;
if (tag == 'sub')
subscript = true;
if (tag == 'subtitle') if (tag == 'subtitle')
inSubTitle = true; inSubTitle = true;
} }
@@ -322,6 +374,10 @@ class ConvertHtml extends ConvertBase {
bold = false; bold = false;
if (tag == 'emphasis') if (tag == 'emphasis')
italic = false; italic = false;
if (tag == 'sup')
superscript = false;
if (tag == 'sub')
subscript = false;
if (tag == 'subtitle') if (tag == 'subtitle')
inSubTitle = false; inSubTitle = false;
} }

View File

@@ -1,9 +1,11 @@
//const _ = require('lodash');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path'); const path = require('path');
const sax = require('../../sax'); const sax = require('../../sax');
const utils = require('../../utils'); const utils = require('../../utils');
const ConvertHtml = require('./ConvertHtml'); const ConvertHtml = require('./ConvertHtml');
const xmlParser = require('../../xmlParser');
class ConvertPdf extends ConvertHtml { class ConvertPdf extends ConvertHtml {
check(data, opts) { check(data, opts) {
@@ -16,15 +18,24 @@ class ConvertPdf extends ConvertHtml {
async run(notUsed, opts) { async run(notUsed, opts) {
if (!this.check(notUsed, opts)) if (!this.check(notUsed, opts))
return false; return false;
await this.checkExternalConverterPresent(); await this.checkExternalConverterPresent();
const {inputFiles, callback, abort} = opts; const {inputFiles, callback, abort, uploadFileName} = opts;
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`; const inpFile = inputFiles.sourceFile;
const outBasename = `${inputFiles.filesDir}/${utils.randomHexString(10)}`;
const outFile = `${outBasename}.xml`;
const metaFile = `${outBasename}_metadata.xml`;
const pdfaltoPath = `${this.config.dataDir}/pdfalto/pdfalto`;
if (!await fs.pathExists(pdfaltoPath))
throw new Error('Внешний конвертер pdfalto не найден');
//конвертируем в xml //конвертируем в xml
let perc = 0; let perc = 0;
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => { await this.execConverter(pdfaltoPath, [inpFile, outFile], () => {
perc = (perc < 80 ? perc + 10 : 40); perc = (perc < 80 ? perc + 10 : 40);
callback(perc); callback(perc);
}, abort); }, abort);
@@ -33,17 +44,22 @@ class ConvertPdf extends ConvertHtml {
const data = await fs.readFile(outFile); const data = await fs.readFile(outFile);
callback(90); callback(90);
await utils.sleep(100);
//парсим xml //парсим xml
let lines = []; let lines = [];
let pagelines = [];
let line = {text: ''};
let page = {};
let fonts = {};
let sectionTitleFound = false;
let images = []; let images = [];
let loading = []; let loading = [];
let inText = false;
let bold = false;
let italic = false;
let title = ''; let title = '';
let prevTop = 0; let author = '';
let i = -1; let i = -1;
let titleCount = 0;
const loadImage = async(image) => { const loadImage = async(image) => {
const src = path.parse(image.src); const src = path.parse(image.src);
@@ -57,7 +73,7 @@ class ConvertPdf extends ConvertHtml {
image.type = type; image.type = type;
image.name = src.base; image.name = src.base;
} }
} };
const putImage = (curTop) => { const putImage = (curTop) => {
if (!isNaN(curTop) && images.length) { if (!isNaN(curTop) && images.length) {
@@ -67,104 +83,180 @@ class ConvertPdf extends ConvertHtml {
images.shift(); images.shift();
} }
} }
};
const putPageLines = () => {
pagelines.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left))
//объединяем в одну строку равные по высоте
const pl = [];
let pt = 0;
let j = -1;
pagelines.forEach(line => {
//добавим закрывающий тег стиля
line.text += line.tClose;
//проверим, возможно это заголовок
if (line.fonts.length == 1 && line.pageWidth) {
const f = (line.fonts.length ? fonts[line.fonts[0]] : null);
const centerLeft = (line.pageWidth - line.width)/2;
if (f && f.isBold && Math.abs(centerLeft - line.left) < 3) {
if (!sectionTitleFound) {
line.isSectionTitle = true;
sectionTitleFound = true;
} else {
line.isSubtitle = true;
}
}
} }
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars //объединяем
if (!cutCounter && inText) { if (pt == 0 || Math.abs(pt - line.top) > 3) {
let tOpen = (bold ? '<b>' : ''); j++;
tOpen += (italic ? '<i>' : ''); pl[j] = line;
let tClose = (italic ? '</i>' : ''); } else {
tClose += (bold ? '</b>' : ''); pl[j].text += ` ${line.text}`;
}
pt = line.top;
});
lines[i].text += `${tOpen}${text}${tClose} `; //заполняем lines
if (titleCount < 2 && text.trim() != '') { const lastIndex = i;
title += text + (titleCount ? '' : ' - '); pl.forEach(line => {
titleCount++; putImage(line.top);
//добавим пустую строку, если надо
const prevLine = (i > lastIndex ? lines[i] : {fonts: [], top: 0});
if (prevLine && !prevLine.isImage) {
const f = (prevLine.fonts.length ? fonts[prevLine.fonts[0]] : (line.fonts.length ? fonts[line.fonts[0]] : null));
if (f && f.fontSize && !line.isImage && line.top - prevLine.top > f.fontSize*1.8) {
i++;
lines[i] = {text: '<br>'};
} }
} }
i++;
lines[i] = line;
});
pagelines = [];
putImage(100000);
}; };
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter) { if (tag == 'textstyle') {
if (inText) { const attrs = sax.getAttrsSync(tail);
switch (tag) { const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
case 'i': const fontStyle = (attrs.fontstyle && attrs.fontstyle.value ? attrs.fontstyle.value : '');
italic = true; const fontSize = (attrs.fontsize && attrs.fontsize.value ? attrs.fontsize.value : '');
break;
case 'b': if (fontId) {
bold = true; const styleTags = {bold: 'b', italics: 'i', superscript: 'sup', subscript: 'sub'};
break; const f = fonts[fontId] = {tOpen: '', tClose: '', isBold: false, fontSize};
if (fontStyle) {
const styles = fontStyle.split(' ');
styles.forEach(style => {
const s = styleTags[style];
if (s) {
f.tOpen += `<${s}>`;
f.tClose = `</${s}>${f.tClose}`;
if (s == 'b')
f.isBold = true;
}
});
}
} }
} }
if (tag == 'text' && !inText) { if (tag == 'page') {
let attrs = sax.getAttrsSync(tail); const attrs = sax.getAttrsSync(tail);
const line = { page = {
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
};
putPageLines();
}
if (tag == 'textline') {
const attrs = sax.getAttrsSync(tail);
line = {
text: '', text: '',
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10), top: parseInt((attrs.vpos && attrs.vpos.value ? attrs.vpos.value : null), 10),
left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10), left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.value : null), 10),
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10), width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10), height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
tOpen: '',
tClose: '',
isSectionTitle: false,
isSubtitle: false,
pageWidth: page.width,
fonts: [],
}; };
if (line.width != 0 || line.height != 0) { if (line.width != 0 || line.height != 0) {
inText = true; pagelines.push(line);
if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) {
putImage(line.top);
i++;
lines[i] = line;
}
prevTop = line.top;
} }
} }
if (tag == 'image') { if (tag == 'string') {
const attrs = sax.getAttrsSync(tail); const attrs = sax.getAttrsSync(tail);
const src = (attrs.src && attrs.src.value ? attrs.src.value : ''); if (attrs.content && attrs.content.value) {
let tOpen = '';
let tClose = '';
const fontId = (attrs.stylerefs && attrs.stylerefs.value ? attrs.stylerefs.value : '');
if (fontId && fonts[fontId]) {
tOpen = fonts[fontId].tOpen;
tClose = fonts[fontId].tClose;
if (!line.fonts.length || line.fonts[0] != fontId)
line.fonts.push(fontId);
}
if (line.tOpen != tOpen) {
line.text += line.tClose + tOpen;
line.tOpen = tOpen;
line.tClose = tClose;
}
line.text += `${line.text.length ? ' ' : ''}${attrs.content.value}`;
}
}
if (tag == 'illustration') {
const attrs = sax.getAttrsSync(tail);
if (attrs.type && attrs.type.value == 'image') {
let src = (attrs.fileid && attrs.fileid.value ? attrs.fileid.value : '');
if (src) { if (src) {
const image = { const image = {
isImage: true, isImage: true,
src, src,
data: '', data: '',
type: '', type: '',
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0, top: parseInt((attrs.vpos && attrs.vpos.value ? attrs.vpos.value : null), 10) || 0,
left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.value : null), 10) || 0,
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10) || 0,
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10) || 0,
}; };
const exists = images.filter(img => (img.top == image.top && img.left == image.left && img.width == image.width && img.height == image.height));
if (!exists.length) {
loading.push(loadImage(image)); loading.push(loadImage(image));
images.push(image); images.push(image);
images.sort((a, b) => a.top - b.top) images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
} }
} }
if (tag == 'page') {
putImage(100000);
} }
} }
}; };
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (inText) {
switch (tag) {
case 'i':
italic = false;
break;
case 'b':
bold = false;
break;
}
}
if (tag == 'text')
inText = false;
};
let buf = this.decode(data).toString(); let buf = this.decode(data).toString();
sax.parseSync(buf, { sax.parseSync(buf, {
onStartNode, onEndNode, onTextNode onStartNode
}); });
putImage(100000); putPageLines();
await Promise.all(loading); await Promise.all(loading);
await utils.sleep(100);
//найдем параграфы и отступы //найдем параграфы и отступы
const indents = []; const indents = [];
@@ -185,16 +277,51 @@ class ConvertPdf extends ConvertHtml {
} }
indents[0] = 0; indents[0] = 0;
//title
if (fs.pathExists(metaFile)) {
const metaXmlString = (await fs.readFile(metaFile)).toString();
let metaXmlParsed = xmlParser.parseXml(metaXmlString);
metaXmlParsed = xmlParser.simplifyXmlParsed(metaXmlParsed);
if (metaXmlParsed.metadata) {
title = (metaXmlParsed.metadata.title ? metaXmlParsed.metadata.title._t : '');
author = (metaXmlParsed.metadata.author ? metaXmlParsed.metadata.author._t : '');
}
}
if (!title && uploadFileName)
title = uploadFileName;
//console.log(JSON.stringify(lines, null, 2));
//формируем текст //формируем текст
let text = `<title>${title}</title>`; const limitSize = 2*this.config.maxUploadFileSize;
let text = '';
if (title)
text += `<fb2-title>${title}</fb2-title>`;
if (author)
text += `<fb2-author>${author}</fb2-author>`;
let concat = ''; let concat = '';
let sp = ''; let sp = '';
for (const line of lines) { for (const line of lines) {
if (text.length > limitSize) {
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
}
if (line.isImage) { if (line.isImage) {
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`; text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
continue; continue;
} }
if (line.isSectionTitle) {
text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
continue;
}
if (line.isSubtitle) {
text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
continue;
}
if (concat == '') { if (concat == '') {
const left = line.left || 0; const left = line.left || 0;
sp = ' '.repeat(indents[left]); sp = ' '.repeat(indents[left]);
@@ -212,7 +339,9 @@ class ConvertPdf extends ConvertHtml {
if (concat) if (concat)
text += sp + concat + "\n"; text += sp + concat + "\n";
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true}); //console.log(text);
await utils.sleep(100);
return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
} }
} }

View File

@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
if (text === false) if (text === false)
return false; return false;
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true}); return await super.run(Buffer.from(text), {skipCheck: true});
} }
getTitle(text) { getTitle(text) {
@@ -79,7 +79,7 @@ class ConvertSites extends ConvertHtml {
let book = this.getTitle(text); let book = this.getTitle(text);
book = book.replace(' (fb2) | Флибуста', ''); book = book.replace(' (fb2) | Флибуста', '');
const title = `<title>${author}${(author ? ' - ' : '')}${book}</title>`; const title = `<fb2-title>${author}${(author ? ' - ' : '')}${book}</fb2-title>`;
let begin = '<h3 class="book">'; let begin = '<h3 class="book">';
if (text.indexOf(begin) <= 0) if (text.indexOf(begin) <= 0)
@@ -95,12 +95,12 @@ class ConvertSites extends ConvertHtml {
return text.substring(l, r) return text.substring(l, r)
.replace(/blockquote class="?book"?/g, 'p') .replace(/blockquote class="?book"?/g, 'p')
.replace(/<br\/?>\s*<\/h3>/g, '</h3>') .replace(/<br\/?>\s*<\/h3>/g, '</h3>')
.replace(/<h3 class="?book"?>/g, '<br><br><subtitle>') .replace(/<h3 class="?book"?>/g, '<br><br><fb2-subtitle>')
.replace(/<h5 class="?book"?>/g, '<br><br><subtitle>') .replace(/<h5 class="?book"?>/g, '<br><br><fb2-subtitle>')
.replace(/<h3>/g, '<br><br><subtitle>') .replace(/<h3>/g, '<br><br><fb2-subtitle>')
.replace(/<h5>/g, '<br><br><subtitle>') .replace(/<h5>/g, '<br><br><fb2-subtitle>')
.replace(/<\/h3>/g, '</subtitle><br>') .replace(/<\/h3>/g, '</fb2-subtitle><br>')
.replace(/<\/h5>/g, '</subtitle><br>') .replace(/<\/h5>/g, '</fb2-subtitle><br>')
.replace(/<div class="?stanza"?>/g, '<br>') .replace(/<div class="?stanza"?>/g, '<br>')
.replace(/<div>/g, '<br>') .replace(/<div>/g, '<br>')
+ title; + title;

View File

@@ -4,6 +4,7 @@ const FileDetector = require('../../FileDetector');
//порядок важен //порядок важен
const convertClassFactory = [ const convertClassFactory = [
require('./ConvertEpub'), require('./ConvertEpub'),
require('./ConvertDjvu'),
require('./ConvertPdf'), require('./ConvertPdf'),
require('./ConvertRtf'), require('./ConvertRtf'),
require('./ConvertDocX'), require('./ConvertDocX'),

View File

@@ -12,7 +12,7 @@ const utils = require('../utils');
const log = new (require('../AppLogger'))().log;//singleton const log = new (require('../AppLogger'))().log;//singleton
const cleanDirPeriod = 60*60*1000;//1 раз в час const cleanDirPeriod = 60*60*1000;//1 раз в час
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
let instance = null; let instance = null;
@@ -30,7 +30,7 @@ class ReaderWorker {
this.workerState = new WorkerState(); this.workerState = new WorkerState();
this.down = new FileDownloader(config.maxUploadFileSize); this.down = new FileDownloader(config.maxUploadFileSize);
this.decomp = new FileDecompressor(2*config.maxUploadFileSize); this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
this.bookConverter = new BookConverter(this.config); this.bookConverter = new BookConverter(this.config);
this.remoteWebDavStorage = false; this.remoteWebDavStorage = false;
@@ -81,7 +81,7 @@ class ReaderWorker {
const decompDirname = utils.randomHexString(30); const decompDirname = utils.randomHexString(30);
//download or use uploaded //download or use uploaded
if (url.indexOf('file://') != 0) {//download if (url.indexOf('disk://') != 0) {//download
const downdata = await this.down.load(url, (progress) => { const downdata = await this.down.load(url, (progress) => {
wState.set({progress}); wState.set({progress});
}, q.abort); }, q.abort);
@@ -130,6 +130,8 @@ class ReaderWorker {
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`; convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => { await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
wState.set({progress}); wState.set({progress});
if (queue.freed > 0)
q.resetTimeout();
}, q.abort); }, q.abort);
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256 //сжимаем файл в tmp, если там уже нет с тем же именем-sha256
@@ -171,9 +173,15 @@ class ReaderWorker {
} catch (e) { } catch (e) {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
if (e.message == 'abort') let mes = e.message.split('|FORLOG|');
e.message = overLoadMes; if (mes[1])
wState.set({state: 'error', error: e.message}); log(LM_ERR, mes[0] + mes[1]);
log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
mes = mes[0];
if (mes == 'abort')
mes = overLoadMes;
wState.set({state: 'error', error: mes});
} finally { } finally {
//clean //clean
if (q) if (q)
@@ -208,7 +216,7 @@ class ReaderWorker {
await fs.remove(file.path); await fs.remove(file.path);
} }
return `file://${hash}`; return `disk://${hash}`;
} }
async restoreRemoteFile(filename) { async restoreRemoteFile(filename) {
@@ -266,30 +274,38 @@ class ReaderWorker {
files.push({name, stat}); files.push({name, stat});
} }
} }
log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files`); log(`clean dir ${dir}, maxSize=${maxSize}, found ${files.length} files, total size=${size}`);
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs); files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
let i = 0; let i = 0;
let j = 0;
while (i < files.length && size > maxSize) { while (i < files.length && size > maxSize) {
const file = files[i]; const file = files[i];
const oldFile = `${dir}/${file.name}`; const oldFile = `${dir}/${file.name}`;
let remoteSuccess = true;
//отправляем только this.config.tempPublicDir //отправляем только this.config.tempPublicDir
//TODO: убрать в будущем, т.к. уже делается ленивое сохранение compFilename в удаленном хранилище
if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) { if (this.remoteWebDavStorage && dir === this.config.tempPublicDir) {
remoteSuccess = false;
try { try {
//log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`); //log(`remoteWebDavStorage.putFile ${path.basename(oldFile)}`);
await this.remoteWebDavStorage.putFile(oldFile); await this.remoteWebDavStorage.putFile(oldFile);
remoteSuccess = true;
} catch (e) { } catch (e) {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
} }
} }
//реально удаляем только если сохранили в хранилище
if (remoteSuccess || size > maxSize*1.2) {
await fs.remove(oldFile); await fs.remove(oldFile);
j++;
}
size -= file.stat.size; size -= file.stat.size;
i++; i++;
} }
log(`removed ${i} files`); log(`removed ${j} files`);
} catch(e) { } catch(e) {
log(LM_ERR, e.stack); log(LM_ERR, e.stack);
} finally { } finally {

View File

@@ -76,13 +76,13 @@ class ZipStreamer {
if (limitFileCount || limitFileSize || decodeEntryNameCallback) { if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
const entries = Object.values(unzip.entries()); const entries = Object.values(unzip.entries());
if (limitFileCount && entries.length > limitFileCount) { if (limitFileCount && entries.length > limitFileCount) {
reject('Слишком много файлов'); reject(new Error('Слишком много файлов'));
return; return;
} }
for (const entry of entries) { for (const entry of entries) {
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) { if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
reject('Файл слишком большой'); reject(new Error('Файл слишком большой'));
return; return;
} }

View File

@@ -6,7 +6,8 @@ function parseSync(xstr, options) {
onCdata: _onCdata = dummy, onCdata: _onCdata = dummy,
onComment: _onComment = dummy, onComment: _onComment = dummy,
onProgress: _onProgress = dummy, onProgress: _onProgress = dummy,
innerCut = new Set() innerCut = new Set(),
lowerCase = true,
} = options; } = options;
let i = 0; let i = 0;
@@ -91,6 +92,7 @@ function parseSync(xstr, options) {
} else { } else {
tag = tagData; tag = tagData;
} }
if (lowerCase)
tag = tag.toLowerCase(); tag = tag.toLowerCase();
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) { if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
@@ -146,7 +148,8 @@ async function parse(xstr, options) {
onCdata: _onCdata = dummy, onCdata: _onCdata = dummy,
onComment: _onComment = dummy, onComment: _onComment = dummy,
onProgress: _onProgress = dummy, onProgress: _onProgress = dummy,
innerCut = new Set() innerCut = new Set(),
lowerCase = true,
} = options; } = options;
let i = 0; let i = 0;
@@ -231,6 +234,7 @@ async function parse(xstr, options) {
} else { } else {
tag = tagData; tag = tagData;
} }
if (lowerCase)
tag = tag.toLowerCase(); tag = tag.toLowerCase();
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) { if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
@@ -276,7 +280,7 @@ async function parse(xstr, options) {
await _onProgress(100); await _onProgress(100);
} }
function getAttrsSync(tail) { function getAttrsSync(tail, lowerCase = true) {
let result = {}; let result = {};
let name = ''; let name = '';
let value = ''; let value = '';
@@ -287,13 +291,16 @@ function getAttrsSync(tail) {
let waitEq = false; let waitEq = false;
const pushResult = () => { const pushResult = () => {
if (lowerCase)
name = name.toLowerCase();
if (name != '') { if (name != '') {
const fn = name;
let ns = ''; let ns = '';
if (name.indexOf(':') >= 0) { if (fn.indexOf(':') >= 0) {
[ns, name] = name.split(':'); [ns, name] = fn.split(':');
} }
result[name] = {value, ns}; result[name] = {value, ns, fn};
} }
name = ''; name = '';
value = ''; value = '';

View File

@@ -1,7 +1,9 @@
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const baseX = require('base-x'); const baseX = require('base-x');
const pidusage = require('pidusage');
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz'; const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
const bs36 = baseX(BASE36); const bs36 = baseX(BASE36);
@@ -45,10 +47,11 @@ async function touchFile(filename) {
} }
function spawnProcess(cmd, opts) { function spawnProcess(cmd, opts) {
let {args, killAfter, onData, abort} = opts; let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts;
killAfter = (killAfter ? killAfter : 120);//seconds killAfter = (killAfter ? killAfter : 120);//seconds
onData = (onData ? onData : () => {}); onData = (onData ? onData : () => {});
args = (args ? args : []); args = (args ? args : []);
onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds
return new Promise((resolve, reject) => { (async() => { return new Promise((resolve, reject) => { (async() => {
let resolved = false; let resolved = false;
@@ -75,9 +78,19 @@ function spawnProcess(cmd, opts) {
reject({status: 'error', error, stdout, stderr}); reject({status: 'error', error, stdout, stderr});
}); });
//ждем процесс, контролируем его работу раз в секунду
let onUsageCounter = onUsageInterval;
while (!resolved) { while (!resolved) {
await sleep(1000); await sleep(1000);
killAfter -= 1;
onUsageCounter--;
if (onUsage && onUsageCounter <= 0) {
const stats = await pidusage(proc.pid);
onUsage(stats);
onUsageCounter = onUsageInterval;
}
killAfter--;
if (killAfter <= 0 || (abort && abort())) { if (killAfter <= 0 || (abort && abort())) {
process.kill(proc.pid); process.kill(proc.pid);
if (killAfter <= 0) { if (killAfter <= 0) {
@@ -91,6 +104,22 @@ function spawnProcess(cmd, opts) {
})().catch(reject); }); })().catch(reject); });
} }
async function findFiles(callback, dir) {
if (!(callback && dir))
return;
let result = true;
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const found = path.resolve(dir, file.name);
if (file.isDirectory())
result = await findFiles(callback, found);
else
await callback(found);
}
return result;
}
module.exports = { module.exports = {
toBase36, toBase36,
fromBase36, fromBase36,
@@ -99,5 +128,6 @@ module.exports = {
sleep, sleep,
randomHexString, randomHexString,
touchFile, touchFile,
spawnProcess spawnProcess,
findFiles
}; };

143
server/core/xmlParser.js Normal file
View File

@@ -0,0 +1,143 @@
const sax = require('./sax');
function formatXml(xmlParsed, encoding = 'utf-8', textFilterFunc) {
let out = `<?xml version="1.0" encoding="${encoding}"?>`;
out += formatXmlNode(xmlParsed, textFilterFunc);
return out;
}
function formatXmlNode(node, textFilterFunc) {
textFilterFunc = (textFilterFunc ? textFilterFunc : text => text);
const formatNode = (node, name) => {
let out = '';
if (Array.isArray(node)) {
for (const n of node) {
out += formatNode(n);
}
} else if (typeof node == 'string') {
if (name)
out += `<${name}>${textFilterFunc(node)}</${name}>`;
else
out += textFilterFunc(node);
} else {
if (node._n)
name = node._n;
let attrs = '';
if (node._attrs) {
for (let attrName in node._attrs) {
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
}
}
let tOpen = '';
let tBody = '';
let tClose = '';
if (name)
tOpen += `<${name}${attrs}>`;
if (node.hasOwnProperty('_t'))
tBody += textFilterFunc(node._t);
for (let nodeName in node) {
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
continue;
const n = node[nodeName];
tBody += formatNode(n, nodeName);
}
if (name)
tClose += `</${name}>`;
out += `${tOpen}${tBody}${tClose}`;
}
return out;
}
return formatNode(node);
}
function parseXml(xmlString, lowerCase = true) {
let result = {};
let node = result;
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
node._t = text;
};
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (tag == '?xml')
return;
const newNode = {_n: tag, _p: node};
if (tail) {
const parsedAttrs = sax.getAttrsSync(tail, lowerCase);
const atKeys = Object.keys(parsedAttrs);
if (atKeys.length) {
const attrs = {};
for (let i = 0; i < atKeys.length; i++) {
const attrName = atKeys[i];
attrs[parsedAttrs[attrName].fn] = parsedAttrs[attrName].value;
}
newNode._attrs = attrs;
}
}
if (!node._a)
node._a = [];
node._a.push(newNode);
node = newNode;
};
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (node._p && node._n == tag)
node = node._p;
};
sax.parseSync(xmlString, {
onStartNode, onEndNode, onTextNode, lowerCase
});
if (result._a)
result = result._a[0];
return result;
}
function simplifyXmlParsed(node) {
const simplifyNodeArray = (a) => {
const result = {};
for (let i = 0; i < a.length; i++) {
const child = a[i];
if (child._n && !result[child._n]) {
result[child._n] = {};
if (child._a) {
result[child._n] = simplifyNodeArray(child._a);
}
if (child._t) {
result[child._n]._t = child._t;
}
if (child._attrs) {
result[child._n]._attrs = child._attrs;
}
}
}
return result;
};
return simplifyNodeArray([node]);
}
module.exports = {
formatXml,
formatXmlNode,
parseXml,
simplifyXmlParsed
}