Compare commits

...

125 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -141,6 +141,7 @@
@load-file="loadFile" @load-file="loadFile"
@book-pos-changed="bookPosChanged" @book-pos-changed="bookPosChanged"
@do-action="doAction" @do-action="doAction"
@hide-tool-bar="hideToolBar"
></component> ></component>
</keep-alive> </keep-alive>
@@ -193,6 +194,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager'; import bookManager from './share/bookManager';
import wallpaperStorage from './share/wallpaperStorage'; import wallpaperStorage from './share/wallpaperStorage';
import coversStorage from './share/coversStorage';
import dynamicCss from '../../share/dynamicCss'; import dynamicCss from '../../share/dynamicCss';
import rstore from '../../store/modules/reader'; import rstore from '../../store/modules/reader';
@@ -201,6 +203,7 @@ import miscApi from '../../api/misc';
import {versionHistory} from './versionHistory'; import {versionHistory} from './versionHistory';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import LockQueue from '../../share/LockQueue';
const componentOptions = { const componentOptions = {
components: { components: {
@@ -313,6 +316,8 @@ class Reader {
this.reader = this.$store.state.reader; this.reader = this.$store.state.reader;
this.config = this.$store.state.config; this.config = this.$store.state.config;
this.lock = new LockQueue(100);
this.$root.addEventHook('key', this.keyHook); this.$root.addEventHook('key', this.keyHook);
this.lastActivePage = false; this.lastActivePage = false;
@@ -345,6 +350,13 @@ class Reader {
this.debouncedSetRecentBook(newValue); this.debouncedSetRecentBook(newValue);
}, 15000, {maxWait: 20000}); }, 15000, {maxWait: 20000});
this.debouncedHideToolBar = _.debounce((event) => {
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
this.commit('reader/setToolBarActive', !!event.show);
this.$root.eventHook('resize');
}
}, 200);
document.addEventListener('fullscreenchange', () => { document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null); this.fullScreenActive = (document.fullscreenElement !== null);
}); });
@@ -355,6 +367,8 @@ class Reader {
mounted() { mounted() {
(async() => { (async() => {
await wallpaperStorage.init(); await wallpaperStorage.init();
await coversStorage.init();
await bookManager.init(this.settings); await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent); bookManager.addEventListener(this.bookManagerEvent);
@@ -402,6 +416,7 @@ class Reader {
this.clickControlActive = this.clickControl; this.clickControlActive = this.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton; this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.enableSitesFilter = settings.enableSitesFilter; this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify; this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara; this.splitToPara = settings.splitToPara;
@@ -438,22 +453,47 @@ class Reader {
//wallpaper css //wallpaper css
async loadWallpapers() { async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength(); if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
this.wallpaperDataLength = wallpaperDataLength;
let newCss = ''; let newCss = '';
let updated = false;
const wallpaperExists = new Set();
for (const wp of this.userWallpapers) { for (const wp of this.userWallpapers) {
const data = await wallpaperStorage.getData(wp.cssClass); wallpaperExists.add(wp.cssClass);
let data = await wallpaperStorage.getData(wp.cssClass);
if (!data) { if (!data) {
//здесь будем восстанавливать данные с сервера //здесь будем восстанавливать данные с сервера
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
try {
data = await readerApi.getUploadedFileBuf(url);
await wallpaperStorage.setData(wp.cssClass, data);
updated = true;
} catch (e) {
console.error(e);
}
} }
if (data) { if (data) {
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`; newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
} }
} }
//почистим wallpaperStorage
for (const key of await wallpaperStorage.getKeys()) {
if (!wallpaperExists.has(key)) {
await wallpaperStorage.removeData(key);
}
}
//обновим settings, если загружали обои из /upload/
if (updated) {
const newSettings = _.cloneDeep(this.settings);
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
this.commit('reader/setSettings', newSettings);
}
dynamicCss.replace('wallpapers', newCss); dynamicCss.replace('wallpapers', newCss);
} }
} }
@@ -662,6 +702,10 @@ class Reader {
this.$root.eventHook('resize'); this.$root.eventHook('resize');
} }
hideToolBar(event) {
this.debouncedHideToolBar(event);
}
fullScreenToggle() { fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive; this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) { if (this.fullScreenActive) {
@@ -897,7 +941,7 @@ class Reader {
refreshBook() { refreshBook() {
const mrb = this.mostRecentBook(); const mrb = this.mostRecentBook();
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true}); this.loadBook(Object.assign({}, mrb, {force: true}));
} }
undoAction() { undoAction() {
@@ -982,7 +1026,6 @@ class Reader {
classResult = classDisabled; classResult = classDisabled;
break; break;
case 'refresh': case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive) if (!this.mostRecentBookReactive)
classResult = classDisabled; classResult = classDisabled;
break; break;
@@ -1051,7 +1094,7 @@ class Reader {
return result; return result;
} }
async loadBook(opts) { async _loadBook(opts) {
if (!opts || !opts.url) { if (!opts || !opts.url) {
this.mostRecentBook(); this.mostRecentBook();
return; return;
@@ -1061,10 +1104,6 @@ class Reader {
let url = encodeURI(decodeURI(opts.url)); let url = encodeURI(decodeURI(opts.url));
//TODO: убрать конвертирование 'file://' после 06.2021
if (url.length == 71 && url.indexOf('file://') == 0)
url = url.replace(/^file/, 'disk');
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) && if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('disk://') != 0)) (url.indexOf('disk://') != 0))
url = 'http://' + url; url = 'http://' + url;
@@ -1091,33 +1130,37 @@ class Reader {
progress.show(); progress.show();
progress.setState({state: 'parse'}); progress.setState({state: 'parse'});
// есть ли среди недавних // есть ли среди загруженных
const key = bookManager.keyFromUrl(url); let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
let wasOpened = await bookManager.getRecentBook({key}); wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos); wasOpened = Object.assign(wasOpened, {
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen); url: (opts.url !== undefined ? opts.url : wasOpened.url),
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : ''); path: (opts.path !== undefined ? opts.path : wasOpened.path),
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
});
let book = null; let book = null;
if (!opts.force) { if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша // пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => { const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
progress.setState({progress: prog}); progress.setState({progress: prog});
}); });
// если есть в локальном кэше // если есть в локальном кэше
if (bookParsed) { if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed)); await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
this.mostRecentBook(); this.mostRecentBook();
this.addAction(bookPos); this.addAction(wasOpened.bookPos);
this.loaderActive = false; this.loaderActive = false;
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage(); this.blinkCachedLoadMessage();
this.checkBookPosPercent(); this.checkBookPosPercent();
await this.activateClickMapPage(); this.activateClickMapPage();//no await
return; return;
} }
@@ -1131,7 +1174,7 @@ class Reader {
}); });
book = Object.assign({}, wasOpened, {data: resp.data}); book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) { } catch (e) {
//молчим this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
} }
} }
} }
@@ -1142,7 +1185,7 @@ class Reader {
if (!book) { if (!book) {
book = await readerApi.loadBook({ book = await readerApi.loadBook({
url, url,
uploadFileName, uploadFileName: wasOpened.uploadFileName,
enableSitesFilter: this.enableSitesFilter, enableSitesFilter: this.enableSitesFilter,
skipHtmlCheck: (this.splitToPara ? true : false), skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false), isText: (this.splitToPara ? true : false),
@@ -1159,14 +1202,44 @@ class Reader {
// добавляем в bookManager // добавляем в bookManager
progress.setState({state: 'parse', step: 5}); progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (prog) => { const addedBook = await bookManager.addBook(book, (prog) => {
progress.setState({progress: prog}); progress.setState({progress: prog});
}); });
// sameBookKey
if (url.indexOf('disk://') == 0) {
//ищем такой файл в загруженных
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
found = (found ? _.cloneDeep(found) : found);
if (found) {
if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
await this.$root.stdDialog.askYesNo(`
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
<br>Объединить позицию?`, 'Найдена похожая книга');
if (askResult) {
wasOpened.bookPos = found.bookPos;
wasOpened.bookPosSeen = found.bookPosSeen;
wasOpened.sameBookKey = found.sameBookKey;
}
}
} else {
wasOpened.sameBookKey = wasOpened.uploadFileName;
}
} else {
wasOpened.sameBookKey = addedBook.url;
}
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
delete wasOpened.loadTime;
// добавляем в историю // добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook)); await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
this.mostRecentBook(); this.mostRecentBook();
this.addAction(bookPos); this.addAction(wasOpened.bookPos);
this.updateRoute(true); this.updateRoute(true);
this.loaderActive = false; this.loaderActive = false;
@@ -1177,11 +1250,11 @@ class Reader {
this.stopBlink = true; this.stopBlink = true;
this.checkBookPosPercent(); this.checkBookPosPercent();
await this.activateClickMapPage(); this.activateClickMapPage();//no await
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
if (!this.showHelpOnErrorIfNeeded(e.message)) { if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'}); this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
} }
} finally { } finally {
@@ -1189,7 +1262,16 @@ class Reader {
} }
} }
async loadFile(opts) { async loadBook(opts) {
await this.lock.get();
try {
await this._loadBook(opts);
} finally {
this.lock.ret();
}
}
async _loadFile(opts) {
this.progressActive = true; this.progressActive = true;
await this.$nextTick(); await this.$nextTick();
@@ -1205,7 +1287,7 @@ class Reader {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
await this.loadBook({url, uploadFileName: opts.file.name, force: true}); await this._loadBook({url, uploadFileName: opts.file.name, force: true});
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
@@ -1213,6 +1295,15 @@ class Reader {
} }
} }
async loadFile(opts) {
await this.lock.get();
try {
await this._loadFile(opts);
} finally {
this.lock.ret();
}
}
blinkCachedLoadMessage() { blinkCachedLoadMessage() {
if (!this.blinkCachedLoad) if (!this.blinkCachedLoad)
return; return;

View File

@@ -54,7 +54,7 @@
<br><br> <br><br>
<div class="row justify-center"> <div class="row justify-center">
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate"> <!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
Помочь проекту Помочь проекту
</q-btn--> </q-btn-->
</div> </div>

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
<template> <template>
<Window ref="window" height="95%" width="600px" @close="close"> <Window ref="window" width="600px" @close="close">
<template #header> <template #header>
Настройки Настройки
</template> </template>
<div class="col row"> <div class="col row">
<a ref="download" style="display: none;" target="_blank"></a>
<div class="full-height"> <div class="full-height">
<q-tabs <q-tabs
ref="tabs" ref="tabs"
@@ -24,7 +26,7 @@
<div v-show="tabsScrollable" class="q-pt-lg" /> <div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" /> <q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" /> <q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" /> <q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" /> <q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" /> <q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." /> <q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
@@ -82,8 +84,8 @@
</div> </div>
</div> </div>
<!-- Кнопки ----------------------------------------------------------------------> <!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel"> <div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ButtonsTab.inc'); @@include('./ToolBarTab.inc');
</div> </div>
<!-- Управление ------------------------------------------------------------------> <!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column"> <div v-if="selectedTab == 'keys'" class="fit column">
@@ -124,6 +126,7 @@ import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue'; import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage'; import wallpaperStorage from '../share/wallpaperStorage';
import readerApi from '../../../api/reader';
import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
import defPalette from './defPalette'; import defPalette from './defPalette';
@@ -636,8 +639,17 @@ class SettingsPage {
if (index < 0) if (index < 0)
newUserWallpapers.push({label, cssClass}); newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass)) if (!wallpaperStorage.keyExists(cssClass)) {
await wallpaperStorage.setData(cssClass, data); await wallpaperStorage.setData(cssClass, data);
//отправим data на сервер в файл `/upload/${key}`
try {
//const res =
await readerApi.uploadFileBuf(data);
//console.log(res);
} catch (e) {
console.error(e);
}
}
this.userWallpapers = newUserWallpapers; this.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass; this.wallpaper = cssClass;
@@ -664,6 +676,27 @@ class SettingsPage {
} }
} }
async downloadWallpaper() {
if (this.wallpaper.indexOf('user-paper') != 0)
return;
try {
const d = this.$refs.download;
const dataUrl = await wallpaperStorage.getData(this.wallpaper);
if (!dataUrl)
throw new Error('Файл обоев не найден');
d.href = dataUrl;
d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
d.click();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
keyHook(event) { keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') { if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close(); this.close();
@@ -702,11 +735,11 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px; margin-bottom: 5px;
} }
.label-1, .label-7 { .label-1, .label-3, .label-7 {
width: 75px; width: 75px;
} }
.label-2, .label-3, .label-4, .label-5 { .label-2, .label-4, .label-5 {
width: 110px; width: 110px;
} }

View File

@@ -0,0 +1,18 @@
<div class="part-header">Отображение</div>
<div class="item row no-wrap">
<div class="label-3"></div>
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Скрывать/показывть панель при прокрутке текста вперед/назад
</q-tooltip>
</q-checkbox>
</div>
<div class="part-header">Показывать кнопки</div>
<div class="item row no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div>
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>

View File

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

View File

@@ -102,6 +102,11 @@
Удалить выбранные обои Удалить выбранные обои
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Скачать выбранные обои
</q-tooltip>
</q-btn>
</div> </div>
</div> </div>

View File

@@ -66,7 +66,14 @@ const componentOptions = {
watch: { watch: {
bookPos: function() { bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
this.draw(); this.draw();
if (this.userBookPosChange) {
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
this.prevBookPos = this.bookPos;
this.userBookPosChange = false;
}
}, },
bookPosSeen: function() { bookPosSeen: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -99,6 +106,8 @@ class TextPage {
lastBook = null; lastBook = null;
bookPos = 0; bookPos = 0;
bookPosSeen = null; bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null; fontStyle = null;
fontSize = null; fontSize = null;
@@ -155,7 +164,7 @@ class TextPage {
this.$root.addEventHook('resize', async() => { this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
await utils.sleep(500); await utils.sleep(200);
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
}); });
} }
@@ -499,12 +508,25 @@ class TextPage {
} }
async onResize() { async onResize() {
if (this.resizing)
return;
this.resizing = true;
try { try {
const scrolled = this.doingScrolling;
if (scrolled)
await this.stopTextScrolling();
this.calcDrawProps(); this.calcDrawProps();
this.setBackground(); this.setBackground();
this.draw(); this.draw();
if (scrolled)
this.startTextScrolling();
} catch (e) { } catch (e) {
// //
} finally {
this.resizing = false;
} }
} }
@@ -652,7 +674,7 @@ class TextPage {
} }
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) { if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
this.doEnd(true); this.doEnd(true, false);
return; return;
} }
@@ -675,7 +697,7 @@ class TextPage {
this.debouncedDrawPageDividerAndOrnament(); this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) { if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true); this.doEnd(true, false);
return; return;
} }
} }
@@ -911,12 +933,14 @@ class TextPage {
doDown() { doDown() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) { if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesDown[1].begin; this.bookPos = this.linesDown[1].begin;
} }
} }
doUp() { doUp() {
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) { if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesUp[1].begin; this.bookPos = this.linesUp[1].begin;
} }
} }
@@ -929,6 +953,7 @@ class TextPage {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) { if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true; this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = this.linesDown[i].begin; this.bookPos = this.linesDown[i].begin;
} else } else
this.doEnd(); this.doEnd();
@@ -944,6 +969,7 @@ class TextPage {
if (i >= 0 && this.linesUp.length > i) { if (i >= 0 && this.linesUp.length > i) {
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false; this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = this.linesUp[i].begin; this.bookPos = this.linesUp[i].begin;
} }
} }
@@ -952,10 +978,11 @@ class TextPage {
doHome() { doHome() {
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false; this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = 0; this.bookPos = 0;
} }
doEnd(noAni) { doEnd(noAni, isUser = true) {
if (this.parsed.para.length && this.pageLineCount > 0) { if (this.parsed.para.length && this.pageLineCount > 0) {
let i = this.parsed.para.length - 1; let i = this.parsed.para.length - 1;
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1; let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
@@ -966,6 +993,7 @@ class TextPage {
if (!noAni) if (!noAni)
this.currentAnimation = this.pageChangeAnimation; this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true; this.pageChangeDirectionDown = true;
this.userBookPosChange = isUser;
this.bookPos = lines[i].begin; this.bookPos = lines[i].begin;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,40 @@
export const versionHistory = [ export const versionHistory = [
{
version: '0.11.8',
releaseDate: '2022-07-14',
showUntil: '2022-07-13',
content:
`
<ul>
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
<li>добавлена синхронизация обоев</li>
</ul>
`
},
{
version: '0.11.7',
releaseDate: '2022-07-12',
showUntil: '2022-07-19',
content:
`
<ul>
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
<li>изменения в окне загруженных книг:</li>
<ul>
<li>добавлена группировка по версиям файла одной и той же книги</li>
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
<li>добавлены различные методы сортировки списка загруженных книг</li>
<li>нумерация всегда осуществляется по времени загрузки</li>
</ul>
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
<li>исправления багов</li>
</ul>
`
},
{ {
version: '0.11.6', version: '0.11.6',
releaseDate: '2022-07-02', releaseDate: '2022-07-02',

View File

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

View File

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

View File

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

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

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

View File

@@ -363,4 +363,50 @@ export function getBookTitle(fb2) {
]).join(' - '); ]).join(' - ');
return result; return result;
}
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
return new Promise ((resolve, reject) => { (async() => {
const img = new Image();
let resolved = false;
img.onload = () => {
try {
let width = img.width;
let height = img.height;
if (width > height) {
if (width > toWidth) {
height = height * (toWidth / width);
width = toWidth;
}
} else {
if (height > toHeight) {
width = width * (toHeight / height);
height = toHeight;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
const result = canvas.toDataURL('image/jpeg', quality);
resolved = true;
resolve(result);
} catch (e) {
reject(e);
return;
}
};
img.onerror = reject;
img.src = dataUrl;
await sleep(1000);
if (!resolved)
reject('Не удалось изменить размер');
})().catch(reject); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

186
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.11.5", "version": "0.11.8",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "Liberama", "name": "Liberama",
"version": "0.11.5", "version": "0.11.8",
"hasInstallScript": true, "hasInstallScript": true,
"license": "CC0-1.0", "license": "CC0-1.0",
"dependencies": { "dependencies": {
@@ -21,15 +21,15 @@
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"he": "^1.2.0", "he": "^1.2.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jembadb": "^3.0.6", "jembadb": "^3.0.8",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"multer": "^1.4.3", "multer": "^1.4.5-lts.1",
"pako": "^2.0.4", "pako": "^2.0.4",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"pidusage": "^3.0.0", "pidusage": "^3.0.0",
"quasar": "^2.3.2", "quasar": "^2.7.5",
"safe-buffer": "^5.2.1", "safe-buffer": "^5.2.1",
"sanitize-html": "^2.5.3", "sanitize-html": "^2.5.3",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
@@ -38,8 +38,8 @@
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"tar-fs": "^2.1.1", "tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3", "unbzip2-stream": "^1.4.3",
"vue": "^3.2.22", "vue": "^3.2.37",
"vue-router": "^4.0.12", "vue-router": "^4.1.1",
"vuex": "^4.0.2", "vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0", "vuex-persistedstate": "^4.1.0",
"webdav": "^4.7.0", "webdav": "^4.7.0",
@@ -61,7 +61,7 @@
"css-loader": "^6.5.1", "css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^4.0.0", "css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-plugin-vue": "^9.1.1", "eslint-plugin-vue": "^9.2.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.4", "mini-css-extract-plugin": "^2.4.4",
"pkg": "^5.5.1", "pkg": "^5.5.1",
@@ -3239,15 +3239,14 @@
} }
}, },
"node_modules/busboy": { "node_modules/busboy": {
"version": "0.2.14", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": { "dependencies": {
"dicer": "0.2.5", "streamsearch": "^1.1.0"
"readable-stream": "1.1.x"
}, },
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=10.16.0"
} }
}, },
"node_modules/byte-length": { "node_modules/byte-length": {
@@ -4348,18 +4347,6 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"dependencies": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/dir-glob": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -4785,9 +4772,9 @@
} }
}, },
"node_modules/eslint-plugin-vue": { "node_modules/eslint-plugin-vue": {
"version": "9.1.1", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.2.0.tgz",
"integrity": "sha512-W9n5PB1X2jzC7CK6riG0oAcxjmKrjTF6+keL1rni8n57DZeilx/Fulz+IRJK3lYseLNAygN0I62L7DvioW40Tw==", "integrity": "sha512-W2hc+NUXoce8sZtWgZ45miQTy6jNyuSdub5aZ1IBune4JDeAyzucYX0TzkrQ1jMO52sNUDYlCIHDoaNePe0p5g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"eslint-utils": "^3.0.0", "eslint-utils": "^3.0.0",
@@ -6429,11 +6416,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -6538,9 +6520,9 @@
} }
}, },
"node_modules/jembadb": { "node_modules/jembadb": {
"version": "3.0.6", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/jembadb/-/jembadb-3.0.6.tgz", "resolved": "https://registry.npmjs.org/jembadb/-/jembadb-3.0.8.tgz",
"integrity": "sha512-JRs6bTPDMq/UbkI2OweSSGpJX20JiUSQyBJjAOAn1ITPkFGr7q8F3ClEio1gPcmUJryy6EWMX+UnxDS6smJdJA==", "integrity": "sha512-ukLawbH4IXsKeDTJWNmAEV7D9flQ0m3OMcDSbSjpFWimoyzSn7TyBhWSB8hqmM+9Dyqyh2/nZAcCBu2exXgirQ==",
"engines": { "engines": {
"node": ">=14.4.0" "node": ">=14.4.0"
} }
@@ -7242,22 +7224,20 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "1.4.4", "version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^0.2.11", "busboy": "^1.0.0",
"concat-stream": "^1.5.2", "concat-stream": "^1.5.2",
"mkdirp": "^0.5.4", "mkdirp": "^0.5.4",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4", "type-is": "^1.6.4",
"xtend": "^4.0.0" "xtend": "^4.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 6.0.0"
} }
}, },
"node_modules/multistream": { "node_modules/multistream": {
@@ -8802,9 +8782,9 @@
} }
}, },
"node_modules/quasar": { "node_modules/quasar": {
"version": "2.7.4", "version": "2.7.5",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.4.tgz", "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.5.tgz",
"integrity": "sha512-8OIa6azm7N6QUPjcZ5AhDCEBha5NnNqt+D1BMIteqaSqkVKFYBf+FMhUCC8R/Tc6Myz85vK7KGPn9tvaC6gXYQ==", "integrity": "sha512-DWI0S+bXASfMSPrB8c/LVsXpA4dF7cBUbaJlcrM+1ioTNBHtiudma2Nhk2SDd5bzk9AYVHh5A8JCZuKqQAXt7g==",
"engines": { "engines": {
"node": ">= 10.18.1", "node": ">= 10.18.1",
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
@@ -8924,17 +8904,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/rechoir": { "node_modules/rechoir": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
@@ -9735,18 +9704,13 @@
} }
}, },
"node_modules/streamsearch": { "node_modules/streamsearch": {
"version": "0.1.2", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=10.0.0"
} }
}, },
"node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -10611,11 +10575,11 @@
} }
}, },
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.0.16", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.16.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.1.tgz",
"integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==", "integrity": "sha512-Wp1mEf2xCwT0ez7o9JvgpfBp9JGnVb+dPERzXDbugTatzJAJ60VWOhJKifQty85k+jOreoFHER4r5fu062PhPw==",
"dependencies": { "dependencies": {
"@vue/devtools-api": "^6.0.0" "@vue/devtools-api": "^6.1.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/posva" "url": "https://github.com/sponsors/posva"
@@ -13960,12 +13924,11 @@
"dev": true "dev": true
}, },
"busboy": { "busboy": {
"version": "0.2.14", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==", "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"requires": { "requires": {
"dicer": "0.2.5", "streamsearch": "^1.1.0"
"readable-stream": "1.1.x"
} }
}, },
"byte-length": { "byte-length": {
@@ -14792,15 +14755,6 @@
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true "dev": true
}, },
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
}
},
"dir-glob": { "dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -15220,9 +15174,9 @@
} }
}, },
"eslint-plugin-vue": { "eslint-plugin-vue": {
"version": "9.1.1", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.2.0.tgz",
"integrity": "sha512-W9n5PB1X2jzC7CK6riG0oAcxjmKrjTF6+keL1rni8n57DZeilx/Fulz+IRJK3lYseLNAygN0I62L7DvioW40Tw==", "integrity": "sha512-W2hc+NUXoce8sZtWgZ45miQTy6jNyuSdub5aZ1IBune4JDeAyzucYX0TzkrQ1jMO52sNUDYlCIHDoaNePe0p5g==",
"dev": true, "dev": true,
"requires": { "requires": {
"eslint-utils": "^3.0.0", "eslint-utils": "^3.0.0",
@@ -16340,11 +16294,6 @@
"call-bind": "^1.0.2" "call-bind": "^1.0.2"
} }
}, },
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"isexe": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -16421,9 +16370,9 @@
} }
}, },
"jembadb": { "jembadb": {
"version": "3.0.6", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/jembadb/-/jembadb-3.0.6.tgz", "resolved": "https://registry.npmjs.org/jembadb/-/jembadb-3.0.8.tgz",
"integrity": "sha512-JRs6bTPDMq/UbkI2OweSSGpJX20JiUSQyBJjAOAn1ITPkFGr7q8F3ClEio1gPcmUJryy6EWMX+UnxDS6smJdJA==" "integrity": "sha512-ukLawbH4IXsKeDTJWNmAEV7D9flQ0m3OMcDSbSjpFWimoyzSn7TyBhWSB8hqmM+9Dyqyh2/nZAcCBu2exXgirQ=="
}, },
"jest-worker": { "jest-worker": {
"version": "27.5.1", "version": "27.5.1",
@@ -16980,16 +16929,15 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"multer": { "multer": {
"version": "1.4.4", "version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==", "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"requires": { "requires": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^0.2.11", "busboy": "^1.0.0",
"concat-stream": "^1.5.2", "concat-stream": "^1.5.2",
"mkdirp": "^0.5.4", "mkdirp": "^0.5.4",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4", "type-is": "^1.6.4",
"xtend": "^4.0.0" "xtend": "^4.0.0"
} }
@@ -18077,9 +18025,9 @@
} }
}, },
"quasar": { "quasar": {
"version": "2.7.4", "version": "2.7.5",
"resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.4.tgz", "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.7.5.tgz",
"integrity": "sha512-8OIa6azm7N6QUPjcZ5AhDCEBha5NnNqt+D1BMIteqaSqkVKFYBf+FMhUCC8R/Tc6Myz85vK7KGPn9tvaC6gXYQ==" "integrity": "sha512-DWI0S+bXASfMSPrB8c/LVsXpA4dF7cBUbaJlcrM+1ioTNBHtiudma2Nhk2SDd5bzk9AYVHh5A8JCZuKqQAXt7g=="
}, },
"querystring": { "querystring": {
"version": "0.2.1", "version": "0.2.1",
@@ -18158,17 +18106,6 @@
} }
} }
}, },
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"rechoir": { "rechoir": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
@@ -18773,14 +18710,9 @@
} }
}, },
"streamsearch": { "streamsearch": {
"version": "0.1.2", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==" "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
}, },
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
@@ -19424,11 +19356,11 @@
} }
}, },
"vue-router": { "vue-router": {
"version": "4.0.16", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.16.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.1.tgz",
"integrity": "sha512-JcO7cb8QJLBWE+DfxGUL3xUDOae/8nhM1KVdnudadTAORbuxIC/xAydC5Zr/VLHUDQi1ppuTF5/rjBGzgzrJNA==", "integrity": "sha512-Wp1mEf2xCwT0ez7o9JvgpfBp9JGnVb+dPERzXDbugTatzJAJ60VWOhJKifQty85k+jOreoFHER4r5fu062PhPw==",
"requires": { "requires": {
"@vue/devtools-api": "^6.0.0" "@vue/devtools-api": "^6.1.4"
} }
}, },
"vue-style-loader": { "vue-style-loader": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.11.6", "version": "0.11.8",
"author": "Book Pauk <bookpauk@gmail.com>", "author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0", "license": "CC0-1.0",
"repository": "bookpauk/liberama", "repository": "bookpauk/liberama",
@@ -32,7 +32,7 @@
"css-loader": "^6.5.1", "css-loader": "^6.5.1",
"css-minimizer-webpack-plugin": "^4.0.0", "css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-plugin-vue": "^9.1.1", "eslint-plugin-vue": "^9.2.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.4", "mini-css-extract-plugin": "^2.4.4",
"pkg": "^5.5.1", "pkg": "^5.5.1",
@@ -59,15 +59,15 @@
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"he": "^1.2.0", "he": "^1.2.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jembadb": "^3.0.6", "jembadb": "^3.0.8",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"multer": "^1.4.3", "multer": "^1.4.5-lts.1",
"pako": "^2.0.4", "pako": "^2.0.4",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"pidusage": "^3.0.0", "pidusage": "^3.0.0",
"quasar": "^2.3.2", "quasar": "^2.7.5",
"safe-buffer": "^5.2.1", "safe-buffer": "^5.2.1",
"sanitize-html": "^2.5.3", "sanitize-html": "^2.5.3",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
@@ -76,8 +76,8 @@
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"tar-fs": "^2.1.1", "tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3", "unbzip2-stream": "^1.4.3",
"vue": "^3.2.22", "vue": "^3.2.37",
"vue-router": "^4.0.12", "vue-router": "^4.1.1",
"vuex": "^4.0.2", "vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0", "vuex-persistedstate": "^4.1.0",
"webdav": "^4.7.0", "webdav": "^4.7.0",

View File

@@ -49,7 +49,7 @@ module.exports = {
servers: [ servers: [
{ {
serverName: '1', serverName: '1',
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top' mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
ip: '0.0.0.0', ip: '0.0.0.0',
port: '33080', port: '33080',
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ class FileDownloader {
estSize = res.headers['content-length']; estSize = res.headers['content-length'];
} }
if (estSize > this.limitDownloadSize) { if (this.limitDownloadSize && estSize > this.limitDownloadSize) {
throw new Error('Файл слишком большой'); throw new Error('Файл слишком большой');
} }

View File

@@ -49,7 +49,7 @@ class BaseLog {
this.outputBuffer = []; this.outputBuffer = [];
await this.flushImpl(this.data) await this.flushImpl(this.data)
.catch(e => { console.log(e); ayncExit.exit(1); } ); .catch(e => { console.error(`Logger error: ${e}`); ayncExit.exit(1); } );
this.flushing = false; this.flushing = false;
} }
@@ -218,6 +218,8 @@ class Logger {
} else { } else {
console.log(mes); console.log(mes);
} }
return mes;
} }
async close() { async close() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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