Compare commits

...

369 Commits

Author SHA1 Message Date
Book Pauk
f926732070 Merge branch 'release/0.12.2-2' 2022-10-05 15:16:24 +07:00
Book Pauk
3fbe6e9d9b Улучшение обработки ошибок 2022-10-05 15:15:26 +07:00
Book Pauk
225230381f Добавлена чистка output перед сборкой 2022-10-01 13:39:02 +07:00
Book Pauk
b58d3a1b8b Поправки параметров CopyWebpackPlugin 2022-09-20 20:21:41 +07:00
Book Pauk
ffedce4351 Поправки обработки ошибок сервера 2022-09-12 15:23:22 +07:00
Book Pauk
a4fdb67913 Merge tag '0.12.2-1' into develop
0.12.2-1
2022-09-04 21:44:06 +07:00
Book Pauk
6ba46421b9 Merge branch 'release/0.12.2-1' 2022-09-04 21:43:54 +07:00
Book Pauk
d201961046 Поправка положения notify-сообщений 2022-09-04 21:42:50 +07:00
Book Pauk
614a7f9da7 Merge tag '0.12.2' into develop
0.12.2
2022-09-04 21:22:39 +07:00
Book Pauk
113ab3e596 Merge branch 'release/0.12.2' 2022-09-04 21:22:28 +07:00
Book Pauk
c95870bfe5 Добавлено сохранение во vuex настройки offlineModeActive 2022-09-04 21:20:21 +07:00
Book Pauk
e69e9335f9 Исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц 2022-09-04 21:19:30 +07:00
Book Pauk
fd21cd77dd Node 16 2022-09-01 21:13:31 +07:00
Book Pauk
d1880acaf9 Merge tag '0.12.1' into develop
0.12.1
2022-09-01 21:10:57 +07:00
Book Pauk
428b507257 Merge branch 'release/0.12.1' 2022-09-01 21:10:52 +07:00
Book Pauk
043dab0731 Версия 0.12.1 2022-09-01 21:08:56 +07:00
Book Pauk
a7b4d9c0d8 Добавлена форма доната 2022-09-01 21:05:22 +07:00
Book Pauk
6f9c95e351 Переход на node 16, актуализация пакетов 2022-09-01 15:36:28 +07:00
Book Pauk
7a53063ea8 Исправление багов 2022-09-01 15:31:16 +07:00
Book Pauk
ec4d5cac4f Поправлен баг 2022-08-16 23:40:40 +07:00
Book Pauk
f8557cba88 Исправление багов 2022-08-05 02:25:45 +07:00
Book Pauk
5dead039f5 Дебаг 2022-08-05 01:09:47 +07:00
Book Pauk
ea38392df4 Дебаг 2022-08-05 00:57:18 +07:00
Book Pauk
0cc9d90a94 Поправлен мелкий баг 2022-08-05 00:31:56 +07:00
Book Pauk
8c7b86c458 Поправлен баг 2022-08-05 00:16:54 +07:00
Book Pauk
0e29546fc5 Добавлены таймауты 2022-08-04 23:53:46 +07:00
Book Pauk
c9fa90d07c Поправлен donate-адрес 2022-08-04 15:08:43 +07:00
Book Pauk
7d8e0525b1 Активировал DonateHelpPage 2022-08-04 15:03:48 +07:00
Book Pauk
ddf69876a6 Добавлено сообщение при изменении чекбокса проверки обновления 2022-08-04 13:23:32 +07:00
Book Pauk
1d78e75e38 Merge tag '0.12.0-2' into develop
0.12.0-2
2022-08-03 15:58:49 +07:00
Book Pauk
7ed58fe3c6 Merge branch 'release/0.12.0-2' 2022-08-03 15:58:42 +07:00
Book Pauk
058c79570b Поправки багов 2022-08-03 15:52:48 +07:00
Book Pauk
ec8fbcdf38 Исправление багов 2022-08-03 15:34:24 +07:00
Book Pauk
76673295bf Добавлена автоотмена проверки обновлений книг по истечении заданного количества дней 2022-08-03 14:57:01 +07:00
Book Pauk
084401b9c3 Мелкие поправки 2022-08-03 14:53:58 +07:00
Book Pauk
49038b10f7 Улучшение обработки ошибок 2022-07-29 17:45:33 +07:00
Book Pauk
45ea26810a Улучшение fillCheckQueue 2022-07-28 20:22:38 +07:00
Book Pauk
18c8b2d803 Мелкие поправки 2022-07-28 18:50:56 +07:00
Book Pauk
f4a7482b3b Улучшение парсинга head-запроса 2022-07-28 18:38:49 +07:00
Book Pauk
32dff128f4 Улучшение парсинга head-запроса 2022-07-28 18:04:47 +07:00
Book Pauk
a00b2d6574 Исправлен баг 2022-07-27 23:29:52 +07:00
Book Pauk
10c6e7d522 Merge tag '0.12.0-1' into develop
0.12.0-1
2022-07-27 21:33:56 +07:00
Book Pauk
df6a256d51 Merge branch 'release/0.12.0-1' 2022-07-27 21:33:49 +07:00
Book Pauk
fbdb74ee68 Поправка текста 2022-07-27 21:33:22 +07:00
Book Pauk
9ad7250da0 Merge tag '0.12.0' into develop
0.12.0
2022-07-27 21:10:04 +07:00
Book Pauk
8c86984ea1 Merge branch 'release/0.12.0' 2022-07-27 21:09:59 +07:00
Book Pauk
834b3f6210 Версия 0.12.0 2022-07-27 21:09:42 +07:00
Book Pauk
105b8d5042 Мелкие поправки 2022-07-27 21:02:26 +07:00
Book Pauk
7ca8fd9ca1 Доработки отправки bookUrls 2022-07-27 20:50:39 +07:00
Book Pauk
0067c2800a Дебаг 2022-07-27 20:37:56 +07:00
Book Pauk
688c8796f4 Поправлен баг 2022-07-27 19:00:25 +07:00
Book Pauk
56af65742b Улучшение настроек для BookUpdateChecker 2022-07-27 18:49:51 +07:00
Book Pauk
629ad26d40 Доработки BookUpdateChecker 2022-07-27 17:55:29 +07:00
Book Pauk
4b0e499c10 Работа над BookUpdateChecker 2022-07-27 17:28:02 +07:00
Book Pauk
4697b46cba Работа над BookUpdateChecker 2022-07-27 16:50:24 +07:00
Book Pauk
7f17e7daed Работа над BookUpdateChecker 2022-07-27 15:40:46 +07:00
Book Pauk
a1fcb7597b Работа над BookUpdateChecker 2022-07-27 14:08:59 +07:00
Book Pauk
35e46d0685 Работа над BookUpdateChecker 2022-07-27 12:44:10 +07:00
Book Pauk
e2c0f3658b Улучшения ServerStorage 2022-07-27 11:42:39 +07:00
Book Pauk
a3541ec16a Работа над BookUpdateChecker 2022-07-26 20:37:49 +07:00
Book Pauk
08d0d3e7f3 Работа над BookUpdateChecker 2022-07-26 20:12:44 +07:00
Book Pauk
2c47b2bee3 Работа над BookUpdateChecker 2022-07-26 18:43:42 +07:00
Book Pauk
e6008b5ec4 Работа над BookUpdateChecker 2022-07-26 17:30:34 +07:00
Book Pauk
e214ddf8d5 Работа над BookUpdateChecker 2022-07-26 00:41:07 +07:00
Book Pauk
52927c6188 Работа над BookUpdateChecker 2022-07-26 00:11:15 +07:00
Book Pauk
92ca9dd983 Работа над BookUpdateChecker 2022-07-25 23:27:38 +07:00
Book Pauk
ed8be34c12 Работа над BookUpdateChecker 2022-07-25 17:52:57 +07:00
Book Pauk
93bddfd05e Переход на vuex-persist вместо vuex-persistedstate 2022-07-25 17:03:29 +07:00
Book Pauk
8c99101bb3 Обновление пакетов 2022-07-25 16:41:07 +07:00
Book Pauk
d874f9ded4 Актуализация пакетов 2022-07-25 16:30:38 +07:00
Book Pauk
d7be4d3d94 Окончательное избавление от sqlite в пользу jembadb 2022-07-25 16:12:15 +07:00
Book Pauk
a2fa312839 Merge tag '0.11.8-7' into develop
0.11.8-7
2022-07-19 00:52:43 +07:00
Book Pauk
f7e1e09928 Merge branch 'release/0.11.8-7' 2022-07-19 00:52:36 +07:00
Book Pauk
f0832b07cb Исправление привнесенного бага 2022-07-19 00:50:44 +07:00
Book Pauk
7c253df291 Merge tag '0.11.8-6' into develop
0.11.8-6
2022-07-19 00:36:00 +07:00
Book Pauk
bb7cd9cbde Merge branch 'release/0.11.8-6' 2022-07-19 00:35:55 +07:00
Book Pauk
56c4182985 Небольшой тюнинг 2022-07-19 00:35:12 +07:00
Book Pauk
cb6c7536bf Небольшой тюнинг 2022-07-19 00:32:52 +07:00
Book Pauk
fbfe8cbda0 Решение проблемы невалидного tls-сертификата 2022-07-19 00:27:54 +07:00
Book Pauk
6129d2d7eb Небольшие поправки 2022-07-19 00:14:18 +07:00
Book Pauk
16b30c922a Улучшение работы с удаленным хранилищем 2022-07-18 23:54:25 +07:00
Book Pauk
c42ad66be6 Merge tag '0.11.8-5' into develop
0.11.8-5
2022-07-17 21:15:37 +07:00
Book Pauk
f36c13fea1 Merge branch 'release/0.11.8-5' 2022-07-17 21:15:31 +07:00
Book Pauk
4fd9d579e0 Небольшие доработки remoteSent, оптимизация отправки файлов 2022-07-17 21:10:52 +07:00
Book Pauk
e65a8a13ea Рефакторинг 2022-07-17 20:04:23 +07:00
Book Pauk
6ddb97d43e Тюнинг таймаутов 2022-07-17 17:11:34 +07:00
Book Pauk
89082603de Merge tag '0.11.8-4' into develop
0.11.8-4
2022-07-17 16:54:15 +07:00
Book Pauk
a9a3227433 Merge branch 'release/0.11.8-4' 2022-07-17 16:53:59 +07:00
Book Pauk
60cb3514b2 Тюнинг таймаутов 2022-07-17 16:53:12 +07:00
Book Pauk
4aeaa05f0b Merge tag '0.11.8-3' into develop
0.11.8-3
2022-07-17 15:58:34 +07:00
Book Pauk
9c06552278 Merge branch 'release/0.11.8-3' 2022-07-17 15:58:28 +07:00
Book Pauk
000f8dde82 Переход на RemoteStorage 2022-07-17 15:43:12 +07:00
Book Pauk
9ffc218002 Поправка 2022-07-16 21:36:50 +07:00
Book Pauk
68a188f099 Конфиг nginx 2022-07-16 21:10:33 +07:00
Book Pauk
8829bb3810 Конфиг nginx 2022-07-16 21:07:16 +07:00
Book Pauk
5164d2f536 Merge tag '0.11.8-2' into develop
0.11.8-2
2022-07-16 21:02:05 +07:00
Book Pauk
451538fcf7 Merge branch 'release/0.11.8-2' 2022-07-16 21:01:56 +07:00
Book Pauk
82a02ef339 Удаление более ненужной функциональности 2022-07-16 20:48:50 +07:00
Book Pauk
b834d4951f Обработка ошибок 2022-07-16 20:40:21 +07:00
Book Pauk
edc3b669be Добавлено восстановление файлов из webdav 2022-07-16 20:35:34 +07:00
Book Pauk
522826311d Переделка механизма чистки папок и отправки через RemoteWebDavStorage 2022-07-16 20:24:37 +07:00
Book Pauk
e69b9951d5 Отключил проверку валидности tls-сертификата 2022-07-16 18:43:09 +07:00
Book Pauk
c6300222ea Мелкий рефакторинг 2022-07-16 17:54:27 +07:00
Book Pauk
5aa6ee899c Изменение механизма работы с /tmp и /upload (начало) 2022-07-16 17:35:32 +07:00
Book Pauk
4b76f97d2b Поправки конфигов nginx 2022-07-16 15:45:52 +07:00
Book Pauk
5ccfe71c55 Начало работы над BookUpdateChecker 2022-07-16 13:16:57 +07:00
Book Pauk
97fc902cdb Поправлен баг 2022-07-15 23:53:54 +07:00
Book Pauk
7e935951d7 Поправка разметки 2022-07-15 23:17:30 +07:00
Book Pauk
810c6d68d2 Поправка разметки 2022-07-15 23:14:09 +07:00
Book Pauk
003dc70f4f Merge tag '0.11.8-1' into develop
0.11.8-1
2022-07-15 18:14:12 +07:00
Book Pauk
371ff64a95 Merge branch 'release/0.11.8-1' 2022-07-15 18:14:06 +07:00
Book Pauk
b0de5adbf3 Добавлена возможность скачивать обои 2022-07-15 18:11:24 +07:00
Book Pauk
d1d2b07c33 Поправки разметки 2022-07-15 17:42:19 +07:00
Book Pauk
d9b2444c1a Улучшен механизм загрузки обложек 2022-07-15 17:36:49 +07:00
Book Pauk
e7fae27031 Убрал отладку 2022-07-15 17:17:00 +07:00
Book Pauk
eb0c7b0a32 Отладка 2022-07-15 17:11:58 +07:00
Book Pauk
3d7ad0dd9a Небюольшие оптимизации загрузки обложек 2022-07-15 17:05:17 +07:00
Book Pauk
ae04feb311 Merge tag '0.11.8' into develop
0.11.8
2022-07-15 02:11:03 +07:00
Book Pauk
7b59f911ef Merge branch 'release/0.11.8' 2022-07-15 02:10:58 +07:00
Book Pauk
d3444da647 Поправки разметки 2022-07-15 01:58:42 +07:00
Book Pauk
66738d0c9c К предыдущему 2022-07-15 01:51:28 +07:00
Book Pauk
7e187acd68 Версия 0.11.8 2022-07-15 01:50:17 +07:00
Book Pauk
c751372a54 Добавлен resizeImage 2022-07-15 01:38:25 +07:00
Book Pauk
7fc98fc7da Добавление отображения обложки (coverpage) в окне загруженных файлов 2022-07-15 00:47:24 +07:00
Book Pauk
b56f45694e Добавлен coversStorage для хранения coverpage 2022-07-15 00:45:56 +07:00
Book Pauk
091ca521ef Новые upload-методы 2022-07-15 00:45:09 +07:00
Book Pauk
c7a17b0a76 Добавлена синхронизация файлов обоев 2022-07-14 20:14:40 +07:00
Book Pauk
26468b996a Мелкая поправка 2022-07-14 20:12:37 +07:00
Book Pauk
c4e240d87c Увеличил maxPayloadSize 2022-07-14 20:11:17 +07:00
Book Pauk
04713f47c8 Небольшие поправки 2022-07-14 16:14:25 +07:00
Book Pauk
37ab3493db Merge tag '0.11.7-6' into develop
0.11.7-6
2022-07-14 03:52:50 +07:00
Book Pauk
a4cb3c628e Merge branch 'release/0.11.7-6' 2022-07-14 03:52:44 +07:00
Book Pauk
8492da8a13 Небольшое улучшение 2022-07-14 03:51:59 +07:00
Book Pauk
98d7c64a56 Исправление багов 2022-07-14 03:34:55 +07:00
Book Pauk
25f121e5ed Merge tag '0.11.7-5' into develop
0.11.7-5
2022-07-14 01:57:36 +07:00
Book Pauk
4c8797c99c Merge branch 'release/0.11.7-5' 2022-07-14 01:57:30 +07:00
Book Pauk
1155aa285d Лишние пробелы 2022-07-14 01:57:03 +07:00
Book Pauk
239bbb8263 Добавлено восстановление из архива 2022-07-14 01:55:09 +07:00
Book Pauk
e6b9330108 Добавление работы с архивом 2022-07-14 01:17:09 +07:00
Book Pauk
935b767c2e Поправил поведение buttonActiveClass 2022-07-14 00:31:24 +07:00
Book Pauk
8acf3295b5 Поправил разметку 2022-07-14 00:31:09 +07:00
Book Pauk
48c3a12fa0 Улучшение парсинга плохих fb2 2022-07-14 00:30:27 +07:00
Book Pauk
a1dea514b7 Поправка разметки 2022-07-13 23:47:55 +07:00
Book Pauk
d4788439cb Merge tag '0.11.7-4' into develop
0.11.7-4
2022-07-13 16:38:10 +07:00
Book Pauk
0a60ad354c Merge branch 'release/0.11.7-4' 2022-07-13 16:38:04 +07:00
Book Pauk
c565a20344 Поправки разметки 2022-07-13 16:37:47 +07:00
Book Pauk
735ee88f0b Merge tag '0.11.7-3' into develop
0.11.7-3
2022-07-13 16:34:22 +07:00
Book Pauk
9405ce2cc0 Merge branch 'release/0.11.7-3' 2022-07-13 16:34:16 +07:00
Book Pauk
115277d88a Поправки разметки 2022-07-13 16:34:00 +07:00
Book Pauk
6925c11dbd Merge tag '0.11.7-2' into develop
0.11.7-2
2022-07-13 16:25:11 +07:00
Book Pauk
984d835892 Merge branch 'release/0.11.7-2' 2022-07-13 16:25:05 +07:00
Book Pauk
23353a4960 Улучшен парсинг fb2 2022-07-13 16:23:52 +07:00
Book Pauk
955bcda032 Поправки разметки 2022-07-13 15:01:35 +07:00
Book Pauk
81ad5d7a2c Поправки разметки 2022-07-13 14:47:24 +07:00
Book Pauk
dada7980ec Merge tag '0.11.7-1' into develop
0.11.7-1
2022-07-12 19:23:38 +07:00
Book Pauk
511a308646 Merge branch 'release/0.11.7-1' 2022-07-12 19:23:33 +07:00
Book Pauk
65c8f2cc81 Небольшие поправки на панели, изменена нумерация на обратную 2022-07-12 19:21:26 +07:00
Book Pauk
238c18bc48 Merge tag '0.11.7' into develop
0.11.7
2022-07-12 19:08:35 +07:00
Book Pauk
873a08fee1 Merge branch 'release/0.11.7' 2022-07-12 19:08:27 +07:00
Book Pauk
7e89228803 Версия 0.11.7 2022-07-12 19:07:39 +07:00
Book Pauk
fc630923a4 Настройка методов сортировки 2022-07-12 18:50:35 +07:00
Book Pauk
928f911d03 Добавлены подсказки к кнопкам 2022-07-12 17:53:14 +07:00
Book Pauk
7ffcd3fe1b Поправки поведения при скроллинге 2022-07-12 17:33:03 +07:00
Book Pauk
0efbaf643a Поправил сообщение об ошибке 2022-07-12 17:32:19 +07:00
Book Pauk
f1bf8e54ae Добавлен метод scrollToActiveBook 2022-07-12 17:10:50 +07:00
Book Pauk
b4aa6ab6c8 Поправки поиска 2022-07-12 16:58:34 +07:00
Book Pauk
72431f0202 Работа над группировкой 2022-07-12 16:51:32 +07:00
Book Pauk
04a326c0e4 Работа над группировкой 2022-07-12 15:51:43 +07:00
Book Pauk
931966f4f3 Поправки разметки 2022-07-12 15:05:17 +07:00
Book Pauk
8808cc4779 Работа над группировкой по файлам 2022-07-12 14:46:34 +07:00
Book Pauk
988c959eba Работа над группировкой файлов 2022-07-12 04:05:51 +07:00
Book Pauk
c0b658d9e6 К предыдущему 2022-07-12 01:41:18 +07:00
Book Pauk
3190246f34 Улучшена реакция на onResize 2022-07-12 01:35:19 +07:00
Book Pauk
d957b4a5f9 Добавлена возможность автосокрытия панели при прокрутке 2022-07-12 01:03:44 +07:00
Book Pauk
bef9e5705c Поправки текстовых строк 2022-07-11 23:53:54 +07:00
Book Pauk
eb2affa518 Приведение input к единому стилю 2022-07-11 23:50:51 +07:00
Book Pauk
07b9a3c033 Мелкие правки 2022-07-11 22:28:48 +07:00
Book Pauk
3ca14ae06a Работа над группировкой 2022-07-11 22:26:34 +07:00
Book Pauk
7caa0c2112 Начало добавления группировки в RecentBooksPage 2022-07-11 20:11:38 +07:00
Book Pauk
9c69f5bc01 Поправил размер иконки 2022-07-11 20:10:51 +07:00
Book Pauk
125a2e0f17 Исправление багов 2022-07-11 17:12:17 +07:00
Book Pauk
1b4360b897 Дополнение в convertRecent 2022-07-11 16:26:03 +07:00
Book Pauk
4775d6e47b Поправлен баг 2022-07-10 20:07:33 +07:00
Book Pauk
33fc553c55 Добавлен запрос на объединение позиций при
обнаружении похожего файла в загруженных
2022-07-10 19:54:00 +07:00
Book Pauk
25cad81c50 Улучшение отображения загруженных 2022-07-10 19:53:30 +07:00
Book Pauk
02a2099c1f Поправлен z-index 2022-07-10 19:52:58 +07:00
Book Pauk
1cda186b1a Добавлен диалог askYesNo 2022-07-10 19:52:29 +07:00
Book Pauk
f10291b6c6 Поправка названия действия 2022-07-10 19:51:31 +07:00
Book Pauk
26ab5d6765 Рефакторинг 2022-07-10 18:27:05 +07:00
Book Pauk
5edeed0747 Изменение механизма хранения книг 2022-07-10 17:31:21 +07:00
Book Pauk
c878ce432f Небольшое исправление опознававния кодировки 2022-07-10 17:20:47 +07:00
Book Pauk
81798897c8 Изменения в механизме хранения книг:
теперь ориентируемся на "ключ-filepath", а не "ключ-url"
2022-07-10 16:38:54 +07:00
Book Pauk
63840fadbc К предыдущему 2022-07-10 14:59:39 +07:00
Book Pauk
36aa057035 Поправка цвета 2022-07-09 21:00:09 +07:00
Book Pauk
30afd2421c Рефакторинг 2022-07-09 20:50:31 +07:00
Book Pauk
53a1d90bd8 Улучшение поведения при очереди загрузки книг 2022-07-09 02:01:14 +07:00
Book Pauk
2ecf6beef2 Небольшой багфикс 2022-07-09 01:56:42 +07:00
Book Pauk
85910a20e9 Улучшение ContentsPage 2022-07-08 20:50:55 +07:00
Book Pauk
66cf7790b3 Улучшения ContentsPage 2022-07-08 19:09:57 +07:00
Book Pauk
4a9eb7e4bb Удалил устаревшее 2022-07-08 14:30:44 +07:00
Book Pauk
07446696c1 Поправлен цвет заголовка 2022-07-08 13:52:45 +07:00
Book Pauk
a29f9d9a4b Унификация размеров окон 2022-07-08 13:43:59 +07:00
Book Pauk
d49c9baec3 Унификация интерфейса 2022-07-08 13:34:53 +07:00
Book Pauk
8c9d4a12ee Настройка цветов 2022-07-08 13:24:13 +07:00
Book Pauk
fce69e4657 Настройка цветов 2022-07-08 13:21:42 +07:00
Book Pauk
b387509f88 Добавил блокировку при загрузке книг, теперь загружаются последовательно 2022-07-08 12:26:47 +07:00
Book Pauk
8dc8bdc0d6 Merge tag '0.11.6-2' into develop
0.11.6-2
2022-07-07 19:43:47 +07:00
Book Pauk
00caae8363 Merge branch 'release/0.11.6-2' 2022-07-07 19:43:40 +07:00
Book Pauk
2ead8570a7 Небольшая поправка 2022-07-07 19:39:02 +07:00
Book Pauk
408315466b Частичный откат предыдущих изменений 2022-07-07 19:38:17 +07:00
Book Pauk
c651836554 Поправки скриптов запуска 2022-07-07 19:33:32 +07:00
Book Pauk
03a1e70fce Поправки, чтобы не падал в случае детача скрина 2022-07-07 19:05:54 +07:00
Book Pauk
ab5a11a24f Убрал сайт flibs.in из сетевых библиотек 2022-07-07 17:42:05 +07:00
Book Pauk
8cd6ed472c Изменил client_max_body_size 100m 2022-07-07 17:37:25 +07:00
Book Pauk
055181b744 Исправлен баг выпадающих списков в оглавлении 2022-07-07 17:34:03 +07:00
Book Pauk
e331a3920b Актуализация пакетов 2022-07-07 17:29:47 +07:00
Book Pauk
c62bccb470 Улучшил журналирование ошибок БД 2022-07-07 16:24:59 +07:00
Book Pauk
ea351ea293 Merge tag '0.11.6-1' into develop
0.11.6-1
2022-07-04 12:23:55 +07:00
Book Pauk
d806a07c60 Merge branch 'release/0.11.6-1' 2022-07-04 12:23:48 +07:00
Book Pauk
c0ea096f1f Обновил jembadb 2022-07-04 12:22:27 +07:00
Book Pauk
011d4a1672 Merge tag '0.11.6' into develop
0.11.6
2022-07-02 17:41:42 +07:00
Book Pauk
4836a737c6 Merge branch 'release/0.11.6' 2022-07-02 17:41:34 +07:00
Book Pauk
5712b2ee17 Версия 0.11.6 2022-07-02 17:40:28 +07:00
Book Pauk
32dd17694e Улучшено копирование текстов со страницы 2022-07-02 17:36:12 +07:00
Book Pauk
3ebc932a6a Поправил список расширений 2022-07-02 14:46:22 +07:00
Book Pauk
8f351d9bef Удалил неиспользуемый код 2022-07-02 14:18:16 +07:00
Book Pauk
5ae3ea94e4 Добавлены типы файлов в диалог загрузки 2022-07-02 13:57:44 +07:00
Book Pauk
f203d453a4 Актуализация пакетов 2022-07-02 13:21:30 +07:00
Book Pauk
0d5cba121b Мелкий рефакторинг 2022-07-02 13:02:22 +07:00
Book Pauk
0cd6a48a46 Актуализация пакетов 2022-07-02 12:59:07 +07:00
Book Pauk
4e07ce2b5c Актуализация пакетов 2022-07-02 12:55:39 +07:00
Book Pauk
85a525e301 Актуализация пакета base-x 2022-07-02 12:46:10 +07:00
Book Pauk
03e4a6d723 Мелкий рефакторинг 2022-07-02 12:36:59 +07:00
Book Pauk
ab28af1abe Актуализация пакетов 2022-07-02 12:16:52 +07:00
Book Pauk
7fceed5301 Переход на axios 2022-07-02 12:16:19 +07:00
Book Pauk
0077816afa Улучшена обработка и журналирование ошибок 2022-07-02 12:07:42 +07:00
Book Pauk
cb01423147 Поправил настройки прокси 2022-07-02 00:00:13 +07:00
Book Pauk
61b0712d36 Переход на axios 2022-07-01 21:38:32 +07:00
Book Pauk
12d7843377 Merge tag '0.11.5' into develop
0.11.5
2022-04-15 16:42:40 +07:00
Book Pauk
9293c0a0d4 Merge branch 'release/0.11.5' 2022-04-15 16:42:35 +07:00
Book Pauk
bb9522197a 0.11.5 2022-04-15 16:41:24 +07:00
Book Pauk
450a2e0664 Поправки css 2022-04-15 16:38:34 +07:00
Book Pauk
41e35f3ec8 Поправки css 2022-04-15 16:09:41 +07:00
Book Pauk
a9bc98abe3 Рефакторинг 2022-04-15 15:12:28 +07:00
Book Pauk
47bca03532 Поправки подсказок 2022-04-15 15:02:02 +07:00
Book Pauk
942021371c Merge tag '0.11.4-2' into develop
0.11.4-2
2022-04-14 19:54:20 +07:00
Book Pauk
ea2f178730 Merge branch 'release/0.11.4-2' 2022-04-14 19:54:14 +07:00
Book Pauk
4b5c8d9efe Добавил подсказку 2022-04-14 19:53:47 +07:00
Book Pauk
28ebf13c3a Merge tag '0.11.4-1' into develop
0.11.4-1
2022-04-14 19:19:21 +07:00
Book Pauk
5d52e63dd9 Merge branch 'release/0.11.4-1' 2022-04-14 19:19:14 +07:00
Book Pauk
1a0e024050 Поправил баг 2022-04-14 19:18:49 +07:00
Book Pauk
e627a0d970 Merge tag '0.11.4' into develop
0.11.4
2022-04-14 19:05:36 +07:00
Book Pauk
48668d94ad Merge branch 'release/0.11.4' 2022-04-14 19:05:31 +07:00
Book Pauk
e08c431dd9 Версия 0.11.4 2022-04-14 19:05:07 +07:00
Book Pauk
5ee58ad6f0 Поправка багов 2022-04-14 19:00:04 +07:00
Book Pauk
ac0a4f0586 Добавлена кнопка 'Управление кликом' 2022-04-14 18:50:11 +07:00
Book Pauk
b6f4c153e5 Добавлена кнопка 'Загрузить из буфера обмена' 2022-04-14 18:34:41 +07:00
Book Pauk
4fdaf5f555 Добавлена кнопка 'Загрузить файл с диска' 2022-04-14 17:48:51 +07:00
Book Pauk
b4ee9d6c00 Скрыта опция "Помочь проекту".
Добавлена кнопка "Вызвать справку".
2022-04-14 17:27:29 +07:00
Book Pauk
7c73c74730 Добавлена подсказка при невалидном URL книги 2022-04-14 17:13:38 +07:00
Book Pauk
c20aa089fa npm 2022-03-29 17:45:57 +07:00
Book Pauk
b0e15c22ea Merge tag '0.11.3' into develop
0.11.3
2022-03-29 17:41:03 +07:00
Book Pauk
d58a2c065a Merge branch 'release/0.11.3' 2022-03-29 17:40:57 +07:00
Book Pauk
53135e7ee8 Поправка даты 2022-03-29 17:40:29 +07:00
Book Pauk
5c48ca9e6c Рефакторинг versionHistory, небольшие поправки 2022-03-29 17:37:24 +07:00
Book Pauk
c4a280f3d8 Скрыл устаревший чекбокс 2022-03-29 16:52:03 +07:00
Book Pauk
ba2943c722 Поправлен баг 2022-03-29 16:49:04 +07:00
Book Pauk
26f6ffc83a Убрал PayPal из списка 2022-03-29 16:25:26 +07:00
Book Pauk
bcf075a72c Доработки WebSocketConnection 2022-03-29 16:23:34 +07:00
Book Pauk
02d458d192 Миграция "jembadb" => "^2.3.0" 2022-03-29 15:49:48 +07:00
Book Pauk
a349d8af68 Обновил пакет JembaDb 2022-02-08 20:55:31 +07:00
Book Pauk
0dbaf32aac Merge tag '0.11.2' into develop
0.11.2
2022-01-11 23:25:23 +07:00
Book Pauk
e8c41ef3a8 Merge branch 'release/0.11.2' 2022-01-11 23:24:58 +07:00
Book Pauk
e43a44e986 0.11.2 2022-01-11 23:24:37 +07:00
Book Pauk
f14b8ed277 Добавлена реакция на сигнал SIGUSR2 2022-01-11 23:23:54 +07:00
Book Pauk
bbfe8a64cb Мелкая поправка 2022-01-11 23:11:04 +07:00
Book Pauk
bcf3c2dab0 Улучшение обработки ошибок 2022-01-11 22:23:35 +07:00
Book Pauk
d5404fd260 Убрал устаревший код 2022-01-11 21:30:43 +07:00
Book Pauk
54bc662e43 Поправил конфиг для nginx 2021-12-24 17:59:26 +07:00
Book Pauk
42546ca97e Обновление jembadb до версии 1.3.0 2021-12-21 20:21:32 +07:00
Book Pauk
5c13cf0eb9 Добавил -C GZip для pkg 2021-12-20 17:27:04 +07:00
Book Pauk
2a9d44ae9a Поправка конфига для eslint 2021-12-20 17:26:19 +07:00
Book Pauk
38414ae7b6 Переход на пакет jembadb 2021-12-17 20:05:57 +07:00
Book Pauk
3ecb3e80ac Удалил комментарии 2021-12-12 01:56:24 +07:00
Book Pauk
4968828488 Merge tag '0.11.1-2' into develop
0.11.1-2
2021-12-03 15:25:17 +07:00
Book Pauk
4db3cd24df Merge branch 'release/0.11.1-2' 2021-12-03 15:25:11 +07:00
Book Pauk
45c6d3da77 Поправил таймаут, улучшение скорости синхронизации 2021-12-03 15:16:39 +07:00
Book Pauk
4aab1da3c6 Merge tag '0.11.1-1' into develop
0.11.1-1
2021-12-03 15:03:46 +07:00
Book Pauk
bf5dfa1c15 Merge branch 'release/0.11.1-1' 2021-12-03 15:03:37 +07:00
Book Pauk
7549bdd2b4 Обновил pkg 2021-12-03 15:02:56 +07:00
Book Pauk
1bb2525ab2 Merge tag '0.11.1' into develop
0.11.1
2021-12-03 14:35:04 +07:00
Book Pauk
22a556f612 Merge branch 'release/0.11.1' 2021-12-03 14:34:56 +07:00
Book Pauk
056611e87c Версия 0.11.1 2021-12-03 14:34:36 +07:00
Book Pauk
6debe24880 Удален более ненужный файл 2021-12-03 14:30:57 +07:00
Book Pauk
56559bddab Мелкий рефакторинг 2021-12-03 14:28:17 +07:00
Book Pauk
9ec74eccb4 Добавлен папаметр forceAutoRepair 2021-12-03 14:21:50 +07:00
Book Pauk
3d2f45c20d Мелие поправки 2021-12-03 14:21:36 +07:00
Book Pauk
fb2eedd5ba Добавлен конвертер SQLITE -> JambaDb 2021-12-03 14:07:32 +07:00
Book Pauk
e278b4a00e Мелкие поправки 2021-12-02 18:39:28 +07:00
Book Pauk
0beaa611f6 Переход на JembaDb 2021-12-02 18:36:49 +07:00
Book Pauk
14ca2daa39 Небольшой рефакторинг 2021-12-01 22:09:48 +07:00
Book Pauk
714eb3ae83 Поправки по результату тестирования 2021-12-01 21:26:26 +07:00
Book Pauk
6286d663c9 Поправлен баг 2021-12-01 19:27:16 +07:00
Book Pauk
b5db2079d2 Jemba-миграции 2021-12-01 17:50:48 +07:00
Book Pauk
b3b30b9bd9 Поправил триггер для autorepair 2021-11-24 15:15:22 +07:00
Book Pauk
0b6a726503 Новый движок БД 2021-11-24 14:15:09 +07:00
Book Pauk
609334c5a6 Пометил модули устаревшими 2021-11-24 14:14:24 +07:00
Book Pauk
4852c7aec3 Добавлен модуль AsyncExit для выполненния cleanup-процедур перед выходом из приложения 2021-11-24 14:13:13 +07:00
Book Pauk
b1e3d33694 Merge tag '0.11.0-1' into develop
0.11.0-1
2021-11-22 21:12:42 +07:00
Book Pauk
2bfc557071 Merge branch 'release/0.11.0-1' 2021-11-22 21:12:35 +07:00
Book Pauk
e1216109bc Поправлен баг с maxBodyLength клиента WebDav 2021-11-22 21:12:02 +07:00
Book Pauk
990b8f390c Merge tag '0.11.0' into develop
0.11.0
2021-11-18 18:43:53 +07:00
Book Pauk
e6f6cd4ff3 Merge branch 'release/0.11.0' 2021-11-18 18:43:45 +07:00
Book Pauk
7deb745651 Поправил ссылку на инструкцию certbot 2021-11-18 18:28:25 +07:00
Book Pauk
70f3ca8067 Добавил beta-конфиг nginx для http 2021-11-18 18:25:04 +07:00
Book Pauk
bb8497a997 Поправил ошибку в доке 2021-11-18 18:22:08 +07:00
Book Pauk
2127e2ec0a Версия 0.11.0 2021-11-18 18:17:22 +07:00
Book Pauk
9be4011d54 Небольшая поправка формирования заголовка 2021-11-18 17:56:54 +07:00
Book Pauk
c534edfeb5 Поправки 2021-11-16 15:41:51 +07:00
Book Pauk
adc8cd7243 Переход на Vue 3 2021-11-16 15:05:00 +07:00
Book Pauk
522d2d3b9c Актуализация пакетов 2021-11-16 14:41:53 +07:00
Book Pauk
046933a05e Поправка багов 2021-11-16 14:32:54 +07:00
Book Pauk
9143288de2 Поправка копирования assets 2021-11-16 14:03:21 +07:00
Book Pauk
6053ca6c0e Настройка правильных редиректов роутера 2021-11-07 15:38:17 +07:00
Book Pauk
084197530e Форматирование кода 2021-11-07 15:38:05 +07:00
Book Pauk
9f366ca811 Поправлен баг resize 2021-11-07 14:49:33 +07:00
Book Pauk
7c07e6f004 Поправка бага 2021-11-01 19:08:17 +07:00
Book Pauk
3d4d7e0342 Переход на Vue 3 2021-11-01 18:23:58 +07:00
Book Pauk
1a8f241aad Переход на Vue 3, небольшая реструктуризация файлов 2021-11-01 17:56:45 +07:00
Book Pauk
33e938b76a Поправлен баг 2021-10-31 21:51:03 +07:00
Book Pauk
e2db546066 Переход на Vue 3 2021-10-31 21:28:31 +07:00
Book Pauk
def9ee52e2 Поправка разметки 2021-10-31 13:19:11 +07:00
Book Pauk
1afe10be03 Переход на Vue 3 2021-10-31 13:14:12 +07:00
Book Pauk
fa44641fa2 Актуализация пакетов 2021-10-31 13:00:41 +07:00
Book Pauk
9a1ef85c93 Переход на Vue 3 2021-10-29 19:11:10 +07:00
Book Pauk
b848cf5aa7 Переход на Vue 3 2021-10-29 18:24:23 +07:00
Book Pauk
8057e18ebc Переход на Vue 3 2021-10-29 16:33:38 +07:00
Book Pauk
76e09ef34e Переход на Vue 3, в процессе 2021-10-29 15:27:04 +07:00
Book Pauk
00cb2dc274 Переход на Vue 3, в процессе 2021-10-29 12:56:28 +07:00
Book Pauk
ed46e91432 Переход на Vue 3, в процессе 2021-10-29 12:21:53 +07:00
Book Pauk
88d75fb0d8 Переход на Vue 3, в процессе 2021-10-28 16:55:44 +07:00
Book Pauk
a1d7a73459 Переход на Vue 3, в процессе 2021-10-28 15:17:19 +07:00
Book Pauk
687f89729b Переход на Vue 3, в процессе 2021-10-28 14:53:22 +07:00
Book Pauk
6bf678e01f Функция для преобразования Vue-класса во Vue-компонент 2021-10-28 13:52:25 +07:00
Book Pauk
a18aec2f96 Переход на Vue 3 - начало, пока ничего не работает 2021-10-27 23:09:20 +07:00
Book Pauk
1c0cf303a0 Поправка настроек eslint 2021-10-27 16:11:20 +07:00
Book Pauk
5c7ae73982 Поправки по требованиям eslint 2021-10-27 15:07:25 +07:00
Book Pauk
4e9c69a1cf Настройка eslint 2021-10-27 15:05:37 +07:00
Book Pauk
78375be8bf Поправки по требованиям eslint 2021-10-27 15:05:18 +07:00
Book Pauk
b684725094 Настройка eslint 2021-10-27 04:11:07 +07:00
Book Pauk
ff52602c3a Мелкие поправки 2021-10-27 04:10:19 +07:00
Book Pauk
ce704c5e26 Актуализация пакетов 2021-10-27 01:28:16 +07:00
Book Pauk
4503e4ed17 Актуализация пакетов 2021-10-27 01:03:47 +07:00
Book Pauk
01c384c43a Актуализация пакетов 2021-10-27 00:52:57 +07:00
Book Pauk
dda2de58a8 Актуализация пакетов, в процессе 2021-10-27 00:06:42 +07:00
Book Pauk
0365acbf7a Актуализация пакетов 2021-10-26 01:05:09 +07:00
Book Pauk
bbf1ab7180 Актуализация пакетов 2021-10-26 00:35:45 +07:00
Book Pauk
83bf1f1d3a Актуализация пакетов 2021-10-26 00:18:18 +07:00
Book Pauk
fdf04fed0e Актуализация пакетов 2021-10-25 23:55:26 +07:00
Book Pauk
acce32bfa7 Актуализация пакетов 2021-10-25 16:15:15 +07:00
Book Pauk
614c45ac7d Актуализировал readme 2021-10-25 15:31:07 +07:00
Book Pauk
c4c0199a1b Merge tag '0.10.3' into develop
0.10.3
2021-10-25 01:54:07 +07:00
Book Pauk
a53ebb9355 Merge branch 'release/0.10.3' 2021-10-25 01:54:00 +07:00
Book Pauk
06e12930c7 Актуализирован конвертер для samlib.ru 2021-10-25 01:01:26 +07:00
Book Pauk
0f7655773a Версия 0.10.3 2021-10-20 18:29:52 +07:00
Book Pauk
26660461d4 Исправлен баг парсера с пустыми параграфами (содержащими только разметку) 2021-10-20 18:05:38 +07:00
Book Pauk
b41ee91db5 Актуализировал инструкцию 2021-10-20 15:49:07 +07:00
Book Pauk
746dd8d37a Актуализация инструкции 2021-10-20 15:42:09 +07:00
Book Pauk
fb4a57027d Merge tag '0.10.2' into develop
0.10.2
2021-10-19 23:33:48 +07:00
131 changed files with 21347 additions and 15057 deletions

6
.babelrc Normal file
View File

@@ -0,0 +1,6 @@
{
"presets": [['@babel/preset-env', { "targets": { "esmodules": true } }]],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}

View File

@@ -1,17 +1,18 @@
{ {
"parser": "vue-eslint-parser",
"parserOptions": { "parserOptions": {
"parser": "babel-eslint" "parser": "@babel/eslint-parser",
"sourceType": "module"
}, },
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:vue/essential" "plugin:vue/recommended"
], ],
"plugins": [ "plugins": [
"vue", "@babel"
"html",
"node"
], ],
"env": { "env": {
"es6": true,
"browser": true, "browser": true,
"node": true "node": true
}, },
@@ -24,6 +25,14 @@
"LM_TOTAL": false "LM_TOTAL": false
}, },
"rules": { "rules": {
"vue/html-indent": ["warn", 4, {
"alignAttributesVertically": false
}],
"vue/max-attributes-per-line": "off",
"vue/html-self-closing": "off",
"vue/no-v-html": "off",
"vue/no-v-model-argument": "off",
"strict": 0, "strict": 0,
"indent": [0, 4, { "indent": [0, 4, {
"SwitchCase": 1 "SwitchCase": 1

View File

@@ -1,43 +1,43 @@
# Liberama # Liberama
Браузерная онлайн-читалка книг и децентрализованная библиотека. Браузерная онлайн-читалка книг и децентрализованная библиотека.
Читалка ![](https://omnireader.ru/favicon.ico)[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS: Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
![](docs/assets/face.jpg) ![](docs/assets/face.jpg)
![](docs/assets/reader.jpg) ![](docs/assets/reader.jpg)
## VPS ## VPS
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md) Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
## Сборка проекта ## Сборка проекта
Необходима версия node.js не ниже 10. Необходима версия node.js не ниже 14.
``` ```
$ git clone https://github.com/bookpauk/liberama $ git clone https://github.com/bookpauk/liberama
$ cd liberama $ cd liberama
$ npm i $ npm i
``` ```
### Windows ### Windows
``` ```
$ npm run build:win $ npm run build:win
``` ```
### Linux ### Linux
``` ```
$ npm run build:linux $ npm run build:linux
``` ```
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла
### Разработка ### Разработка
``` ```
$ npm run dev $ npm run dev
``` ```
## Помочь проекту ## Помочь проекту
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85 * bitcoin: bc1q3tyumaj648pp2e69jalsez2lnt462ttc33nup9
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ * litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz * monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz

View File

@@ -4,7 +4,7 @@ const util = require('util');
const stream = require('stream'); const stream = require('stream');
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
const got = require('got'); const axios = require('axios');
const FileDecompressor = require('../server/core/FileDecompressor'); const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist'); const distDir = path.resolve(__dirname, '../dist');
@@ -23,30 +23,14 @@ async function main() {
await fs.ensureDir(tempDownloadDir); await fs.ensureDir(tempDownloadDir);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
//ipfs //ipfs
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`; const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
if (!await fs.pathExists(ipfsDecompressedFilename)) { if (!await fs.pathExists(ipfsDecompressedFilename)) {
// Скачиваем ipfs // Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz'; const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`)); const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
console.log(`done downloading ${ipfsRemoteUrl}`); console.log(`done downloading ${ipfsRemoteUrl}`);
//распаковываем //распаковываем

View File

@@ -1,71 +1,81 @@
const path = require('path'); const path = require('path');
//const webpack = require('webpack'); const DefinePlugin = require('webpack').DefinePlugin;
const VueLoaderPlugin = require('vue-loader/lib/plugin'); const { VueLoaderPlugin } = require('vue-loader');
const clientDir = path.resolve(__dirname, '../client'); const clientDir = path.resolve(__dirname, '../client');
module.exports = { module.exports = {
resolve: {
alias: {
ws: false,
//vue: '@vue/compat'
}
},
entry: [`${clientDir}/main.js`], entry: [`${clientDir}/main.js`],
output: { output: {
publicPath: '/app/', publicPath: '/app/',
clean: true
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.vue$/, test: /\.vue$/,
loader: "vue-loader" loader: 'vue-loader',
/*options: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}*/
}, },
{ {
test: /\.includer$/,
resourceQuery: /^\?vue/, resourceQuery: /^\?vue/,
use: path.resolve('build/includer.js') use: path.resolve(__dirname, 'includer.js')
}, },
{ {
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader', loader: 'babel-loader',
exclude: /node_modules/, exclude: /node_modules/,
query: { options: {
presets: [['@babel/preset-env', { targets: { esmodules: true } }]],
plugins: [ plugins: [
'syntax-dynamic-import', ['@babel/plugin-proposal-decorators', { legacy: true }]
'transform-decorators-legacy',
'transform-class-properties',
// ["component", { "libraryName": "element-ui", "styleLibraryName": `~${clientDir}/theme` } ]
] ]
} }
}, },
{ {
test: /\.gif$/, test: /\.(gif|png)$/,
loader: "url-loader", type: 'asset/inline',
options: {
name: "images/[name]-[hash:6].[ext]"
}
},
{
test: /\.png$/,
loader: "url-loader",
options: {
name: "images/[name]-[hash:6].[ext]"
}
}, },
{ {
test: /\.jpg$/, test: /\.jpg$/,
loader: "file-loader", type: 'asset/resource',
options: { generator: {
name: "images/[name]-[hash:6].[ext]" filename: 'images/[name]-[hash:6][ext]'
} },
}, },
{ {
test: /\.(ttf|eot|woff|woff2)$/, test: /\.(ttf|eot|woff|woff2)$/,
loader: "file-loader", type: 'asset/resource',
options: { generator: {
name: "fonts/[name]-[hash:6].[ext]" filename: 'fonts/[name]-[hash:6][ext]'
} },
}, },
] ]
}, },
plugins: [ plugins: [
new DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__QUASAR_SSR__: false,
__QUASAR_SSR_SERVER__: false,
__QUASAR_SSR_CLIENT__: false,
__QUASAR_VERSION__: false,
}),
new VueLoaderPlugin(), new VueLoaderPlugin(),
] ]
}; };

View File

@@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const merge = require('webpack-merge'); const { merge } = require('webpack-merge');
const baseWpConfig = require('./webpack.base.config'); const baseWpConfig = require('./webpack.base.config');
baseWpConfig.entry.unshift('webpack-hot-middleware/client'); baseWpConfig.entry.unshift('webpack-hot-middleware/client');
@@ -13,10 +13,11 @@ const clientDir = path.resolve(__dirname, '../client');
module.exports = merge(baseWpConfig, { module.exports = merge(baseWpConfig, {
mode: 'development', mode: 'development',
devtool: "#inline-source-map", devtool: 'inline-source-map',
output: { output: {
path: `${publicDir}/app`, path: `${publicDir}/app`,
filename: 'bundle.js' filename: 'bundle.js',
clean: true
}, },
module: { module: {
@@ -38,6 +39,6 @@ module.exports = merge(baseWpConfig, {
template: `${clientDir}/index.html.template`, template: `${clientDir}/index.html.template`,
filename: `${publicDir}/index.html` filename: `${publicDir}/index.html`
}), }),
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]) new CopyWebpackPlugin({patterns: [{context: `${clientDir}/assets`, from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
] ]
}); });

View File

@@ -1,12 +1,12 @@
const path = require('path'); const path = require('path');
//const webpack = require('webpack'); //const webpack = require('webpack');
const merge = require('webpack-merge'); const { merge } = require('webpack-merge');
const baseWpConfig = require('./webpack.base.config'); const baseWpConfig = require('./webpack.base.config');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin'); //const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const {GenerateSW} = require('workbox-webpack-plugin'); const {GenerateSW} = require('workbox-webpack-plugin');
@@ -18,7 +18,8 @@ module.exports = merge(baseWpConfig, {
mode: 'production', mode: 'production',
output: { output: {
path: `${publicDir}/app_new`, path: `${publicDir}/app_new`,
filename: 'bundle.[contenthash].js' filename: 'bundle.[contenthash].js',
clean: true
}, },
module: { module: {
rules: [ rules: [
@@ -34,19 +35,18 @@ module.exports = merge(baseWpConfig, {
optimization: { optimization: {
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
cache: true,
parallel: true, parallel: true,
terserOptions: { terserOptions: {
output: { format: {
comments: false, comments: false,
}, },
}, },
}), }),
new OptimizeCSSAssetsPlugin() new CssMinimizerWebpackPlugin()
] ]
}, },
plugins: [ plugins: [
new CleanWebpackPlugin([publicDir], {root: path.resolve(__dirname, '..')}), //new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "[name].[contenthash].css" filename: "[name].[contenthash].css"
}), }),
@@ -54,7 +54,9 @@ module.exports = merge(baseWpConfig, {
template: `${clientDir}/index.html.template`, template: `${clientDir}/index.html.template`,
filename: `${publicDir}/index.html` filename: `${publicDir}/index.html`
}), }),
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]), new CopyWebpackPlugin({patterns:
[{context: `${clientDir}/assets`, from: `${clientDir}/assets/*`, to: `${publicDir}/` }]
}),
new GenerateSW({ new GenerateSW({
cacheId: 'liberama', cacheId: 'liberama',
swDest: `${publicDir}/service-worker.js`, swDest: `${publicDir}/service-worker.js`,

View File

@@ -4,7 +4,7 @@ const util = require('util');
const stream = require('stream'); const stream = require('stream');
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
const got = require('got'); const axios = require('axios');
const FileDecompressor = require('../server/core/FileDecompressor'); const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist'); const distDir = path.resolve(__dirname, '../dist');
@@ -23,30 +23,14 @@ async function main() {
await fs.ensureDir(tempDownloadDir); await fs.ensureDir(tempDownloadDir);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
//ipfs //ipfs
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`; const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
if (!await fs.pathExists(ipfsDecompressedFilename)) { if (!await fs.pathExists(ipfsDecompressedFilename)) {
// Скачиваем ipfs // Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip'; const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`)); const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
console.log(`done downloading ${ipfsRemoteUrl}`); console.log(`done downloading ${ipfsRemoteUrl}`);
//распаковываем //распаковываем

View File

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

View File

@@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
import * as cryptoUtils from '../share/cryptoUtils';
import wsc from './webSocketConnection'; import wsc from './webSocketConnection';
const api = axios.create({ const api = axios.create({
@@ -119,32 +120,7 @@ class Reader {
estSize = response.headers['content-length']; estSize = response.headers['content-length'];
} }
} catch (e) { } catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака //
let response = null
try {
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
if (response.state == 'error') {
throw new Error(response.error);
}
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
} }
return estSize; return estSize;
@@ -174,11 +150,10 @@ class Reader {
return await axios.get(url, options); return await axios.get(url, options);
} }
async uploadFile(file, maxUploadFileSize, callback) { async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
if (!maxUploadFileSize)
maxUploadFileSize = 10*1024*1024;
if (file.size > maxUploadFileSize) if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`); throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData(); let formData = new FormData();
formData.append('file', file, file.name); formData.append('file', file, file.name);
@@ -219,12 +194,52 @@ class Reader {
const state = response.state; const state = response.state;
if (!state) if (!state)
throw new Error('Неверный ответ api'); throw new Error('Неверный ответ api');
if (response.state == 'error') { if (state == 'error') {
throw new Error(response.error); throw new Error(response.error);
} }
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;
}
async checkBuc(bookUrls) {
const response = await wsc.message(await wsc.send({action: 'check-buc', bookUrls}));
if (response.error)
throw new Error(response.error);
if (!response.data)
throw new Error(`response.data is empty`);
return response.data;
}
} }
export default new Reader(); export default new Reader();

View File

@@ -1,25 +1,28 @@
<template> <template>
<div class="fit row"> <div class="fit row">
<Notify ref="notify"/> <Notify ref="notify" />
<StdDialog ref="stdDialog"/> <StdDialog ref="stdDialog" />
<keep-alive v-if="showPage">
<router-view class="col"></router-view> <router-view v-slot="{ Component }">
</keep-alive> <keep-alive v-if="showPage">
<component :is="Component" class="col" />
</keep-alive>
</router-view>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from './vueComponent.js';
import Component from 'vue-class-component';
import Notify from './share/Notify.vue'; import Notify from './share/Notify.vue';
import StdDialog from './share/StdDialog.vue'; import StdDialog from './share/StdDialog.vue';
import sanitizeHtml from 'sanitize-html';
import miscApi from '../api/misc'; import miscApi from '../api/misc';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Notify, Notify,
StdDialog, StdDialog,
@@ -31,8 +34,9 @@ export default @Component({
} }
}, },
}) };
class App extends Vue { class App {
_options = componentOptions;
showPage = false; showPage = false;
itemRuText = { itemRuText = {
@@ -54,7 +58,7 @@ class App extends Vue {
//root route //root route
let cachedRoute = ''; let cachedRoute = '';
let cachedPath = ''; let cachedPath = '';
this.$root.rootRoute = () => { this.$root.getRootRoute = () => {
if (this.$route.path != cachedPath) { if (this.$route.path != cachedPath) {
cachedPath = this.$route.path; cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i); const m = cachedPath.match(/^(\/[^/]*).*$/i);
@@ -73,46 +77,50 @@ class App extends Vue {
} }
}); });
// set-app-title this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
this.$root.$on('set-app-title', this.setAppTitle);
//global keyHooks // setAppTitle
this.keyHooks = []; this.$root.setAppTitle = this.setAppTitle;
this.keyHook = (event) => {
for (const hook of this.keyHooks) //sanitize
this.$root.sanitize = sanitizeHtml;
//global event hooks
this.eventHooks = {};
this.$root.eventHook = (hookName, event) => {
if (!this.eventHooks[hookName])
return;
for (const hook of this.eventHooks[hookName])
hook(event); hook(event);
} }
this.$root.addKeyHook = (hook) => { this.$root.addEventHook = (hookName, hook) => {
if (this.keyHooks.indexOf(hook) < 0) if (!this.eventHooks[hookName])
this.keyHooks.push(hook); this.eventHooks[hookName] = [];
if (this.eventHooks[hookName].indexOf(hook) < 0)
this.eventHooks[hookName].push(hook);
} }
this.$root.removeKeyHook = (hook) => { this.$root.removeEventHook = (hookName, hook) => {
const i = this.keyHooks.indexOf(hook); if (!this.eventHooks[hookName])
return;
const i = this.eventHooks[hookName].indexOf(hook);
if (i >= 0) if (i >= 0)
this.keyHooks.splice(i, 1); this.eventHooks[hookName].splice(i, 1);
} }
document.addEventListener('keyup', (event) => { document.addEventListener('keyup', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
document.addEventListener('keypress', (event) => { document.addEventListener('keypress', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
window.addEventListener('resize', () => {
this.$root.$emit('resize');
});
}
routerReady() { window.addEventListener('resize', (event) => {
return new Promise ((resolve) => { this.$root.eventHook('resize', event);
this.$router.onReady(() => {
resolve();
});
}); });
} }
@@ -142,14 +150,14 @@ class App extends Vue {
if (navigator.storage && navigator.storage.persist) { if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist(); navigator.storage.persist();
} }
await this.routerReady(); await this.$router.isReady();
this.redirectIfNeeded(); this.redirectIfNeeded();
})(); })();
} }
toggleCollapse() { toggleCollapse() {
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse); this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
this.$root.$emit('resize'); this.$root.eventHook('resize');
} }
get isCollapse() { get isCollapse() {
@@ -184,7 +192,7 @@ class App extends Vue {
} }
get rootRoute() { get rootRoute() {
return this.$root.rootRoute(); return this.$root.getRootRoute();
} }
setAppTitle(title) { setAppTitle(title) {
@@ -194,7 +202,7 @@ class App extends Vue {
} else if (this.mode == 'omnireader') { } else if (this.mode == 'omnireader') {
document.title = `Omni Reader - всегда с вами`; document.title = `Omni Reader - всегда с вами`;
} else if (this.config && this.mode !== null) { } else if (this.config && this.mode !== null) {
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`; document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
} }
} else { } else {
document.title = title; document.title = title;
@@ -230,7 +238,7 @@ class App extends Vue {
const url = s[1] || ''; const url = s[1] || '';
const q = utils.parseQuery(s[0] || ''); const q = utils.parseQuery(s[0] || '');
if (url) { if (url) {
q.url = decodeURIComponent(url); q.url = url;
} }
window.history.replaceState({}, '', '/'); window.history.replaceState({}, '', '/');
@@ -239,6 +247,8 @@ class App extends Vue {
} }
} }
} }
export default vueComponent(App);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -261,6 +271,10 @@ body, html, #app {
font: normal 12pt ReaderDefault; font: normal 12pt ReaderDefault;
} }
.notify-margin {
margin-top: 55px;
}
.dborder { .dborder {
border: 2px solid magenta !important; border: 2px solid magenta !important;
} }

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Book {
})
class Book extends Vue {
created() { created() {
} }
} }
export default vueComponent(Book);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Card {
})
class Card extends Vue {
created() { created() {
} }
} }
export default vueComponent(Card);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,15 +1,16 @@
<template> <template>
<div> <div>
<keep-alive> <router-view v-slot="{ Component }">
<router-view></router-view> <keep-alive>
</keep-alive> <component :is="Component" />
</keep-alive>
</router-view>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
const selfRoute = '/cardindex'; const selfRoute = '/cardindex';
@@ -21,20 +22,32 @@ const tab2Route = [
]; ];
let lastActiveTab = null; let lastActiveTab = null;
export default @Component({ const componentOptions = {
watch: { watch: {
selectedTab: function(newValue, oldValue) { selectedTab: function(newValue) {
lastActiveTab = newValue; lastActiveTab = newValue;
this.setRouteByTab(newValue); this.setRouteByTab(newValue);
}, },
curRoute: function(newValue, oldValue) { curRoute: function(newValue) {
this.setTabByRoute(newValue); this.setTabByRoute(newValue);
}, },
}, },
}) };
class CardIndex extends Vue { class CardIndex {
_options = componentOptions;
selectedTab = null; selectedTab = null;
created() {
this.$watch(
() => this.$route.path,
(newValue) => {
if (newValue == '/cardindex' && this.isReader) {
this.$router.replace({ path: '/reader' });
}
}
)
}
mounted() { mounted() {
this.setTabByRoute(this.curRoute); this.setTabByRoute(this.curRoute);
} }
@@ -57,12 +70,22 @@ class CardIndex extends Vue {
} }
} }
get mode() {
return this.$store.state.config.mode;
}
get curRoute() { get curRoute() {
const m = this.$route.path.match(/^(\/[^\/]*\/[^\/]*).*$/i); const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
return (m ? m[1] : this.$route.path); return (m ? m[1] : this.$route.path);
} }
get isReader() {
return (this.mode !== null && (this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top'));
}
} }
export default vueComponent(CardIndex);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class History {
})
class History extends Vue {
created() { created() {
} }
} }
export default vueComponent(History);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Search {
})
class Search extends Vue {
created() { created() {
} }
} }
export default vueComponent(Search);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,63 +1,79 @@
<template> <template>
<Window ref="window" width="600px" height="95%" @close="close"> <Window ref="window" width="600px" height="95%" @close="close">
<template slot="header"> <template #header>
Настроить закладки Настроить закладки
</template> </template>
<div class="col column fit"> <div class="col column fit">
<div class="row items-center top-panel bg-grey-3"> <div class="row items-center top-panel bg-grey-3">
<q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected"> <q-btn :disabled="!selected" class="q-mr-md" round dense color="blue" icon="la la-check" size="16px" @click.stop="openSelected">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть выбранную закладку
</q-tooltip>
</q-btn> </q-btn>
<q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search"> <q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
<template v-slot: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>
</q-input> </q-input>
</div> </div>
<div class="col row"> <div class="col row">
<div class="left-panel column items-center no-wrap bg-grey-3"> <div class="left-panel column items-center no-wrap bg-grey-3">
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px"> <q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" size="14px" @click.stop="addBookmark">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить закладку
</q-tooltip>
</q-btn> </q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length"> <q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-minus" size="14px" @click.stop="delBookmark">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Удалить отмеченные закладки
</q-tooltip>
</q-btn> </q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0"> <q-btn :disabled="!selected || selected.indexOf('r-') == 0" class="q-mb-sm" round dense color="blue" icon="la la-edit" size="14px" @click.stop="editBookmark">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Редактировать закладку
</q-tooltip>
</q-btn> </q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length"> <q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" size="14px" @click.stop="moveBookmark(false)">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Переместить отмеченные вверх
</q-tooltip>
</q-btn> </q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length"> <q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" size="14px" @click.stop="moveBookmark(true)">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Переместить отмеченные вниз
</q-tooltip>
</q-btn> </q-btn>
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px"> <q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" size="14px" @click.stop="setDefaultBookmarks">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Установить по умолчанию
</q-tooltip>
</q-btn> </q-btn>
<div class="space"/> <div class="space" />
</div> </div>
<div class="col fit tree"> <div class="col fit tree">
<div v-show="nodes.length" class="checkbox-tick-all"> <div v-show="nodes.length" class="checkbox-tick-all">
<q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" /> <q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
</div> </div>
<q-tree <q-tree
v-model:selected="selected"
v-model:ticked="ticked"
v-model:expanded="expanded"
class="q-my-xs" class="q-my-xs"
:nodes="nodes" :nodes="nodes"
node-key="key" node-key="key"
tick-strategy="leaf" tick-strategy="leaf"
:selected.sync="selected"
:ticked.sync="ticked"
:expanded.sync="expanded"
selected-color="black" selected-color="black"
:filter="search" :filter="search"
no-nodes-label="Закладок пока нет" no-nodes-label="Закладок пока нет"
no-results-label="Ничего не найдено" no-results-label="Ничего не найдено"
> >
<template v-slot:default-header="p"> <template #default-header="p">
<div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div> <div class="q-px-xs" :class="{selected: selected == p.key}">
{{ p.node.label }}
</div>
</template> </template>
</q-tree> </q-tree>
</div> </div>
@@ -68,32 +84,31 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import * as lu from '../linkUtils'; import * as lu from '../linkUtils';
import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
const BookmarkSettingsProps = Vue.extend({ const componentOptions = {
props: {
libs: Object,
addBookmarkVisible: Boolean,
}
});
export default @Component({
components: { components: {
Window, Window,
}, },
watch: { watch: {
ticked: function() { ticked() {
this.checkAllTicked(); this.checkAllTicked();
}, },
} }
}) };
class BookmarkSettings extends BookmarkSettingsProps { class BookmarkSettings {
_options = componentOptions;
_props = {
libs: Object,
addBookmarkVisible: Boolean,
};
search = ''; search = '';
selected = ''; selected = '';
ticked = []; ticked = [];
@@ -308,6 +323,8 @@ class BookmarkSettings extends BookmarkSettingsProps {
} }
} }
export default vueComponent(BookmarkSettings);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,82 +1,114 @@
<template> <template>
<Window ref="window" @close="close" margin="2px"> <Window ref="window" margin="2px" @close="close">
<template slot="header"> <template #header>
{{ header }} {{ header }}
</template> </template>
<template slot="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>
</template> </template>
<div v-show="ready" class="col column" style="min-width: 600px"> <div v-show="ready" class="col column" style="min-width: 600px">
<div class="row items-center q-px-sm" style="height: 50px"> <div class="row items-center q-px-sm" style="height: 50px">
<q-select class="q-mr-sm" ref="rootLink" v-model="rootLink" :options="rootLinkOptions" @input="rootLinkInput" <q-select
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide" ref="rootLink"
v-model="rootLink"
class="q-mr-sm"
: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
>
<template v-slot:prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
</q-btn>
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
</q-btn>
</template>
<template v-slot:selected>
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
</template>
</q-select>
<q-select class="q-mr-sm" ref="selectedLink" v-model="selectedLink" :options="selectedLinkOptions" @input="selectedLinkInput" style="width: 50px"
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide" @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
> >
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Закладки</q-tooltip> <template #prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" size="12px" @click.stop="addBookmark">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить закладку
</q-tooltip>
</q-btn>
<q-btn round dense color="blue" icon="la la-bars" size="12px" @click.stop="bookmarkSettings">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Настроить закладки
</q-tooltip>
</q-btn>
</template>
<template #selected>
<div style="overflow: hidden; white-space: nowrap;">
{{ rootLinkWithoutProtocol }}
</div>
</template>
</q-select> </q-select>
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги" <q-select
ref="selectedLink"
v-model="selectedLink"
class="q-mr-sm"
:options="selectedLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Закладки
</q-tooltip>
</q-select>
<q-input
ref="input"
v-model="bookUrl"
class="col q-mr-sm"
outlined dense
bg-color="white"
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown" @focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
> >
<template v-slot:prepend> <template #prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px"> <q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" size="12px" @click="goToLink(selectedLink)">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Вернуться на стартовую страницу
</q-tooltip>
</q-btn> </q-btn>
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl"> <q-btn :disabled="!bookUrl" round dense color="blue" icon="la la-angle-double-down" size="12px" @click="openBookUrlInFrame">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Загрузить URL во фрейм
</q-tooltip>
</q-btn> </q-btn>
</template> </template>
<template v-slot:append> <template #append>
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px"> <q-btn round dense color="blue" icon="la la-cog" size="12px" @click.stop="optionsVisible = true">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Опции
</q-tooltip>
</q-btn> </q-btn>
</template> </template>
</q-input> </q-input>
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть <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> Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть в читалке
</q-tooltip>
</q-btn> </q-btn>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div class="col fit" ref="frameBox" style="position: relative;"> <div ref="frameBox" class="col fit" style="position: relative;">
<div ref="frameWrap" class="overflow-hidden"> <div ref="frameWrap" class="overflow-hidden">
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe> <iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
</div> </div>
@@ -84,41 +116,68 @@
</div> </div>
<Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible"> <Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
<template slot="header"> <template #header>
<div class="row items-center"> <div class="row items-center">
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon> <q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div> <div v-if="addBookmarkMode == 'edit'">
<div v-else>Добавить закладку</div> Редактировать закладку
</div>
<div v-else>
Добавить закладку
</div>
</div> </div>
</template> </template>
<div class="q-mx-md row"> <div class="q-mx-md row">
<q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" @keydown="bookmarkLinkKeyDown" <q-input
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus"> ref="bookmarkLink"
v-model="bookmarkLink"
class="col q-mr-sm"
outlined dense
bg-color="white"
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
>
</q-input> </q-input>
<q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px" <q-select
ref="defaultRootLink"
v-model="defaultRootLink"
class="q-mr-sm"
:options="defaultRootLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
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
> >
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Предустановленные ссылки</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Предустановленные ссылки
</q-tooltip>
</q-select> </q-select>
</div> </div>
<div class="q-mx-md q-mt-md"> <div class="q-mx-md q-mt-md">
<q-input class="col q-mr-sm" ref="bookmarkDesc" outlined dense bg-color="white" v-model="bookmarkDesc" @keydown="bookmarkDescKeyDown" <q-input
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus"> ref="bookmarkDesc"
v-model="bookmarkDesc"
class="col q-mr-sm"
outlined dense
bg-color="white"
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
>
</q-input> </q-input>
</div> </div>
<template slot="footer"> <template #footer>
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn> <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn> Отмена
</q-btn>
<q-btn :disabled="!bookmarkLink" class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark">
OK
</q-btn>
</template> </template>
</Dialog> </Dialog>
<Dialog ref="options" v-model="optionsVisible"> <Dialog ref="options" v-model="optionsVisible">
<template slot="header"> <template #header>
<div class="row items-center"> <div class="row items-center">
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon> <q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
Опции Опции
@@ -131,22 +190,29 @@
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" /> <q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
</div> </div>
<template slot="footer"> <template #footer>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn> <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">
OK
</q-btn>
</template> </template>
</Dialog> </Dialog>
</div> </div>
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible" <BookmarkSettings
@do-action="doAction" @close="closeBookmarkSettings"> v-if="bookmarkSettingsActive"
ref="bookmarkSettings"
:libs="libs"
:add-bookmark-visible="addBookmarkVisible"
@do-action="doAction" @close="closeBookmarkSettings"
>
</BookmarkSettings> </BookmarkSettings>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import Window from '../share/Window.vue'; import Window from '../share/Window.vue';
@@ -162,20 +228,20 @@ const proxySubst = {
'http://fantasy-worlds.org': 'http://b.liberama.top:23580', 'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
}; };
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
Dialog, Dialog,
BookmarkSettings BookmarkSettings
}, },
watch: { watch: {
libs: function() { libs() {
this.loadLibs(); this.loadLibs();
}, },
defaultRootLink: function() { defaultRootLink() {
this.updateBookmarkLink(); this.updateBookmarkLink();
}, },
bookUrl: function(newValue) { bookUrl(newValue) {
const value = lu.addProtocol(newValue); const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true); const subst = this.makeProxySubst(value, true);
if (value != subst) { if (value != subst) {
@@ -184,7 +250,7 @@ export default @Component({
}); });
} }
}, },
bookmarkLink: function(newValue) { bookmarkLink(newValue) {
const value = lu.addProtocol(newValue); const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true); const subst = this.makeProxySubst(value, true);
if (value != subst) { if (value != subst) {
@@ -193,18 +259,26 @@ export default @Component({
}); });
} }
}, },
closeAfterSubmit: function(newValue) { closeAfterSubmit(newValue) {
this.commitProp('closeAfterSubmit', newValue); this.commitProp('closeAfterSubmit', newValue);
}, },
openInFrameOnEnter: function(newValue) { openInFrameOnEnter(newValue) {
this.commitProp('openInFrameOnEnter', newValue); this.commitProp('openInFrameOnEnter', newValue);
}, },
openInFrameOnAdd: function(newValue) { openInFrameOnAdd(newValue) {
this.commitProp('openInFrameOnAdd', newValue); this.commitProp('openInFrameOnAdd', newValue);
}, },
rootLink() {
this.rootLinkInput();
},
selectedLink() {
this.selectedLinkInput();
},
} }
}) };
class ExternalLibs extends Vue { class ExternalLibs {
_options = componentOptions;
ready = false; ready = false;
frameVisible = false; frameVisible = false;
rootLink = ''; rootLink = '';
@@ -233,9 +307,9 @@ class ExternalLibs extends Vue {
created() { created() {
this.oldStartLink = ''; this.oldStartLink = '';
this.justOpened = true; this.justOpened = true;
this.$root.addKeyHook(this.keyHook); this.$root.addEventHook('key', this.keyHook);
this.$root.$on('resize', async() => { this.$root.addEventHook('resize', async() => {
await utils.sleep(200); await utils.sleep(200);
this.frameResize(); this.frameResize();
}); });
@@ -252,29 +326,6 @@ class ExternalLibs extends Vue {
} }
mounted() { mounted() {
//Поправка метода toggleOption компонента select фреймворка quasar, необходимо другое поведение
//$emit('input'.. вызывается всегда
this.toggleOption = function(opt, keepOpen) {
if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
return;
}
const optValue = this.getOptionValue(opt);
if (this.multiple !== true) {
if (keepOpen !== true) {
this.updateInputValue(this.fillInput === true ? this.getOptionLabel(opt) : '', true, true);
this.hidePopup();
}
this.$refs.target !== void 0 && this.$refs.target.focus();
this.$emit('input', this.emitValue === true ? optValue : opt);
}
};
this.$refs.rootLink.toggleOption = this.toggleOption;
this.$refs.selectedLink.toggleOption = this.toggleOption;
(async() => { (async() => {
//подождем this.mode //подождем this.mode
let i = 0; let i = 0;
@@ -412,7 +463,7 @@ class ExternalLibs extends Vue {
if (this.ready && this.selectedLink) { if (this.ready && this.selectedLink) {
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`; result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
} }
this.$root.$emit('set-app-title', result); this.$root.setAppTitle(result);
return result; return result;
} }
@@ -621,9 +672,9 @@ class ExternalLibs extends Vue {
this.addBookmarkMode = mode; this.addBookmarkMode = mode;
this.addBookmarkVisible = true; this.addBookmarkVisible = true;
this.$nextTick(() => { this.$nextTick(async() => {
await this.$refs.dialogAddBookmark.waitShown();
this.$refs.bookmarkLink.focus(); this.$refs.bookmarkLink.focus();
this.$refs.defaultRootLink.toggleOption = this.toggleOption;
}); });
} }
@@ -638,10 +689,6 @@ class ExternalLibs extends Vue {
} }
} }
defaultRootLinkInput() {
this.updateBookmarkLink();
}
bookmarkLinkKeyDown(event) { bookmarkLinkKeyDown(event) {
if (event.key == 'Enter') { if (event.key == 'Enter') {
this.$refs.bookmarkDesc.focus(); this.$refs.bookmarkDesc.focus();
@@ -704,7 +751,7 @@ class ExternalLibs extends Vue {
this.commitLibs(libs); this.commitLibs(libs);
} else if (item.c != this.bookmarkDesc) { } else if (item.c != this.bookmarkDesc) {
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` + if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) { `Заменить '${this.$root.sanitize(item.c)}' на '${this.$root.sanitize(this.bookmarkDesc)}'?`, ' ')) {
item.c = this.bookmarkDesc; item.c = this.bookmarkDesc;
this.commitLibs(libs); this.commitLibs(libs);
} else } else
@@ -810,7 +857,7 @@ class ExternalLibs extends Vue {
} }
keyHook(event) { keyHook(event) {
if (this.$root.rootRoute() == '/external-libs') { if (this.$root.getRootRoute() == '/external-libs') {
if (this.$root.stdDialog.active) if (this.$root.stdDialog.active)
return false; return false;
@@ -836,6 +883,8 @@ class ExternalLibs extends Vue {
return false; return false;
} }
} }
export default vueComponent(ExternalLibs);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -845,14 +894,15 @@ class ExternalLibs extends Vue {
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

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Help {
})
class Help extends Vue {
created() { created() {
} }
} }
export default vueComponent(Help);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Income {
})
class Income extends Vue {
created() { created() {
} }
} }
export default vueComponent(Income);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class NotFound404 {
})
class NotFound404 extends Vue {
created() { created() {
} }
} }
export default vueComponent(NotFound404);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,12 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import {sleep} from '../../../share/utils'; import {sleep} from '../../../share/utils';
import {clickMap, clickMapText} from '../share/clickMap'; import {clickMap, clickMapText} from '../share/clickMap';
export default @Component({ class ClickMapPage {
})
class ClickMapPage extends Vue {
fontSize = '200%'; fontSize = '200%';
created() { created() {
@@ -53,6 +50,8 @@ class ClickMapPage extends Vue {
await sleep(5000); await sleep(5000);
} }
} }
export default vueComponent(ClickMapPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,125 +1,146 @@
<template> <template>
<Window width="600px" ref="window" @close="close"> <Window ref="window" width="600px" @close="close">
<template slot="header"> <template #header>
Оглавление/закладки Оглавление/закладки
</template> </template>
<div class="bg-grey-3 row"> <div class="bg-grey-3 row">
<q-tabs <q-tabs
v-model="selectedTab" v-model="selectedTab"
active-color="black" active-color="black"
active-bg-color="white" active-bg-color="white"
indicator-color="white" indicator-color="white"
dense dense
no-caps no-caps
inline-label inline-label
class="no-mp bg-grey-4 text-grey-7" class="no-mp bg-grey-4 text-grey-7"
> >
<q-tab name="contents" icon="la la-list" label="Оглавление" /> <q-tab name="contents" icon="la la-list" label="Оглавление" />
<q-tab name="images" icon="la la-image" label="Изображения" /> <q-tab name="images" icon="la la-image" label="Изображения" />
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" /> <q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
</q-tabs> </q-tabs>
</div> </div>
<div class="q-mb-sm"/> <div class="q-mb-sm" />
<div class="tab-panel" v-show="selectedTab == 'contents'"> <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 v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div> </div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/> <div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
</div> <div
<div class="col row clickable" @click="setBookPos(item.offset)"> v-for="subitem in item.list"
<div :style="item.indentStyle"></div> :ref="`subitem${subitem.key}`"
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div> :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
<div class="column justify-center">{{ item.perc }}%</div> >
</div> <div class="col row clickable" @click="setBookPos(subitem.offset)">
</div> <div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition"> <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<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 class="column justify-center">
<div class="col row clickable" @click="setBookPos(subitem.offset)"> {{ subitem.perc }}%
<div class="no-expand-button"></div> </div>
<div :style="subitem.indentStyle"></div> </div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<div class="column justify-center">{{ subitem.perc }}%</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px"> Оглавление отсутствует
Оглавление отсутствует
</div>
</div>
</div>
<div class="tab-panel" v-show="selectedTab == 'images'">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
</div>
<div class="no-expand-button column justify-center items-center">
<div class="image-num">{{ item.num }}</div>
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
<div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
</div>
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">{{ item.perc }}%</div>
</div>
</div> </div>
</div> </div>
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px"> </div>
Изображения отсутствуют
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
<i class="loading-img-icon la la-images"></i>
</div>
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
</div>
<div class="no-expand-button column justify-center items-center">
<div class="image-num">
{{ item.num }}
</div>
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">
JPG
</div>
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">
PNG
</div>
<div v-show="!item.local" class="image-type it-net-color row justify-center">
INET
</div>
</div>
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div>
</div>
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
Изображения отсутствуют
</div>
</div> </div>
</div> </div>
</div>
<div class="tab-panel" v-show="selectedTab == 'bookmarks'"> <div v-show="selectedTab == 'bookmarks'" class="tab-panel">
<div class="column justify-center items-center" style="height: 100px"> <div class="column justify-center items-center" style="height: 100px">
Раздел находится в разработке Раздел находится в разработке
</div>
</div> </div>
</div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
//import _ from 'lodash'; //import _ from 'lodash';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
const ContentsPageProps = Vue.extend({ const componentOptions = {
props: {
bookPos: Number,
isVisible: Boolean,
}
});
export default @Component({
components: { components: {
Window, Window,
}, },
watch: { watch: {
bookPos: function() { bookPos() {
this.updateBookPosSelection(); this.updateBookPosSelection();
} },
selectedTab() {
this.updateBookPosScrollTop();
},
}, },
}) };
class ContentsPage extends ContentsPageProps { class ContentsPage {
_options = componentOptions;
_props = {
bookPos: Number,
isVisible: Boolean,
};
selectedTab = 'contents'; selectedTab = 'contents';
contents = []; contents = [];
images = []; images = [];
@@ -255,9 +276,9 @@ class ContentsPage extends ContentsPageProps {
const {id, local} = ims[i]; const {id, local} = ims[i];
const bin = this.parsed.binary[id]; const bin = this.parsed.binary[id];
if (local) if (local)
this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : '')); this.imageSrc[id] = (bin ? `data:${bin.type};base64,${bin.data}` : '');
else else
this.$set(this.imageSrc, id, id); this.imageSrc[id] = id;
this.imageLoaded[id] = true; this.imageLoaded[id] = true;
await utils.sleep(5); await utils.sleep(5);
} }
@@ -268,31 +289,30 @@ class ContentsPage extends ContentsPageProps {
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.$set(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.$set(this.contents, i, Object.assign(item, {list: item.list}));
} }
} }
if (bp >= item.offset && bp < nextOffset) {
this.$set(this.contents, i, Object.assign(item, {isBookPos: true}));
} else if (item.isBookPos) {
this.$set(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++) {
@@ -300,11 +320,96 @@ class ContentsPage extends ContentsPageProps {
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.$set(this.images, i, Object.assign(img, {isBookPos: true})); this.images[i].isBookPos = true;
} else if (img.isBookPos) { } else if (img.isBookPos) {
this.$set(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) {
@@ -312,17 +417,17 @@ class ContentsPage extends ContentsPageProps {
const expanded = !item.expanded; const expanded = !item.expanded;
if (!expanded) { if (!expanded) {
const subitems = this.$refs[`subitem${key}`][0]; 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.$set(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}`][0]; let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subitems.style.height = subitems.scrollHeight + 'px'; subdiv.style.height = subdiv.scrollHeight + 'px';
} }
} }
@@ -342,6 +447,8 @@ class ContentsPage extends ContentsPageProps {
return true; return true;
} }
} }
export default vueComponent(ContentsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window @close="close"> <Window @close="close">
<template slot="header"> <template #header>
Скопировать текст Скопировать текст
</template> </template>
@@ -12,18 +12,19 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import {sleep} from '../../../share/utils'; import {sleep} from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
}) };
class CopyTextPage extends Vue { class CopyTextPage {
_options = componentOptions;
text = null; text = null;
initStep = null; initStep = null;
initPercentage = 0; initPercentage = 0;
@@ -101,6 +102,8 @@ class CopyTextPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(CopyTextPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -18,15 +18,20 @@
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li> <li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
</ul> </ul>
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку <p>
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p> В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").
</p>
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p> <p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
<div v-show="mode == 'omnireader' || mode == 'liberama.top'"> <div v-show="mode == 'omnireader' || mode == 'liberama.top'">
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код: <p>
Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>{{ bookmarkText }}</strong> <br><strong>{{ bookmarkText }}</strong>
<q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')"> <q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip> <q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
<br>или перетащив на панель закладок следующую ссылку: <br>или перетащив на панель закладок следующую ссылку:
@@ -41,14 +46,11 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import {copyTextToClipboard} from '../../../../share/utils'; import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({ class CommonHelpPage {
})
class CommonHelpPage extends Vue {
created() { created() {
} }
@@ -69,6 +71,8 @@ class CommonHelpPage extends Vue {
this.$root.notify.error(msg); this.$root.notify.error(msg);
} }
} }
export default vueComponent(CommonHelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,51 +1,17 @@
<template> <template>
<div class="page"> <div class="page">
<div class="box"> <div class="column items-center" style="width: 500px">
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p> <p class="p">
<div class="address"> Здесь вы можете пожертвовать на развитие проекта:
<img class="logo" src="./assets/yoomoney.png"> </p>
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">Пожертвовать</q-btn><br>
<div class="para">{{ yooAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address"> <q-btn no-caps class="q-my-lg" color="green-8" size="14px" style="width: 200px" @click="makeDonation">
<img class="logo" src="./assets/paypal.png"> <q-icon class="q-mr-xs" name="la la-donate" size="24px" />
<div class="para">{{ paypalAddress }} Поддержать проект
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')"> </q-btn>
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address"> <div style="font-size: 60%">
<img class="logo" src="./assets/bitcoin.png"> * Ваш донат является подарком автору проекта
<div class="para">{{ bitcoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/litecoin.png">
<div class="para">{{ litecoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/monero.png">
<div class="para">{{ moneroAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -53,34 +19,20 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({ import * as utils from '../../../../share/utils';
})
class DonateHelpPage extends Vue {
yooAddress = '410018702323056';
paypalAddress = 'bookpauk@gmail.com';
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
class DonateHelpPage {
created() { created() {
} }
donateYooMoney() { makeDonation() {
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank'); utils.makeDonation();
}
async copyAddress(address, prefix) {
const result = await copyTextToClipboard(address);
if (result)
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
else
this.$root.notify.error('Копирование не удалось');
} }
} }
export default vueComponent(DonateHelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -97,31 +49,4 @@ class DonateHelpPage extends Vue {
padding: 0; padding: 0;
text-indent: 20px; text-indent: 20px;
} }
.box {
max-width: 550px;
overflow-wrap: break-word;
}
.address {
padding-top: 10px;
margin-top: 20px;
}
.para {
margin: 10px 10px 10px 40px;
}
.logo {
width: 130px;
position: relative;
top: 10px;
}
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
color: blue;
}
</style> </style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,21 +1,27 @@
<template> <template>
<Window @close="close"> <Window @close="close" style="z-index: 200">
<template slot="header"> <template #header>
Справка Справка
</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 ref="page" class="col" :is="activePage" <component :is="activePage" ref="page" class="col"></component>
></component>
</keep-alive> </keep-alive>
</div> </div>
</Window> </Window>
@@ -23,8 +29,7 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue'; import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
@@ -49,10 +54,12 @@ const tabs = [
['DonateHelpPage', 'Помочь проекту'], ['DonateHelpPage', 'Помочь проекту'],
]; ];
export default @Component({ const componentOptions = {
components: Object.assign({ Window }, pages), components: Object.assign({ Window }, pages),
}) };
class HelpPage extends Vue { class HelpPage {
_options = componentOptions;
selectedTab = 'CommonHelpPage'; selectedTab = 'CommonHelpPage';
close() { close() {
@@ -87,12 +94,10 @@ class HelpPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(HelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style> </style>

View File

@@ -1,29 +1,34 @@
<template> <template>
<div class="page"> <div class="page">
<div style="font-size: 120%"> <div style="font-size: 120%">
<div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div> <div class="text-h6 text-bold">
Доступны следующие клавиатурные команды:
</div>
<br> <br>
</div> </div>
<div class="q-mb-md" style="width: 550px"> <div class="q-mb-md" style="width: 550px">
<div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div> <div class="text-right text-italic" style="font-size: 80%">
<UserHotKeys v-model="userHotKeys" readonly/> * Изменить сочетания клавиш можно в настройках
</div>
<UserHotKeys v-model="userHotKeys" readonly />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue'; import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
export default @Component({ const componentOptions = {
components: { components: {
UserHotKeys, UserHotKeys,
}, },
}) };
class HotkeysHelpPage extends Vue { class HotkeysHelpPage {
_options = componentOptions;
created() { created() {
} }
@@ -36,6 +41,8 @@ class HotkeysHelpPage extends Vue {
} }
} }
export default vueComponent(HotkeysHelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -3,21 +3,28 @@
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span> <span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
<ul> <ul>
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li> <li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
<div class="click-map-page"> <div class="click-map-page">
<ClickMapPage ref="clickMapPage"></ClickMapPage> <ClickMapPage ref="clickMapPage"></ClickMapPage>
</div> </div>
<li><b>ПКМ</b> - показать/скрыть панель управления</li> <li><b>ПКМ</b> - показать/скрыть панель управления</li>
<li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li> <li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
<br> <br>
<li>Жесты для тачскрина:</li> <li>Жесты для тачскрина:</li>
<ul> <ul>
<li style="list-style-type: square">от центра вверх: на весь экран</li> <li style="list-style-type: square">
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li> от центра вверх: на весь экран
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li> </li>
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li> <li style="list-style-type: square">
от центра вниз: плавный скроллинг
</li>
<li style="list-style-type: square">
от центра вправо: увеличить скорость скроллинга
</li>
<li style="list-style-type: square">
от центра влево: уменьшить скорость скроллинга
</li>
</ul> </ul>
</ul> </ul>
* Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках * Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках
</div> </div>
@@ -25,17 +32,18 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue'; import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue';
export default @Component({ const componentOptions = {
components: { components: {
ClickMapPage, ClickMapPage,
}, },
}) };
class MouseHelpPage extends Vue { class MouseHelpPage {
_options = componentOptions;
created() { created() {
} }
@@ -44,6 +52,8 @@ class MouseHelpPage extends Vue {
this.$refs.clickMapPage.$el.style.backgroundColor = '#478355'; this.$refs.clickMapPage.$el.style.backgroundColor = '#478355';
} }
} }
export default vueComponent(MouseHelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -3,9 +3,9 @@
<span class="text-h6 text-bold">История версий:</span> <span class="text-h6 text-bold">История версий:</span>
<br><br> <br><br>
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)"> <span v-for="(item, index) in versionHeader" :key="index" class="clickable" @click="showRelease(item)">
<p> <p>
{{ item }} {{ item }}
</p> </p>
</span> </span>
@@ -20,13 +20,11 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import {versionHistory} from '../../versionHistory'; import {versionHistory} from '../../versionHistory';
export default @Component({ class VersionHistoryPage {
})
class VersionHistoryPage extends Vue {
versionHeader = []; versionHeader = [];
versionContent = []; versionContent = [];
@@ -35,14 +33,15 @@ class VersionHistoryPage extends Vue {
mounted() { mounted() {
let vh = []; let vh = [];
for (const version of versionHistory) { for (const v of versionHistory) {
vh.push(version.header); vh.push(`${v.version} (${v.releaseDate})`);
} }
this.versionHeader = vh; this.versionHeader = vh;
let vc = []; let vc = [];
for (const version of versionHistory) { for (const v of versionHistory) {
vc.push({key: version.header, content: 'Версия ' + version.header + version.content}); let header = `${v.version} (${v.releaseDate})`;
vc.push({key: header, content: 'Версия ' + header + v.content});
} }
this.versionContent = vc; this.versionContent = vc;
} }
@@ -54,6 +53,8 @@ class VersionHistoryPage extends Vue {
} }
} }
} }
export default vueComponent(VersionHistoryPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -4,14 +4,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
//import rstore from '../../../store/modules/reader'; //import rstore from '../../../store/modules/reader';
import _ from 'lodash';
export default @Component({ const componentOptions = {
components: { components: {
Window Window
}, },
@@ -20,8 +20,10 @@ export default @Component({
this.sendLibs(); this.sendLibs();
}, },
} }
}) };
class LibsPage extends Vue { class LibsPage {
_options = componentOptions;
created() { created() {
this.popupWindow = null; this.popupWindow = null;
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -113,13 +115,15 @@ class LibsPage extends Vue {
} }
sendLibs() { sendLibs() {
this.sendMessage({type: 'libs', data: this.libs}); this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs)});
} }
close() { close() {
this.$emit('libs-close'); this.$emit('libs-close');
} }
} }
export default vueComponent(LibsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div ref="main" class="column no-wrap" style="min-height: 500px"> <div ref="main" class="column no-wrap" style="min-height: 500px">
<div v-if="mode != 'liberama.top'" class="relative-position"> <div v-if="mode != 'liberama.top'" class="relative-position">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner> <GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
</div> </div>
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px"> <div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
<span class="greeting"><b>{{ title }}</b></span> <span class="greeting"><b>{{ title }}</b></span>
@@ -12,21 +12,31 @@
</div> </div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden"> <div class="col-auto column justify-start items-center no-wrap overflow-hidden">
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги"> <q-input
<template v-slot:append> ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/> outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
>
<template #append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
</template> </template>
</q-input> </q-input>
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/> <input
id="file" ref="file" type="file"
style="display: none;"
:accept="acceptFileExt"
@change="loadFile"
/>
<div class="q-my-sm"></div> <div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick"> <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
Загрузить файл с диска Загрузить файл с диска
</q-btn> </q-btn>
<div class="q-my-sm"></div> <div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick"> <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена Из буфера обмена
</q-btn> </q-btn>
@@ -45,6 +55,7 @@
</div> </div>
<div class="col column justify-end items-center no-wrap overflow-hidden"> <div class="col column justify-end items-center no-wrap overflow-hidden">
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
<span class="bottom-span clickable" @click="openHelp">Справка</span> <span class="bottom-span clickable" @click="openHelp">Справка</span>
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span> <span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
@@ -53,28 +64,46 @@
</div> </div>
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage> <PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
<Dialog ref="dialog1" v-model="findBookVisible">
<template #header>
Подсказка ;-)
</template>
<div style="word-break: normal">
Если вы хотите найти определенную книгу, добро пожаловать в
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
<a href="https://liberama.top" target="_blank">liberama.top</a>
</div>
</Dialog>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import GithubCorner from './GithubCorner/GithubCorner.vue'; import GithubCorner from './GithubCorner/GithubCorner.vue';
import Dialog from '../../share/Dialog.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue'; import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
import * as utils from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
GithubCorner, GithubCorner,
Dialog,
PasteTextPage, PasteTextPage,
}, },
}) };
class LoaderPage extends Vue { class LoaderPage {
_options = componentOptions;
bookUrl = null; bookUrl = null;
loadPercent = 0; loadPercent = 0;
pasteTextActive = false; pasteTextActive = false;
findBookVisible = false;
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -107,14 +136,16 @@ class LoaderPage extends Vue {
return this.$store.state.config.version; return this.$store.state.config.version;
} }
get acceptFileExt() {
return this.$store.state.config.acceptFileExt;
}
get isExternalConverter() { get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter; return this.$store.state.config.useExternalBookConverter;
} }
get clientVersion() { get clientVersion() {
let v = versionHistory[0].header; return versionHistory[0].version;
v = v.split(' ')[0];
return v;
} }
submitUrl() { submitUrl() {
@@ -136,7 +167,7 @@ class LoaderPage extends Vue {
} }
loadBufferClick() { loadBufferClick() {
this.pasteTextToggle(); this.showPasteText();
} }
loadBuffer(opts) { loadBuffer(opts) {
@@ -146,6 +177,10 @@ class LoaderPage extends Vue {
} }
} }
showPasteText() {
this.pasteTextActive = true;
}
pasteTextToggle() { pasteTextToggle() {
this.pasteTextActive = !this.pasteTextActive; this.pasteTextActive = !this.pasteTextActive;
} }
@@ -158,6 +193,10 @@ class LoaderPage extends Vue {
this.$emit('do-action', {action: 'donate'}); this.$emit('do-action', {action: 'donate'});
} }
findBook() {
this.findBookVisible = true;
}
openComments() { openComments() {
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank'); window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
} }
@@ -166,30 +205,30 @@ class LoaderPage extends Vue {
window.open('http://old.omnireader.ru', '_blank'); window.open('http://old.omnireader.ru', '_blank');
} }
async onInputKeydown(event) {
if (event.key == 'Enter') {
await utils.sleep(100);
this.submitUrl();
}
}
keyHook(event) { keyHook(event) {
if (this.$refs.dialog1.active)
return true;
if (this.pasteTextActive) { if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event); return this.$refs.pasteTextPage.keyHook(event);
} }
//недостатки сторонних ui const input = this.$refs.input.getNativeElement();
const input = this.$refs.input.$refs.input; if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
this.submitUrl();
return true; return true;
}
if (event.type == 'keydown' && document.activeElement !== input) {
const action = this.$root.readerActionByKeyEvent(event);
switch (action) {
case 'help':
this.openHelp(event);
return true;
}
}
return false; return false;
} }
} }
export default vueComponent(LoaderPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window @close="close"> <Window @close="close">
<template slot="header"> <template #header>
<span style="position: relative; top: -3px"> <span style="position: relative; top: -3px">
Вставьте текст и нажмите Вставьте текст и нажмите
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span> <span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
@@ -8,27 +8,28 @@
</span> </span>
</template> </template>
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/> <q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
<hr/> <hr />
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea> <textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../../share/Window.vue'; import Window from '../../../share/Window.vue';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../../share/utils'; import * as utils from '../../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
}) };
class PasteTextPage extends Vue { class PasteTextPage {
_options = componentOptions;
bookTitle = ''; bookTitle = '';
created() { created() {
@@ -59,15 +60,19 @@ class PasteTextPage extends Vue {
calcTitle(event) { calcTitle(event) {
if (this.bookTitle == '') { if (this.bookTitle == '') {
let text = event.clipboardData.getData('text'); this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`;
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}: ` + _.compact([ if (event) {
this.getNonEmptyLine3words(text, 1), let text = event.clipboardData.getData('text');
this.getNonEmptyLine3words(text, 2) this.bookTitle += ': ' + _.compact([
]).join(' - '); this.getNonEmptyLine3words(text, 1),
this.getNonEmptyLine3words(text, 2)
]).join(' - ');
}
} }
} }
loadBuffer() { loadBuffer() {
this.calcTitle();
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`}); this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
this.close(); this.close();
} }
@@ -90,6 +95,8 @@ class PasteTextPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(PasteTextPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

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
@@ -17,7 +17,7 @@
<div> <div>
<span class="text-yellow">{{ text }}</span> <span class="text-yellow">{{ text }}</span>
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/> <q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px" />
</div> </div>
</div> </div>
</div> </div>
@@ -25,8 +25,8 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
const ruMessage = { const ruMessage = {
@@ -42,9 +42,7 @@ const ruMessage = {
'upload': 'отправка', 'upload': 'отправка',
}; };
export default @Component({ class ProgressPage {
})
class ProgressPage extends Vue {
text = ''; text = '';
totalSteps = 1; totalSteps = 1;
step = 1; step = 1;
@@ -96,5 +94,7 @@ class ProgressPage extends Vue {
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100); return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
} }
} }
export default vueComponent(ProgressPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +1,101 @@
<template> <template>
<div> <div>
<Dialog ref="dialog1" v-model="whatsNewVisible"> <Dialog ref="dialog1" v-model="whatsNewVisible">
<template slot="header"> <template #header>
Что нового: Что нового:
</template> </template>
<div style="line-height: 20px" v-html="whatsNewContent"></div> <div style="line-height: 20px; min-width: 300px">
<div v-html="whatsNewContent"></div>
</div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span> <span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn> <template #footer>
</span> <q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
Больше не показывать
</q-btn>
</template>
</Dialog> </Dialog>
<Dialog ref="dialog2" v-model="donationVisible"> <q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
<template slot="header"> <div class="column bg-white no-wrap q-pa-md">
Здравствуйте, уважаемые читатели! <div class="row justify-center q-mb-md" style="font-size: 110%">
Здравствуйте, дорогие читатели!
</div>
<div class="q-mx-md column" style="word-break: normal">
<div>
Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
Напоминаем вам, что проект является некоммерческим и обладает такими
достоинствами, как:
<ul>
<li>все функции читалки открыты и доступны совершенно бесплатно</li>
<li>в проекте отсутствует какая-либо реклама или баннеры</li>
<li>нет никакой регистрации и монетизации</li>
<li>нет сбора персональных данных</li>
<li>открытый исходный код</li>
<li>проект постепенно улучшается, по мере возможности</li>
</ul>
Однако на оплату хостинга читалки и сервера обновлений автор тратит свои
собственные средства, а также тратит свое время и силы на улучшение проекта.
<br><br>
Поддержим же материально наш ресурс, чтобы и дальше спокойно существовать и развиваться:
</div>
<q-btn style="margin: 10px 50px 10px 50px" color="green-8" size="14px" no-caps @click="makeDonation">
<q-icon class="q-mr-xs" name="la la-donate" size="24px" />
Поддержать проект
</q-btn>
<q-btn style="margin: 0 50px 20px 50px" size="14px" no-caps @click="donationDialogRemind">
Напомнить в следующем месяце
</q-btn>
<div class="row justify-center">
<div class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
Помочь проекту можно в любое время
</div>
</div>
</div>
</div>
</q-dialog>
<Dialog ref="dialog3" v-model="urlHelpVisible">
<template #header>
Обнаружена невалидная ссылка в поле "URL книги".
<br>
</template> </template>
<div style="word-break: normal"> <div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br> Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей. <a href="https://liberama.top" target="_blank">liberama.top</a>
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br> <br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена. Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
<br><br> <q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a> <q-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена
<br><br> </q-btn>
<div class="row justify-center"> на странице загрузки.
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div> </div>
<span slot="footer">
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog> </Dialog>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Dialog from '../../share/Dialog.vue'; import Dialog from '../../share/Dialog.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
export default @Component({ const componentOptions = {
components: { components: {
Dialog Dialog
}, },
@@ -78,11 +104,14 @@ export default @Component({
this.loadSettings(); this.loadSettings();
}, },
}, },
}) };
class ReaderDialogs extends Vue { class ReaderDialogs {
_options = componentOptions;
whatsNewVisible = false; whatsNewVisible = false;
whatsNewContent = ''; whatsNewContent = '';
donationVisible = false; donationVisible = false;
urlHelpVisible = false;
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -100,43 +129,49 @@ class ReaderDialogs extends Vue {
loadSettings() { loadSettings() {
const settings = this.settings; const settings = this.settings;
this.showWhatsNewDialog = settings.showWhatsNewDialog; this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020; this.showDonationDialog = settings.showDonationDialog;
} }
async showWhatsNew() { async showWhatsNew() {
const whatsNew = versionHistory[0]; const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog && if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') && whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) { this.whatsNewHeader != this.whatsNewContentHash) {
await utils.sleep(2000); await utils.sleep(2000);
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content; this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
this.whatsNewVisible = true; this.whatsNewVisible = true;
} }
} }
async showDonation() { async showDonation() {
const today = utils.formatDate(new Date(), 'coDate'); const today = utils.formatDate(new Date(), 'coMonth');
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) { if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && this.showDonationDialog && this.donationRemindDate != today) {
await utils.sleep(3000); await utils.sleep(3000);
this.donationVisible = true; this.donationVisible = true;
} }
} }
donationDialogDisable() { async showUrlHelp() {
this.donationVisible = false; this.urlHelpVisible = true;
if (this.showDonationDialog2020) { }
this.commit('reader/setSettings', { showDonationDialog2020: false });
} loadBufferClick() {
this.$emit('load-buffer-toggle');
this.urlHelpVisible = false;
} }
donationDialogRemind() { donationDialogRemind() {
this.donationVisible = false; this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate')); this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coMonth'));
}
makeDonation() {
utils.makeDonation();
this.donationDialogRemind();
} }
openDonate() { openDonate() {
this.donationVisible = false;
this.$emit('donate-toggle'); this.$emit('donate-toggle');
} }
@@ -155,8 +190,11 @@ class ReaderDialogs extends Vue {
whatsNewDisable() { whatsNewDisable() {
this.whatsNewVisible = false; this.whatsNewVisible = false;
const whatsNew = versionHistory[0]; this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
this.commit('reader/setWhatsNewContentHash', whatsNew.header); }
get whatsNewHeader() {
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
} }
get mode() { get mode() {
@@ -176,11 +214,13 @@ class ReaderDialogs extends Vue {
} }
keyHook() { keyHook() {
if (this.$refs.dialog1.active || this.$refs.dialog2.active) if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
return true; return true;
return false; return false;
} }
} }
export default vueComponent(ReaderDialogs);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close"> <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header"> <template #header>
{{ header }} {{ header }}
</template> </template>
@@ -8,18 +8,23 @@
<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"/--> class="col" outlined dense
<q-input ref="input" class="col" outlined dense placeholder="Найти"
placeholder="что ищем" @keydown="inputKeyDown"
v-model="needle" @keydown="inputKeyDown"
/> />
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div> <div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
{{ foundText }}
</div>
</div> </div>
<q-btn-group v-show="!initStep" class="button-group row no-wrap"> <q-btn-group v-show="!initStep" class="button-group row no-wrap">
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn> <q-btn class="button" dense stretch @click="showNext">
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn> <q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
</q-btn>
<q-btn class="button" dense stretch @click="showPrev">
<q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
</q-btn>
</q-btn-group> </q-btn-group>
</div> </div>
</Window> </Window>
@@ -27,13 +32,12 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import {sleep} from '../../../share/utils'; import {sleep} from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
@@ -49,8 +53,10 @@ export default @Component({
el.style.paddingRight = newValue.length*12 + 'px'; el.style.paddingRight = newValue.length*12 + 'px';
}, },
}, },
}) };
class SearchPage extends Vue { class SearchPage {
_options = componentOptions;
header = null; header = null;
initStep = null; initStep = null;
initPercentage = 0; initPercentage = 0;
@@ -100,7 +106,7 @@ class SearchPage extends Vue {
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();
@@ -180,6 +186,8 @@ class SearchPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SearchPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -4,21 +4,22 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader'; import readerApi from '../../../api/reader';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils'; import * as cryptoUtils from '../../../share/cryptoUtils';
import LockQueue from '../../../share/LockQueue';
import localForage from 'localforage'; import localForage from 'localforage';
const ssCacheStore = localForage.createInstance({ const ssCacheStore = localForage.createInstance({
name: 'ssCacheStore' name: 'ssCacheStore'
}); });
export default @Component({ const componentOptions = {
watch: { watch: {
serverSyncEnabled: function() { serverSyncEnabled: function() {
this.serverSyncEnabledChanged(); this.serverSyncEnabledChanged();
@@ -39,14 +40,18 @@ export default @Component({
this.debouncedSaveLibs(); this.debouncedSaveLibs();
}, },
}, },
}) };
class ServerStorage extends Vue { class ServerStorage {
_options = componentOptions;
created() { created() {
this.inited = false; this.inited = false;
this.keyInited = false; this.keyInited = false;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.prevServerStorageKey = null; this.prevServerStorageKey = null;
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()}); this.lock = new LockQueue(100);
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
this.debouncedSaveSettings = _.debounce(() => { this.debouncedSaveSettings = _.debounce(() => {
this.saveSettings(); this.saveSettings();
@@ -540,14 +545,16 @@ class ServerStorage extends Vue {
return true; return true;
} }
async saveRecent(itemKey, recurse) { async saveRecent(itemKeys, recurse) {
while (!this.inited || this.savingRecent) while (!this.inited)
await utils.sleep(100); await utils.sleep(100);
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent) if (!this.keyInited || !this.serverSyncEnabled)
return; return;
this.savingRecent = true; let needRecurseCall = false;
await this.lock.get();
try { try {
const bm = bookManager; const bm = bookManager;
@@ -557,24 +564,31 @@ class ServerStorage extends Vue {
//newRecentMod //newRecentMod
let newRecentMod = {}; let newRecentMod = {};
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) { let oneItemKey = null;
if (itemKeys && itemKeys.length == 1)
oneItemKey = itemKeys[0];
if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
newRecentMod = _.cloneDeep(this.cachedRecentMod); newRecentMod = _.cloneDeep(this.cachedRecentMod);
newRecentMod.rev++; newRecentMod.rev++;
newRecentMod.data.key = itemKey; newRecentMod.data.key = oneItemKey;
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]); newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
needSaveRecentMod = true; needSaveRecentMod = true;
} }
this.prevItemKey = itemKey; this.prevItemKey = oneItemKey;
//newRecentPatch //newRecentPatch
let newRecentPatch = {}; let newRecentPatch = {};
if (itemKey && !needSaveRecentMod) { if (itemKeys && !needSaveRecentMod) {
newRecentPatch = _.cloneDeep(this.cachedRecentPatch); newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
newRecentPatch.rev++; newRecentPatch.rev++;
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
let applyMod = this.cachedRecentMod.data; for (const key of itemKeys) {
newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
}
const applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key]) if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true}); newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
@@ -585,11 +599,7 @@ class ServerStorage extends Vue {
//newRecent //newRecent
let newRecent = {}; let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) { if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent
/*while (!bookManager.loaded)
await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)}; newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}}; newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}}; newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
@@ -623,10 +633,8 @@ class ServerStorage extends Vue {
if (res) if (res)
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`); this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) { if (!recurse && itemKeys) {
this.savingRecent = false; needRecurseCall = true;
this.saveRecent(itemKey, true);
return;
} }
} else if (result.state == 'success') { } else if (result.state == 'success') {
if (needSaveRecent && newRecent.rev) if (needSaveRecent && newRecent.rev)
@@ -637,8 +645,11 @@ class ServerStorage extends Vue {
await this.setCachedRecentMod(newRecentMod); await this.setCachedRecentMod(newRecentMod);
} }
} finally { } finally {
this.savingRecent = false; this.lock.ret();
} }
if (needRecurseCall)
await this.saveRecent(itemKeys, true);
} }
async storageCheck(items) { async storageCheck(items) {
@@ -726,13 +737,15 @@ class ServerStorage extends Vue {
const ids = id.split('.'); const ids = id.split('.');
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey)) if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
throw new Error(`decodeStorageItems: bad id - ${id}`); throw new Error(`decodeStorageItems: bad id - ${id}`);
items[utils.fromBase58(ids[1])] = decoded; items[utils.fromBase58(ids[1]).toString()] = decoded;
} }
} }
result.items = items; result.items = items;
return result; return result;
} }
} }
export default vueComponent(ServerStorage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,30 +1,32 @@
<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 slot="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">
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" <q-slider
v-model="sliderValue" v-model="sliderValue"
:max="sliderMax" 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"
label
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)" :max="sliderMax"
color="primary" label
/> :label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
</div>
</div> </div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
@@ -34,8 +36,10 @@ export default @Component({
this.$emit('book-pos-changed', {bookPos: newValue}); this.$emit('book-pos-changed', {bookPos: newValue});
}, },
}, },
}) };
class SetPositionPage extends Vue { class SetPositionPage {
_options = componentOptions;
sliderValue = null; sliderValue = null;
sliderMax = null; sliderMax = null;
@@ -67,12 +71,15 @@ class SetPositionPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SetPositionPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<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

@@ -43,21 +43,10 @@
<div class="item row"> <div class="item row">
<div class="label-6">Уведомление</div> <div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showNeedUpdateNotify"> <q-checkbox size="xs" v-model="showDonationDialog">
Показывать уведомление о новой версии Показывать форму доната
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Напоминать о необходимости обновления страницы<br> Показывать диалог для сбора пожертвований
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showDonationDialog2020">
Показывать "Оплатим хостинг вместе"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомление "Оплатим хостинг вместе"
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
</div> </div>

View File

@@ -1,15 +1,18 @@
<template lang="includer"> <template>
<Window ref="window" height="95%" width="600px" @close="close"> <Window ref="window" width="600px" @close="close">
<template slot="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"
class="bg-grey-3 text-black"
v-model="selectedTab" v-model="selectedTab"
class="bg-grey-3 text-black"
left-icon="la la-caret-up" left-icon="la la-caret-up"
right-icon="la la-caret-down" right-icon="la la-caret-down"
active-color="white" active-color="white"
@@ -20,62 +23,105 @@
stretch stretch
inline-label inline-label
> >
<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="Конвертир." />
<q-tab class="tab" name="update" icon="la la-sync" label="Обновление" />
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" /> <q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" /> <q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
<div v-show="tabsScrollable" class="q-pt-lg"/> <div v-show="tabsScrollable" class="q-pt-lg" />
</q-tabs> </q-tabs>
</div> </div>
<div class="col fit"> <div class="col fit">
<!-- Профили ---------------------------------------------------------------------> <!-- Профили --------------------------------------------------------------------->
<div v-if="selectedTab == 'profiles'" class="fit tab-panel"> <div v-if="selectedTab == 'profiles'" class="fit tab-panel">
@@include('./include/ProfilesTab.inc'); @@include('./ProfilesTab.inc');
</div> </div>
<!-- Вид -------------------------------------------------------------------------> <!-- Вид ------------------------------------------------------------------------->
<div v-if="selectedTab == 'view'" class="fit column"> <div v-if="selectedTab == 'view'" class="fit column">
@@include('./include/ViewTab.inc'); <q-tabs
v-model="selectedViewTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="mode" label="Режим" />
<q-tab name="color" label="Цвет" />
<q-tab name="font" label="Шрифт" />
<q-tab name="text" label="Текст" />
<q-tab name="status" label="Строка статуса" />
</q-tabs>
<div class="q-mb-sm" />
<div class="col tab-panel">
<div v-if="selectedViewTab == 'mode'">
@@include('./ViewTab/Mode.inc');
</div>
<div v-if="selectedViewTab == 'color'">
@@include('./ViewTab/Color.inc');
</div>
<div v-if="selectedViewTab == 'font'">
@@include('./ViewTab/Font.inc');
</div>
<div v-if="selectedViewTab == 'text'">
@@include('./ViewTab/Text.inc');
</div>
<div v-if="selectedViewTab == 'status'">
@@include('./ViewTab/Status.inc');
</div>
</div>
</div> </div>
<!-- Кнопки ----------------------------------------------------------------------> <!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel"> <div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./include/ButtonsTab.inc'); @@include('./ToolBarTab.inc');
</div> </div>
<!-- Управление ------------------------------------------------------------------> <!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column"> <div v-if="selectedTab == 'keys'" class="fit column">
@@include('./include/KeysTab.inc'); @@include('./KeysTab.inc');
</div> </div>
<!-- Листание --------------------------------------------------------------------> <!-- Листание -------------------------------------------------------------------->
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel"> <div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
@@include('./include/PageMoveTab.inc'); @@include('./PageMoveTab.inc');
</div> </div>
<!-- Конвертирование -------------------------------------------------------------> <!-- Конвертирование ------------------------------------------------------------->
<div v-if="selectedTab == 'convert'" class="fit tab-panel"> <div v-if="selectedTab == 'convert'" class="fit tab-panel">
@@include('./include/ConvertTab.inc'); @@include('./ConvertTab.inc');
</div>
<!-- Обновление ------------------------------------------------------------------>
<div v-if="selectedTab == 'update'" class="fit tab-panel">
@@include('./UpdateTab.inc');
</div> </div>
<!-- Прочее ----------------------------------------------------------------------> <!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel"> <div v-if="selectedTab == 'others'" class="fit tab-panel">
@@include('./include/OthersTab.inc'); @@include('./OthersTab.inc');
</div> </div>
<!-- Сброс -----------------------------------------------------------------------> <!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel"> <div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./include/ResetTab.inc'); @@include('./ResetTab.inc');
</div> </div>
</div> </div>
</div> </div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import { ref, watch } from 'vue';
import Component from 'vue-class-component'; import vueComponent from '../../vueComponent.js';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
@@ -85,12 +131,13 @@ 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';
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/; const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
NumInput, NumInput,
@@ -104,8 +151,9 @@ export default @Component({
this.settingsChanged(); this.settingsChanged();
}, },
form: function(newValue) { form: function(newValue) {
if (this.inited) if (this.inited) {
this.commit('reader/setSettings', newValue); this.commit('reader/setSettings', _.cloneDeep(newValue));
}
}, },
fontBold: function(newValue) { fontBold: function(newValue) {
this.fontWeight = (newValue ? 'bold' : ''); this.fontWeight = (newValue ? 'bold' : '');
@@ -165,12 +213,13 @@ export default @Component({
this.statusBarColor = newValue; this.statusBarColor = newValue;
}, },
}, },
}) };
class SettingsPage extends Vue { class SettingsPage {
_options = componentOptions;
selectedTab = 'profiles'; selectedTab = 'profiles';
selectedViewTab = 'mode'; selectedViewTab = 'mode';
selectedKeysTab = 'mouse'; selectedKeysTab = 'mouse';
form = {};
fontBold = false; fontBold = false;
fontItalic = false; fontItalic = false;
vertShift = 0; vertShift = 0;
@@ -186,6 +235,19 @@ class SettingsPage extends Vue {
toolButtons = []; toolButtons = [];
rstore = {}; rstore = {};
setup() {
const settingsProps = { form: ref({}) };
for (let prop in rstore.settingDefaults) {
settingsProps[prop] = ref(_.cloneDeep(rstore.settingDefaults[prop]));
watch(settingsProps[prop], (newValue) => {
settingsProps.form.value = Object.assign({}, settingsProps.form.value, {[prop]: newValue});
}, {deep: true});
}
return settingsProps;
}
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.reader = this.$store.state.reader; this.reader = this.$store.state.reader;
@@ -200,7 +262,7 @@ class SettingsPage extends Vue {
this.$watch( this.$watch(
'$refs.tabs.scrollable', '$refs.tabs.scrollable',
(newValue) => { (newValue) => {
this.tabsScrollable = newValue && !this.$isMobileDevice; this.tabsScrollable = newValue && !this.$root.isMobileDevice;
} }
); );
} }
@@ -215,18 +277,8 @@ class SettingsPage extends Vue {
return; return;
this.form = Object.assign({}, this.settings); this.form = Object.assign({}, this.settings);
if (!this.unwatch) for (const prop in rstore.settingDefaults) {
this.unwatch = {}; this[prop] = _.cloneDeep(this.form[prop]);
for (let prop in rstore.settingDefaults) {
if (this.unwatch && this.unwatch[prop])
this.unwatch[prop]();
this[prop] = this.form[prop];
this.unwatch[prop] = this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue});
});
} }
this.fontBold = (this.fontWeight == 'bold'); this.fontBold = (this.fontWeight == 'bold');
@@ -266,6 +318,10 @@ class SettingsPage extends Vue {
return this.$store.state.reader.profiles; return this.$store.state.reader.profiles;
} }
get configBucEnabled() {
return this.$store.state.config.bucEnabled;
}
get currentProfileOptions() { get currentProfileOptions() {
const profNames = Object.keys(this.profiles) const profNames = Object.keys(this.profiles)
profNames.sort(); profNames.sort();
@@ -421,10 +477,6 @@ class SettingsPage extends Vue {
} }
} }
changeShowToolButton(buttonName) {
this.showToolButton = Object.assign({}, this.showToolButton, {[buttonName]: !this.showToolButton[buttonName]});
}
async addProfile() { async addProfile() {
try { try {
if (Object.keys(this.profiles).length >= 100) { if (Object.keys(this.profiles).length >= 100) {
@@ -457,7 +509,7 @@ class SettingsPage extends Vue {
return; return;
try { try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` + const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` + `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', { `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; }, inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
@@ -550,7 +602,8 @@ class SettingsPage extends Vue {
}); });
if (result && result.value && result.value.toLowerCase() == 'да') { if (result && result.value && result.value.toLowerCase() == 'да') {
this.$root.$emit('generateNewServerStorageKey'); if (this.$root.generateNewServerStorageKey)
this.$root.generateNewServerStorageKey();
} }
} catch (e) { } catch (e) {
// //
@@ -595,8 +648,17 @@ class SettingsPage extends Vue {
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;
@@ -623,6 +685,27 @@ class SettingsPage extends Vue {
} }
} }
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();
@@ -630,6 +713,8 @@ class SettingsPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SettingsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -659,11 +744,11 @@ class SettingsPage extends Vue {
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

@@ -0,0 +1,76 @@
<!---------------------------------------------->
<div class="part-header">Обновление читалки</div>
<div class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
Проверять наличие новой версии
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Напоминать о необходимости обновления страницы<br>
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<!---------------------------------------------->
<div class="part-header">Обновление книг</div>
<div v-show="!configBucEnabled" class="item row">
<div class="label-6"></div>
<div>Сервер обновлений временно не работает</div>
</div>
<div v-show="configBucEnabled" class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="bucEnabled">
Проверять обновления книг
</q-checkbox>
</div>
<div v-show="configBucEnabled && bucEnabled" class="item row">
<div class="label-6"></div>
<div class="col-5 column justify-center items-end q-pr-xs">Разница размеров</div>
<div class="col row">
<NumInput class="col-left" v-model="bucSizeDiff" />
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Уведомлять о наличии обновления книги в списке загруженных<br>
при указанной разнице в размерах старого и нового файлов.<br>
Разница указывается в байтах и может быть отрицательной.
</q-tooltip>
</div>
</div>
<div v-show="configBucEnabled && bucEnabled" class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="bucSetOnNew">
Автопроверка для вновь загружаемых
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Автоматически устанавливать флаг проверки<br>
обновлений для всех вновь загружаемых книг
</q-tooltip>
</q-checkbox>
</div>
<div v-show="configBucEnabled && bucEnabled" class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="bucCancelEnabled">
Отменять проверку через {{ bucCancelDays }} дней{{ (bucCancelEnabled ? ':' : '') }}
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Снимать флаг проверки с книги, если не было<br>
обновлений в течение {{ bucCancelDays }} дней
</q-tooltip>
</q-checkbox>
</div>
<div v-show="configBucEnabled && bucEnabled && bucCancelEnabled" class="item row">
<div class="label-6"></div>
<div class="col-5"></div>
<div class="col row">
<NumInput class="col-left" v-model="bucCancelDays" :min="1" :max="10000"/>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Снимать флаг проверки с книги, если не было<br>
обновлений в течение {{ bucCancelDays }} дней
</q-tooltip>
</div>
</div>

View File

@@ -2,14 +2,20 @@
<div class="table col column no-wrap"> <div class="table col column no-wrap">
<!-- header --> <!-- header -->
<div class="table-row row"> <div class="table-row row">
<div class="desc q-pa-sm bg-blue-2">Команда</div> <div class="desc q-pa-sm bg-blue-2">
Команда
</div>
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap"> <div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
<div style="width: 80px">Сочетание клавиш</div> <div style="width: 80px">
<q-input ref="input" class="q-ml-sm col" Сочетание клавиш
outlined dense rounded </div>
bg-color="grey-4" <q-input
placeholder="Найти" ref="input"
v-model="search" v-model="search"
class="q-ml-sm col"
outlined dense
bg-color="grey-4"
placeholder="Найти"
@click.stop @click.stop
/> />
<div v-show="!readonly" class="q-ml-sm column justify-center"> <div v-show="!readonly" class="q-ml-sm column justify-center">
@@ -23,35 +29,38 @@
</div> </div>
<!-- body --> <!-- body -->
<div class="table-row row" v-for="(action, index) in tableData" :key="index"> <div v-for="(action, index) in tableData" :key="index" class="table-row row">
<div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div> <div class="desc q-pa-sm">
{{ rstore.readerActions[action] }}
</div>
<div class="hotKeys col q-pa-sm"> <div class="hotKeys col q-pa-sm">
<q-chip <q-chip
v-for="(code, index2) in modelValue[action]" :key="index2"
:color="collisions[code] ? 'red' : 'grey-7'" :color="collisions[code] ? 'red' : 'grey-7'"
:removable="!readonly" :clickable="collisions[code] ? true : false" :removable="!readonly" :clickable="collisions[code] ? true : false"
text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)" text-color="white" @remove="removeCode(action, code)"
@click="collisionWarning(code)" @click="collisionWarning(code)"
> >
{{ code }} {{ code }}
</q-chip> </q-chip>
</div> </div>
<div v-show="!readonly" class="column q-pa-xs"> <div v-show="!readonly" class="column q-pa-xs">
<q-icon <q-icon
v-ripple
:disabled="(modelValue[action].length >= maxCodesLength) || null"
name="la la-plus-circle" name="la la-plus-circle"
class="button bg-green-8 text-white" class="button bg-green-8 text-white"
@click="addHotKey(action)" @click="addHotKey(action)"
v-ripple
:disabled="value[action].length >= maxCodesLength"
> >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Добавить сочетание клавиш Добавить сочетание клавиш
</q-tooltip> </q-tooltip>
</q-icon> </q-icon>
<q-icon <q-icon
v-ripple
name="la la-broom" name="la la-broom"
class="button text-grey-5" class="button text-grey-5"
@click="defaultHotKey(action)" @click="defaultHotKey(action)"
v-ripple
> >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
По умолчанию По умолчанию
@@ -64,31 +73,29 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import rstore from '../../../../store/modules/reader'; import rstore from '../../../../store/modules/reader';
//import * as utils from '../../share/utils'; //import * as utils from '../../share/utils';
const UserHotKeysProps = Vue.extend({ const componentOptions = {
props: {
value: Object,
readonly: Boolean,
}
});
export default @Component({
watch: { watch: {
search: function() { search: function() {
this.updateTableData(); this.updateTableData();
}, },
value: function() { modelValue: function() {
this.checkCollisions(); this.checkCollisions();
this.updateTableData(); this.updateTableData();
} }
}, },
}) };
class UserHotKeys extends UserHotKeysProps { class UserHotKeys {
_options = componentOptions;
_props = {
modelValue: Object,
readonly: Boolean,
};
search = ''; search = '';
rstore = {}; rstore = {};
tableData = []; tableData = [];
@@ -113,7 +120,7 @@ class UserHotKeys extends UserHotKeysProps {
const search = this.search.toLowerCase(); const search = this.search.toLowerCase();
const codesIncludeSearch = (action) => { const codesIncludeSearch = (action) => {
for (const code of this.value[action]) { for (const code of this.modelValue[action]) {
if (code.toLowerCase().includes(search)) if (code.toLowerCase().includes(search))
return true; return true;
} }
@@ -131,7 +138,7 @@ class UserHotKeys extends UserHotKeysProps {
checkCollisions() { checkCollisions() {
const cols = {}; const cols = {};
for (const [action, codes] of Object.entries(this.value)) { for (const [action, codes] of Object.entries(this.modelValue)) {
codes.forEach(code => { codes.forEach(code => {
if (!cols[code]) if (!cols[code])
cols[code] = []; cols[code] = [];
@@ -158,26 +165,26 @@ class UserHotKeys extends UserHotKeysProps {
} }
removeCode(action, code) { removeCode(action, code) {
let codes = Array.from(this.value[action]); let codes = Array.from(this.modelValue[action]);
const index = codes.indexOf(code); const index = codes.indexOf(code);
if (index >= 0) { if (index >= 0) {
codes.splice(index, 1); codes.splice(index, 1);
const newValue = Object.assign({}, this.value, {[action]: codes}); const newValue = Object.assign({}, this.modelValue, {[action]: codes});
this.$emit('input', newValue); this.$emit('update:modelValue', newValue);
} }
} }
async addHotKey(action) { async addHotKey(action) {
if (this.value[action].length >= this.maxCodesLength) if (this.modelValue[action].length >= this.maxCodesLength)
return; return;
try { try {
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, ''); const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
if (result) { if (result) {
let codes = Array.from(this.value[action]); let codes = Array.from(this.modelValue[action]);
if (codes.indexOf(result) < 0) { if (codes.indexOf(result) < 0) {
codes.push(result); codes.push(result);
const newValue = Object.assign({}, this.value, {[action]: codes}); const newValue = Object.assign({}, this.modelValue, {[action]: codes});
this.$emit('input', newValue); this.$emit('update:modelValue', newValue);
this.$nextTick(() => { this.$nextTick(() => {
this.collisionWarning(result); this.collisionWarning(result);
}); });
@@ -192,8 +199,8 @@ class UserHotKeys extends UserHotKeysProps {
try { try {
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) { if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]); const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
const newValue = Object.assign({}, this.value, {[action]: codes}); const newValue = Object.assign({}, this.modelValue, {[action]: codes});
this.$emit('input', newValue); this.$emit('update:modelValue', newValue);
} }
} catch (e) { } catch (e) {
// //
@@ -204,13 +211,15 @@ class UserHotKeys extends UserHotKeysProps {
try { try {
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) { if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys); const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
this.$emit('input', newValue); this.$emit('update:modelValue', newValue);
} }
} catch (e) { } catch (e) {
// //
} }
} }
} }
export default vueComponent(UserHotKeys);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -0,0 +1,121 @@
<!---------------------------------------------->
<div class="hidden part-header">
Цвет
</div>
<div class="item row">
<div class="label-2">
Текст
</div>
<div class="col row">
<q-input
v-model="textColorFiltered"
class="col-left no-mp"
outlined dense
:rules="['hexColor']"
style="max-width: 150px"
>
<template #prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color
v-model="textColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="q-mt-md" />
<div class="item row">
<div class="label-2">
Фон
</div>
<div class="col row">
<q-input
v-model="bgColorFiltered"
class="col-left no-mp"
outlined dense
:rules="['hexColor']"
style="max-width: 150px"
>
<template #prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors" />
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="q-mt-md" />
<div class="item row">
<div class="label-2">
Обои
</div>
<div class="col row items-center">
<q-select
v-model="wallpaper"
class="col-left no-mp"
:options="wallpaperOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<template #selected-item="scope">
<div>
{{ scope.opt.label }}
</div>
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
</template>
<template #option="scope">
<q-item
v-bind="scope.itemProps"
>
<q-item-section style="min-width: 50px;">
<q-item-label v-html="scope.opt.label" />
</q-item-section>
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
</q-item>
</template>
</q-select>
<div class="q-px-xs" />
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить файл обоев
</q-tooltip>
</q-btn>
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Удалить выбранные обои
</q-tooltip>
</q-btn>
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Скачать выбранные обои
</q-tooltip>
</q-btn>
</div>
</div>
<div class="q-mt-sm" />
<div class="item row">
<div class="label-2"></div>
<div class="col row items-center">
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
</div>
</div>
<input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />

View File

@@ -1,10 +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" @input="changeShowToolButton(item.name)"
:value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>
</div>

View File

@@ -1,39 +0,0 @@
<q-tabs
v-model="selectedViewTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="mode" label="Режим" />
<q-tab name="color" label="Цвет" />
<q-tab name="font" label="Шрифт" />
<q-tab name="text" label="Текст" />
<q-tab name="status" label="Строка статуса" />
</q-tabs>
<div class="q-mb-sm"/>
<div class="col tab-panel">
<div v-if="selectedViewTab == 'mode'">
@@include('./ViewTab/Mode.inc');
</div>
<div v-if="selectedViewTab == 'color'">
@@include('./ViewTab/Color.inc');
</div>
<div v-if="selectedViewTab == 'font'">
@@include('./ViewTab/Font.inc');
</div>
<div v-if="selectedViewTab == 'text'">
@@include('./ViewTab/Text.inc');
</div>
<div v-if="selectedViewTab == 'status'">
@@include('./ViewTab/Status.inc');
</div>
</div>

View File

@@ -1,95 +0,0 @@
<!---------------------------------------------->
<div class="hidden part-header">Цвет</div>
<div class="item row">
<div class="label-2">Текст</div>
<div class="col row">
<q-input class="col-left no-mp"
outlined dense
v-model="textColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="textColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="q-mt-md"/>
<div class="item row">
<div class="label-2">Фон</div>
<div class="col row">
<q-input class="col-left no-mp"
outlined dense
v-model="bgColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="q-mt-md"/>
<div class="item row">
<div class="label-2">Обои</div>
<div class="col row items-center">
<q-select class="col-left no-mp" v-model="wallpaper" :options="wallpaperOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<template v-slot:selected-item="scope">
<div >{{ scope.opt.label }}</div>
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
</template>
<template v-slot:option="scope">
<q-item
v-bind="scope.itemProps"
v-on="scope.itemEvents"
>
<q-item-section style="min-width: 50px;">
<q-item-label v-html="scope.opt.label" />
</q-item-section>
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;"/>
</q-item>
</template>
</q-select>
<div class="q-px-xs"/>
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить файл обоев</q-tooltip>
</q-btn>
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить выбранные обои</q-tooltip>
</q-btn>
</div>
</div>
<div class="q-mt-sm"/>
<div class="item row">
<div class="label-2"></div>
<div class="col row items-center">
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
</div>
</div>
<input type="file" ref="file" @change="loadWallpaperFile" style='display: none;'/>

View File

@@ -6,26 +6,36 @@
</div> </div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd"> <div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div v-html="page1"></div> <div @copy.prevent="copyText" v-html="page1"></div>
</div> </div>
</div> </div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd"> <div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div v-html="page2"></div> <div @copy.prevent="copyText" v-html="page2"></div>
</div> </div>
</div> </div>
<div v-show="showStatusBar" ref="statusBar" class="layout"> <div v-show="showStatusBar" ref="statusBar" class="layout">
<div v-html="statusBar"></div> <div v-html="statusBar"></div>
</div> </div>
<div v-show="clickControl" ref="layoutEvents" class="layout events" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp" <div
v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@wheel.prevent.stop="onMouseWheel" @wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel" @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
oncontextmenu="return false;"> >
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop <div
@click.prevent.stop="onStatusBarClick"></div> v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div> </div>
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop <div
@click.prevent.stop="onStatusBarClick"> v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
>
</div> </div>
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты --> <!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas> <canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
@@ -35,10 +45,11 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import {loadCSS} from 'fg-loadcss'; import {loadCSS} from 'fg-loadcss';
import _ from 'lodash'; import _ from 'lodash';
import he from 'he';
import './TextPage.css'; import './TextPage.css';
@@ -51,11 +62,18 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100; const minLayoutWidth = 100;
export default @Component({ 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});
@@ -70,8 +88,10 @@ export default @Component({
this.updateLayout(); this.updateLayout();
}, },
}, },
}) };
class TextPage extends Vue { class TextPage {
_options = componentOptions;
toggleLayout = false; toggleLayout = false;
showStatusBar = false; showStatusBar = false;
clickControl = true; clickControl = true;
@@ -86,6 +106,8 @@ class TextPage extends Vue {
lastBook = null; lastBook = null;
bookPos = 0; bookPos = 0;
bookPosSeen = null; bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null; fontStyle = null;
fontSize = null; fontSize = null;
@@ -140,9 +162,9 @@ class TextPage extends Vue {
await this.doPageAnimation(); await this.doPageAnimation();
}, 10); }, 10);
this.$root.$on('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);
}); });
} }
@@ -436,7 +458,7 @@ class TextPage extends Vue {
this.title = bt.fullTitle; this.title = bt.fullTitle;
this.$root.$emit('set-app-title', this.title); this.$root.setAppTitle(this.title);
this.parsed = this.book.parsed; this.parsed = this.book.parsed;
@@ -486,12 +508,25 @@ class TextPage extends Vue {
} }
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;
} }
} }
@@ -639,7 +674,7 @@ class TextPage extends Vue {
} }
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;
} }
@@ -662,7 +697,7 @@ class TextPage extends Vue {
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;
} }
} }
@@ -898,12 +933,14 @@ class TextPage extends Vue {
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;
} }
} }
@@ -916,6 +953,7 @@ class TextPage extends Vue {
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();
@@ -931,6 +969,7 @@ class TextPage extends Vue {
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;
} }
} }
@@ -939,10 +978,11 @@ class TextPage extends Vue {
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;
@@ -953,6 +993,7 @@ class TextPage extends Vue {
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;
} }
} }
@@ -1036,7 +1077,7 @@ class TextPage extends Vue {
} }
onTouchStart(event) { onTouchStart(event) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
@@ -1064,7 +1105,7 @@ class TextPage extends Vue {
} }
onTouchEnd(event) { onTouchEnd(event) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
@@ -1100,13 +1141,13 @@ class TextPage extends Vue {
} }
onTouchCancel() { onTouchCancel() {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
} }
onMouseDown(event) { onMouseDown(event) {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
if (event.button == 0) { if (event.button == 0) {
@@ -1123,13 +1164,13 @@ class TextPage extends Vue {
} }
onMouseUp() { onMouseUp() {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
} }
onMouseWheel(event) { onMouseWheel(event) {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
if (event.deltaY > 0) { if (event.deltaY > 0) {
this.doDown(); this.doDown();
@@ -1192,9 +1233,57 @@ class TextPage extends Vue {
} }
return action; return action;
} }
copyText(event) {
//все это для того, чтобы правильно расставить переносы \n при копировании текста
//прямо с текущей страницы
//подготовка, вытаскиваем весь текст страницы
const lines = this.getLines(this.bookPos);
const decodedLines = [];
for (const line of lines.linesDown) {
let lineText = '';
for (const part of line.parts) {
lineText += part.text;
}
decodedLines.push({text: he.decode(lineText), first: line.first});
}
let i = 0;
const findDecoded = (line) => {
for (let j = i; j < decodedLines.length; j++) {
const decoded = decodedLines[j];
if (decoded.text.indexOf(line) >= 0) {
i = j;
return decoded;
}
}
return;
}
const selection = document.getSelection();
const splitted = selection.toString().split(/[\n\r]/);
let filtered = '';
//формируем filtered, учитывая переносы из decodedLines
for (const line of splitted) {
const found = findDecoded(line);
if (found && found.first) {
filtered += (filtered ? '\n' : '') + line;
} else {
filtered += (filtered ? '\r ' : '') + line;
}
}
//маленькие хитрости, убираем переносы по слогам
filtered = filtered.replace(/-\r /g, '').replace(/\r /g, ' ');
event.clipboardData.setData('text/plain', filtered);
}
} }
export default vueComponent(TextPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>

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 = [];
@@ -159,7 +162,24 @@ export default class BookParser {
const prevParaIndex = paraIndex; const prevParaIndex = paraIndex;
let p = para[paraIndex]; let p = para[paraIndex];
paraOffset -= p.length; paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим непустым
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
let newParaText = p.text.trim();
newParaText = (newParaText.length ? newParaText : ' ');
const ldiff = p.text.length - newParaText.length;
if (ldiff != 0) {
p.text = newParaText;
p.length -= ldiff;
}
//удаление параграфов, которые содержат только разметку, такого не должно быть
if (!p.length) {
delete para[paraIndex];
paraIndex--;
return;
}
//добавление пустых (не)видимых (addEmptyParagraphs) параграфов перед текущим непустым
if (p.text.trim() != '') { if (p.text.trim() != '') {
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
para[paraIndex] = { para[paraIndex] = {
@@ -179,15 +199,6 @@ export default class BookParser {
curSubtitle.paraIndex = paraIndex; curSubtitle.paraIndex = paraIndex;
} }
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
let newParaText = p.text.trim();
newParaText = (newParaText.length ? newParaText : ' ');
const ldiff = p.text.length - newParaText.length;
if (ldiff != 0) {
p.text = newParaText;
p.length -= ldiff;
}
p.index = paraIndex; p.index = paraIndex;
p.offset = paraOffset; p.offset = paraOffset;
para[paraIndex] = p; para[paraIndex] = p;
@@ -203,7 +214,7 @@ export default class BookParser {
let p = { let p = {
index: paraIndex, index: paraIndex,
offset: paraOffset, offset: paraOffset,
length: len, length: len,//длина текста внутри параграфа без учета длины разметки
text: text, text: text,
addIndex: 0, addIndex: 0,
}; };
@@ -218,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) {
@@ -232,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;
@@ -258,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)
@@ -270,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++;
@@ -528,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}`);
@@ -63,48 +67,6 @@ class BookManager {
} }
await this.cleanRecentBooks(); await this.cleanRecentBooks();
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
{
await this.convertFileToDiskPrefix();
if (this.recentRev > 10)
await bmRecentStoreOld.clear();
}
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
let key = null;
const len = await bmRecentStoreOld.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStoreOld.key(i);
if (key) {
let r = await bmRecentStoreOld.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStoreOld.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await bmRecentStoreNew.setItem('recent', this.recent);
this.recentRev = 1;
await bmRecentStoreNew.setItem('rev', this.recentRev);
} }
this.recentChanged = true; this.recentChanged = true;
@@ -112,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() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
@@ -238,8 +234,12 @@ 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.addTime = Date.now(); if (newBook.downloadSize !== undefined && newBook.downloadSize >= 0)
meta.downloadSize = newBook.downloadSize;
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => { const cb = (perc) => {
const p = Math.round(30*perc/100); const p = Math.round(30*perc/100);
@@ -274,10 +274,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];
@@ -292,8 +292,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];
@@ -303,11 +307,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);
@@ -352,9 +351,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
}); });
@@ -367,14 +395,24 @@ 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');
if (rev != this.recentRev && !skipCheck) { if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
const newRecent = await bmRecentStoreNew.getItem('recent'); const newRecent = await bmRecentStoreNew.getItem('recent');
Object.assign(this.recent, newRecent); Object.assign(this.recent, newRecent);
this.recentItem = await bmRecentStoreNew.getItem('recent-item'); this.recentItem = await bmRecentStoreNew.getItem('recent-item');
@@ -411,7 +449,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]) {
@@ -427,9 +468,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);
@@ -439,11 +480,43 @@ 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 setCheckBuc(value, checkBuc) {
const item = this.recent[value.key];
const updateItems = [];
if (item) {
if (item.sameBookKey !== undefined) {
const sorted = this.getSortedRecent();
for (const book of sorted) {
if (!book.deleted && book.sameBookKey === item.sameBookKey)
updateItems.push(book);
}
} else {
updateItems.push(item);
}
}
const now = Date.now();
for (const book of updateItems) {
book.checkBuc = checkBuc;
if (checkBuc)
book.checkBucTime = now;
await this.recentSetItem(book);
}
}
async cleanRecentBooks() { async cleanRecentBooks() {
const sorted = this.getSortedRecent(); const sorted = this.getSortedRecent();
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;
} }
@@ -455,33 +528,6 @@ class BookManager {
return isDel; return isDel;
} }
async convertFileToDiskPrefix() {
let isConverted = false;
const newRecent = {};
for (let key of Object.keys(this.recent)) {
let newKey = key;
let newUrl = this.recent[key].url;
if (newKey.indexOf('66696c65') == 0) {
newKey = newKey.replace(/^66696c65/, '6469736b');
if (newUrl)
newUrl = newUrl.replace(/^file/, 'disk');
isConverted = true;
}
newRecent[newKey] = this.recent[key];
newRecent[newKey].key = newKey;
if (newUrl)
newRecent[newKey].url = newUrl;
}
if (isConverted) {
this.recent = newRecent;
await this.recentSetItem(null, true);
}
return isConverted;
}
mostRecentBook() { mostRecentBook() {
if (this.recentLastKey) { if (this.recentLastKey) {
return this.recent[this.recentLastKey]; return this.recent[this.recentLastKey];
@@ -490,7 +536,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;
@@ -521,6 +567,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,29 +1,183 @@
export const versionHistory = [ export const versionHistory = [
{ {
showUntil: '2021-10-18', version: '0.12.2',
header: '0.10.2 (2021-10-19)', releaseDate: '2022-09-04',
showUntil: '2022-09-11',
content: content:
` `
<ul> <ul>
<li>актуализация версий пакетов и стека используемых технологий</li> <li>исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц</li>
<li>автор приносит извинения за доставленные неудобства</li>
</ul> </ul>
` `
}, },
{ {
showUntil: '2021-10-09', version: '0.12.1',
header: '0.10.1 (2021-10-10)', releaseDate: '2022-09-01',
showUntil: '2022-08-30',
content:
`
<ul>
<li>добавлена форма для доната</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.12.0',
releaseDate: '2022-07-27',
showUntil: '2022-08-03',
content:
`
<ul>
<li>запущен сервер проверки обновлений книг:</li>
<ul>
<li>проверка обновления той или иной книги настраивается в списке загруженных (чекбокс)</li>
<li>для того, чтобы чекбокс появился у ранее загруженной, необходимо принудительно обновить книгу</li>
<li>в настройках можно указать разницу размеров, при которой требуется делать уведомление</li>
</ul>
</ul>
`
},
{
version: '0.11.8',
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',
releaseDate: '2022-07-02',
showUntil: '2022-07-01',
content:
`
<ul>
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
<li>актуализация используемых пакетов</li>
</ul>
`
},
{
version: '0.11.5',
releaseDate: '2022-04-15',
showUntil: '2022-04-14',
content:
`
<ul>
<li>небольшие дополнения интерфейса</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.1',
releaseDate: '2021-12-03',
showUntil: '2021-12-02',
content:
`
<ul>
<li>переход на JembaDb вместо SQLite</li>
</ul>
`
},
{
version: '0.11.0',
releaseDate: '2021-11-18',
showUntil: '2021-11-17',
content:
`
<ul>
<li>переход на Vue 3</li>
</ul>
`
},
{
version: '0.10.3',
releaseDate: '2021-10-24',
showUntil: '2021-10-23',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.10.2',
releaseDate: '2021-10-19',
showUntil: '2021-10-18',
content:
`
<ul>
<li>актуализация версий пакетов и стека используемых технологий</li>
</ul>
`
},
{
version: '0.10.1',
releaseDate: '2021-10-10',
showUntil: '2021-10-09',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.0',
releaseDate: '2021-02-09',
showUntil: '2021-02-16', showUntil: '2021-02-16',
header: '0.10.0 (2021-02-09)',
content: content:
` `
<ul> <ul>
@@ -32,12 +186,14 @@ export const versionHistory = [
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li> <li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
<li>немного улучшен парсинг fb2</li> <li>немного улучшен парсинг fb2</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.12',
releaseDate: '2020-12-18',
showUntil: '2020-12-17', showUntil: '2020-12-17',
header: '0.9.12 (2020-12-18)',
content: content:
` `
<ul> <ul>
@@ -46,23 +202,27 @@ export const versionHistory = [
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li> <li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
<li>улучшения работы конвертеров</li> <li>улучшения работы конвертеров</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.11',
releaseDate: '2020-12-09',
showUntil: '2020-12-08', showUntil: '2020-12-08',
header: '0.9.11 (2020-12-09)',
content: content:
` `
<ul> <ul>
<li>оптимизации, улучшения работы конвертеров</li> <li>оптимизации, улучшения работы конвертеров</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.10',
releaseDate: '2020-12-03',
showUntil: '2020-12-10', showUntil: '2020-12-10',
header: '0.9.10 (2020-12-03)',
content: content:
` `
<ul> <ul>
@@ -70,69 +230,81 @@ export const versionHistory = [
<li>добавлена поддержка Rar-архивов</li> <li>добавлена поддержка Rar-архивов</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.9',
releaseDate: '2020-11-21',
showUntil: '2020-11-20', showUntil: '2020-11-20',
header: '0.9.9 (2020-11-21)',
content: content:
` `
<ul> <ul>
<li>оптимизации, исправления багов</li> <li>оптимизации, исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.8',
releaseDate: '2020-11-13',
showUntil: '2020-11-12', showUntil: '2020-11-12',
header: '0.9.8 (2020-11-13)',
content: content:
` `
<ul> <ul>
<li>добавлено окно "Оглавление/закладки"</li> <li>добавлено окно "Оглавление/закладки"</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.7',
releaseDate: '2020-11-12',
showUntil: '2020-11-11', showUntil: '2020-11-11',
header: '0.9.7 (2020-11-12)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.6',
releaseDate: '2020-11-06',
showUntil: '2020-11-05', showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content: content:
` `
<ul> <ul>
<li>завершена работа над новым окном "Библиотека"</li> <li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.5',
releaseDate: '2020-11-01',
showUntil: '2020-10-31', showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)',
content: content:
` `
<ul> <ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li> <li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.4',
releaseDate: '2020-10-29',
showUntil: '2020-10-28', showUntil: '2020-10-28',
header: '0.9.4 (2020-10-29)',
content: content:
` `
<ul> <ul>
@@ -140,23 +312,27 @@ export const versionHistory = [
<li>для liberama.top добавлено новое окно: "Библиотека"</li> <li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.3',
releaseDate: '2020-05-21',
showUntil: '2020-05-20', showUntil: '2020-05-20',
header: '0.9.3 (2020-05-21)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.2',
releaseDate: '2020-03-15',
showUntil: '2020-04-25', showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content: content:
` `
<ul> <ul>
@@ -164,119 +340,139 @@ export const versionHistory = [
<li>переход на Service Worker вместо AppCache для автономного режима работы</li> <li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.1',
releaseDate: '2020-03-03',
showUntil: '2020-03-02', showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content: content:
` `
<ul> <ul>
<li>улучшение работы серверной части</li> <li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li> <li>незначительные изменения интерфейса</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.0',
releaseDate: '2020-02-26',
showUntil: '2020-02-25', showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',
content: content:
` `
<ul> <ul>
<li>переход на UI-фреймфорк Quasar</li> <li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li> <li>незначительные изменения интерфейса</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.4',
releaseDate: '2020-02-06',
showUntil: '2020-02-05', showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content: content:
` `
<ul> <ul>
<li>добавлен paypal-адрес для пожертвований</li> <li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.3',
releaseDate: '2020-01-28',
showUntil: '2020-01-27', showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content: content:
` `
<ul> <ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li> <li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li> <li>внутренние оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.2',
releaseDate: '2020-01-20',
showUntil: '2020-01-19', showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content: content:
` `
<ul> <ul>
<li>внутренние оптимизации</li> <li>внутренние оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.1',
releaseDate: '2020-01-07',
showUntil: '2020-01-06', showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content: content:
` `
<ul> <ul>
<li>добавлена частичная поддержка формата FB3</li> <li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li> <li>исправлен баг "Request path contains unescaped characters"</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.0',
releaseDate: '2020-01-02',
showUntil: '2020-01-05', showUntil: '2020-01-05',
header: '0.8.0 (2020-01-02)',
content: content:
` `
<ul> <ul>
<li>окончательный переход на https</li> <li>окончательный переход на https</li>
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li> <li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.9',
releaseDate: '2019-11-27',
showUntil: '2019-11-26', showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content: content:
` `
<ul> <ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li> <li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.8',
releaseDate: '2019-11-25',
showUntil: '2019-11-24', showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)',
content: content:
` `
<ul> <ul>
<li>улучшение html-фильтров для сайтов</li> <li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.7',
releaseDate: '2019-11-06',
showUntil: '2019-11-10', showUntil: '2019-11-10',
header: '0.7.7 (2019-11-06)',
content: content:
` `
<ul> <ul>
@@ -288,34 +484,40 @@ export const versionHistory = [
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li> <li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
</ul> </ul>
</ul> </ul>
` `
}, },
{ {
version: '0.7.6',
releaseDate: '2019-10-30',
showUntil: '2019-10-29', showUntil: '2019-10-29',
header: '0.7.6 (2019-10-30)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.5',
releaseDate: '2019-10-22',
showUntil: '2019-10-21', showUntil: '2019-10-21',
header: '0.7.5 (2019-10-22)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.3',
releaseDate: '2019-10-18',
showUntil: '2019-10-17', showUntil: '2019-10-17',
header: '0.7.3 (2019-10-18)',
content: content:
` `
<ul> <ul>
@@ -324,12 +526,14 @@ export const versionHistory = [
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li> <li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.1',
releaseDate: '2019-09-20',
showUntil: '2019-09-19', showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content: content:
` `
<ul> <ul>
@@ -337,12 +541,14 @@ export const versionHistory = [
<li>на панель управления добавлена кнопка "Автономный режим"</li> <li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li> <li>актуализирована справка</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.0',
releaseDate: '2019-09-07',
showUntil: '2019-10-01', showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content: content:
` `
<ul> <ul>
@@ -353,23 +559,27 @@ export const versionHistory = [
<li>немного улучшен внешний вид и управление на смартфонах</li> <li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li> <li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.10',
releaseDate: '2019-07-21',
showUntil: '2019-07-20', showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.9',
releaseDate: '2019-06-23',
showUntil: '2019-06-22', showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content: content:
` `
<ul> <ul>
@@ -380,12 +590,14 @@ export const versionHistory = [
<li>улучшены прогрессбары</li> <li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li> <li>исправления недочетов, небольшие оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.7',
releaseDate: '2019-05-30',
showUntil: '2019-06-05', showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content: content:
` `
<ul> <ul>
@@ -398,36 +610,42 @@ export const versionHistory = [
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li> <li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li> <li>исправления багов и недочетов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.6',
releaseDate: '2019-03-29',
showUntil: '2019-03-29', showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content: content:
` `
<ul> <ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li> <li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li> <li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.4',
releaseDate: '2019-03-24',
showUntil: '2019-03-24', showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content: content:
` `
<ul> <ul>
<li>исправления багов, оптимизации</li> <li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li> <li>добавлена возможность синхронизации данных между устройствами</li>
</ul> </ul>
` `
}, },
{ {
version: '0.5.4',
releaseDate: '2019-03-04',
showUntil: '2019-03-04', showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content: content:
` `
<ul> <ul>
@@ -436,12 +654,14 @@ export const versionHistory = [
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li> <li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li> <li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul> </ul>
` `
}, },
{ {
version: '0.3.0',
releaseDate: '2019-02-17',
showUntil: '2019-02-17', showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content: content:
` `
<ul> <ul>
@@ -449,12 +669,14 @@ export const versionHistory = [
<li>улучшено распознавание текста</li> <li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li> <li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul> </ul>
` `
}, },
{ {
version: '0.1.7',
releaseDate: '2019-02-14',
showUntil: '2019-02-14', showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content: content:
` `
<ul> <ul>
@@ -464,17 +686,20 @@ export const versionHistory = [
<li>добавлена возможность сброса настроек</li> <li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li> <li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul> </ul>
` `
}, },
{ {
version: '0.1.0',
releaseDate: '2019-02-12',
showUntil: '2019-02-12', showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content: content:
` `
<ul> <ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li> <li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul> </ul>
` `
}, },

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Settings {
})
class Settings extends Vue {
created() { created() {
} }
} }
export default vueComponent(Settings);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -6,15 +6,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Sources {
})
class Sources extends Vue {
created() { created() {
} }
} }
export default vueComponent(Sources);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,12 +1,12 @@
<template> <template>
<q-dialog v-model="active" no-route-dismiss> <q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
<div class="column bg-white no-wrap"> <div class="column bg-white no-wrap">
<div class="header row"> <div class="header row">
<div class="caption col row items-center q-ml-md"> <div class="caption col row items-center q-ml-md">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup> <q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon> <q-icon name="la la-times" size="18px"></q-icon>
</q-btn> </q-btn>
</div> </div>
@@ -25,26 +25,42 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component'; import * as utils from '../../share/utils';
const DialogProps = Vue.extend({ class Dialog {
props: { _props = {
value: Boolean, modelValue: Boolean,
} };
});
shown = false;
export default @Component({
})
class Dialog extends DialogProps {
get active() { get active() {
return this.value; return this.modelValue;
} }
set active(value) { set active(value) {
this.$emit('input', value); this.$emit('update:modelValue', value);
}
onShow() {
this.shown = true;
}
onHide() {
this.shown = false;
}
async waitShown() {
let i = 100;
while (!this.shown && i > 0) {
await utils.sleep(10);
i--;
}
} }
} }
export default vueComponent(Dialog);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -4,12 +4,9 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Notify {
})
class Notify extends Vue {
notify(opts) { notify(opts) {
let { let {
caption = null, caption = null,
@@ -30,9 +27,10 @@ class Notify extends Vue {
icon, icon,
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}], actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
html: true, html: true,
classes: 'notify-margin',
message: message:
`<div style="max-width: 350px;"> `<div style="max-width: 350px">
${caption} ${caption}
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div> <div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
</div>` </div>`
@@ -55,5 +53,7 @@ class Notify extends Vue {
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options)); this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
} }
} }
export default vueComponent(Notify);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,17 +1,19 @@
<template> <template>
<q-input outlined dense <q-input
v-model="filteredValue" v-model="filteredValue"
outlined dense
input-style="text-align: center" input-style="text-align: center"
class="no-mp" class="no-mp"
:class="(error ? 'error' : '')" :class="(error ? 'error' : '')"
:disable="disable" :disable="disable"
> >
<slot></slot> <slot></slot>
<template v-slot:prepend> <template #prepend>
<q-icon :class="(validate(value - step) ? '' : 'disable')" <q-icon
v-ripple="validate(modelValue - step)"
:class="(validate(modelValue - step) ? '' : 'disable')"
name="la la-minus-circle" name="la la-minus-circle"
class="button" class="button"
v-ripple="validate(value - step)"
@click="minus" @click="minus"
@mousedown.prevent.stop="onMouseDown($event, 'minus')" @mousedown.prevent.stop="onMouseDown($event, 'minus')"
@mouseup.prevent.stop="onMouseUp" @mouseup.prevent.stop="onMouseUp"
@@ -21,11 +23,12 @@
@touchcancel.prevent.stop="onTouchEnd" @touchcancel.prevent.stop="onTouchEnd"
/> />
</template> </template>
<template v-slot:append> <template #append>
<q-icon :class="(validate(value + step) ? '' : 'disable')" <q-icon
v-ripple="validate(modelValue + step)"
:class="(validate(modelValue + step) ? '' : 'disable')"
name="la la-plus-circle" name="la la-plus-circle"
class="button" class="button"
v-ripple="validate(value + step)"
@click="plus" @click="plus"
@mousedown.prevent.stop="onMouseDown($event, 'plus')" @mousedown.prevent.stop="onMouseDown($event, 'plus')"
@mouseup.prevent.stop="onMouseUp" @mouseup.prevent.stop="onMouseUp"
@@ -40,43 +43,41 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
const NumInputProps = Vue.extend({ const componentOptions = {
props: { watch: {
value: Number, filteredValue: function(newValue) {
if (this.validate(newValue)) {
this.error = false;
this.$emit('update:modelValue', this.string2number(newValue));
} else {
this.error = true;
}
},
modelValue: function(newValue) {
this.filteredValue = newValue;
},
}
};
class NumInput {
_options = componentOptions;
_props = {
modelValue: Number,
min: { type: Number, default: -Number.MAX_VALUE }, min: { type: Number, default: -Number.MAX_VALUE },
max: { type: Number, default: Number.MAX_VALUE }, max: { type: Number, default: Number.MAX_VALUE },
step: { type: Number, default: 1 }, step: { type: Number, default: 1 },
digits: { type: Number, default: 0 }, digits: { type: Number, default: 0 },
disable: Boolean disable: Boolean
} };
});
export default @Component({
watch: {
filteredValue: function(newValue) {
if (this.validate(newValue)) {
this.error = false;
this.$emit('input', this.string2number(newValue));
} else {
this.error = true;
}
},
value: function(newValue) {
this.filteredValue = newValue;
},
}
})
class NumInput extends NumInputProps {
filteredValue = 0; filteredValue = 0;
error = false; error = false;
created() { created() {
this.filteredValue = this.value; this.filteredValue = this.modelValue;
} }
string2number(value) { string2number(value) {
@@ -95,13 +96,13 @@ class NumInput extends NumInputProps {
} }
plus() { plus() {
const newValue = this.value + this.step; const newValue = this.modelValue + this.step;
if (this.validate(newValue)) if (this.validate(newValue))
this.filteredValue = newValue; this.filteredValue = newValue;
} }
minus() { minus() {
const newValue = this.value - this.step; const newValue = this.modelValue - this.step;
if (this.validate(newValue)) if (this.validate(newValue))
this.filteredValue = newValue; this.filteredValue = newValue;
} }
@@ -136,7 +137,7 @@ class NumInput extends NumInputProps {
} }
onTouchStart(event, way) { onTouchStart(event, way) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
if (event.touches.length == 1) { if (event.touches.length == 1) {
this.inTouch = true; this.inTouch = true;
@@ -145,12 +146,14 @@ class NumInput extends NumInputProps {
} }
onTouchEnd() { onTouchEnd() {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.inTouch = false; this.inTouch = false;
this.onMouseUp(); this.onMouseUp();
} }
} }
export default vueComponent(NumInput);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss> <q-dialog ref="dialog" v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
<slot></slot> <slot></slot>
<!---------------------------------------------------> <!--------------------------------------------------->
@@ -10,7 +10,7 @@
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup> <q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon> <q-icon name="la la-times" size="18px"></q-icon>
</q-btn> </q-btn>
</div> </div>
@@ -21,7 +21,9 @@
</div> </div>
<div class="buttons row justify-end q-pa-md"> <div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn> <q-btn class="q-px-md" dense no-caps @click="okClick">
OK
</q-btn>
</div> </div>
</div> </div>
@@ -33,7 +35,7 @@
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup> <q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon> <q-icon name="la la-times" size="18px"></q-icon>
</q-btn> </q-btn>
</div> </div>
@@ -44,8 +46,40 @@
</div> </div>
<div class="buttons row justify-end q-pa-md"> <div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn> <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn> Отмена
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
OK
</q-btn>
</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> </div>
@@ -57,7 +91,7 @@
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup> <q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon> <q-icon name="la la-times" size="18px"></q-icon>
</q-btn> </q-btn>
</div> </div>
@@ -65,13 +99,19 @@
<div class="q-mx-md"> <div class="q-mx-md">
<div v-html="message"></div> <div v-html="message"></div>
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/> <q-input ref="input" v-model="inputValue" class="q-mt-xs" outlined dense />
<div class="error"><span v-show="error != ''">{{ error }}</span></div> <div class="error">
<span v-show="error != ''">{{ error }}</span>
</div>
</div> </div>
<div class="buttons row justify-end q-pa-md"> <div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn> <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn> Отмена
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
OK
</q-btn>
</div> </div>
</div> </div>
@@ -83,7 +123,7 @@
<div v-html="caption"></div> <div v-html="caption"></div>
</div> </div>
<div class="close-icon column justify-center items-center"> <div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup> <q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon> <q-icon name="la la-times" size="18px"></q-icon>
</q-btn> </q-btn>
</div> </div>
@@ -92,14 +132,20 @@
<div class="q-mx-md"> <div class="q-mx-md">
<div v-html="message"></div> <div v-html="message"></div>
<div class="q-my-md text-center"> <div class="q-my-md text-center">
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div> <div v-show="hotKeyCode == ''" class="text-grey-5">
Нет
</div>
<div>{{ hotKeyCode }}</div> <div>{{ hotKeyCode }}</div>
</div> </div>
</div> </div>
<div class="buttons row justify-end q-pa-md"> <div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn> <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn> Отмена
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps :disabled="hotKeyCode == ''" @click="okClick">
OK
</q-btn>
</div> </div>
</div> </div>
</q-dialog> </q-dialog>
@@ -107,19 +153,18 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
export default @Component({ const componentOptions = {
watch: { watch: {
inputValue: function(newValue) { inputValue: function(newValue) {
this.validate(newValue); this.validate(newValue);
}, },
} }
}) };
class StdDialog extends Vue { class StdDialog {
_options = componentOptions;
caption = ''; caption = '';
message = ''; message = '';
active = false; active = false;
@@ -131,8 +176,8 @@ class StdDialog extends Vue {
hotKeyCode = ''; hotKeyCode = '';
created() { created() {
if (this.$root.addKeyHook) { if (this.$root.addEventHook) {
this.$root.addKeyHook(this.keyHook); this.$root.addEventHook('key', this.keyHook);
} }
} }
@@ -245,6 +290,23 @@ class StdDialog extends Vue {
}); });
} }
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;
@@ -313,6 +375,8 @@ class StdDialog extends Vue {
} }
} }
} }
export default vueComponent(StdDialog);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -2,11 +2,17 @@
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove"> <div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop> <div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
<div ref="window" class="window flexfit column no-wrap"> <div ref="window" class="window flexfit column no-wrap">
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown" <div
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove"> ref="header"
class="header row justify-end"
@mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart"
@touchend.stop="onTouchEnd"
@touchmove.stop="onTouchMove"
>
<span class="header-text col"><slot name="header"></slot></span> <span class="header-text col"><slot name="header"></slot></span>
<slot name="buttons"></slot> <slot name="buttons"></slot>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span> <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
</div> </div>
<slot></slot> <slot></slot>
@@ -17,19 +23,17 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Window {
props: { _props = {
height: { type: String, default: '100%' }, height: { type: String, default: '100%' },
width: { type: String, default: '100%' }, width: { type: String, default: '100%' },
maxWidth: { type: String, default: '' }, maxWidth: { type: String, default: '' },
topShift: { type: Number, default: 0 }, topShift: { type: Number, default: 0 },
margin: '', margin: '',
} };
})
class Window extends Vue {
init() { init() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.main.style.top = 0; this.$refs.main.style.top = 0;
@@ -51,7 +55,7 @@ class Window extends Vue {
} }
onMouseDown(event) { onMouseDown(event) {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
if (event.button == 0) { if (event.button == 0) {
this.$refs.header.style.cursor = 'move'; this.$refs.header.style.cursor = 'move';
@@ -81,7 +85,7 @@ class Window extends Vue {
} }
onTouchStart(event) { onTouchStart(event) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
if (event.touches.length == 1) { if (event.touches.length == 1) {
const touch = event.touches[0]; const touch = event.touches[0];
@@ -93,7 +97,7 @@ class Window extends Vue {
} }
onTouchMove(event) { onTouchMove(event) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
if (event.touches.length == 1 && this.moving) { if (event.touches.length == 1 && this.moving) {
const touch = event.touches[0]; const touch = event.touches[0];
@@ -108,7 +112,7 @@ class Window extends Vue {
} }
onTouchEnd() { onTouchEnd() {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.$refs.header.style.cursor = 'default'; this.$refs.header.style.cursor = 'default';
this.moving = false; this.moving = false;
@@ -120,6 +124,8 @@ class Window extends Vue {
this.$emit('close'); this.$emit('close');
} }
} }
export default vueComponent(Window);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -147,7 +153,7 @@ class Window extends Vue {
} }
.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;
} }
@@ -155,8 +161,8 @@ class Window extends Vue {
.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;
} }
@@ -168,7 +174,8 @@ class Window extends Vue {
} }
.close-button:hover { .close-button:hover {
background-color: #69C05F; color: white;
background-color: #FF3030;
} }
</style> </style>

View File

@@ -0,0 +1,52 @@
import { defineComponent } from 'vue';
import _ from 'lodash';
export default function(componentClass) {
const comp = {};
const obj = new componentClass();
//data, options, props
const data = {};
for (const prop of Object.getOwnPropertyNames(obj)) {
if (['_options', '_props'].includes(prop)) {//meta props
if (prop === '_options') {
const options = obj[prop];
for (const optName of ['components', 'watch', 'emits']) {
if (options[optName]) {
comp[optName] = options[optName];
}
}
} else if (prop === '_props') {
comp['props'] = obj[prop];
}
} else {//usual prop
data[prop] = obj[prop];
}
}
comp.data = () => _.cloneDeep(data);
//methods
const classProto = Object.getPrototypeOf(obj);
const classMethods = Object.getOwnPropertyNames(classProto);
const methods = {};
const computed = {};
for (const method of classMethods) {
const desc = Object.getOwnPropertyDescriptor(classProto, method);
if (desc.get) {//has getter, computed
computed[method] = {get: desc.get};
if (desc.set)
computed[method].set = desc.set;
} else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
'setup'].includes(method) ) {
comp[method] = obj[method];
} else if (method !== 'constructor') {//usual
methods[method] = obj[method];
}
}
comp.methods = methods;
comp.computed = computed;
//console.log(comp);
return defineComponent(comp);
}

View File

@@ -1,18 +1,16 @@
import Vue from 'vue'; import { createApp } from 'vue';
import router from './router'; import router from './router';
import store from './store'; import store from './store';
import './quasar'; import q from './quasar';
import vueSanitize from 'vue-sanitize';
Vue.use(vueSanitize);
import App from './components/App.vue'; import App from './components/App.vue';
//Vue.config.productionTip = false;
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
new Vue({ const app = createApp(App);
router,
store, app.use(router);
render: h => h(App), app.use(store);
}).$mount('#app'); app.use(q.quasar, q.options);
q.init();
app.mount('#app');

View File

@@ -1,8 +1,7 @@
import Vue from 'vue';
import 'quasar/dist/quasar.css'; import 'quasar/dist/quasar.css';
import Quasar from 'quasar/src/vue-plugin.js' //import Quasar from 'quasar/dist/quasar.umd.prod.js';
import Quasar from 'quasar/src/vue-plugin.js';
//config //config
const config = {}; const config = {};
@@ -33,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 = {
@@ -63,6 +64,7 @@ const components = {
QChip, QChip,
QTree, QTree,
//QExpansionItem, //QExpansionItem,
QVirtualScroll,
}; };
//directives //directives
@@ -80,16 +82,17 @@ const plugins = {
Notify, Notify,
}; };
//use
Vue.use(Quasar, { config, components, directives, plugins });
//icons //icons
//import '@quasar/extras/material-icons/material-icons.css';
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css'; //import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import '@quasar/extras/line-awesome/line-awesome.css'; import '@quasar/extras/line-awesome/line-awesome.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import lineAwesome from 'quasar/icon-set/line-awesome.js' import lineAwesome from 'quasar/icon-set/line-awesome.js'
Quasar.iconSet.set(lineAwesome);
export default {
quasar: Quasar,
options: { config, components, directives, plugins },
init: () => {
Quasar.iconSet.set(lineAwesome);
}
};

View File

@@ -1,5 +1,4 @@
import Vue from 'vue'; import { createRouter, createWebHashHistory } from 'vue-router';
import VueRouter from 'vue-router';
import _ from 'lodash'; import _ from 'lodash';
const CardIndex = () => import('./components/CardIndex/CardIndex.vue'); const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
@@ -36,7 +35,7 @@ const myRoutes = [
['/settings', Settings], ['/settings', Settings],
['/help', Help], ['/help', Help],
['/404', NotFound404], ['/404', NotFound404],
['*', null, null, '/cardindex'], ['/:pathMatch(.*)*', null, null, '/cardindex'],
]; ];
let routes = {}; let routes = {};
@@ -63,8 +62,7 @@ for (let route of myRoutes) {
} }
routes = routes.children; routes = routes.children;
Vue.use(VueRouter); export default createRouter({
history: createWebHashHistory(),
export default new VueRouter({
routes routes
}); });

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

@@ -45,6 +45,8 @@ export function formatDate(d, format) {
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
case 'coDate': case 'coDate':
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
case 'coMonth':
return `${(d.getMonth() + 1).toString().padStart(2, '0')}`;
case 'noDate': case 'noDate':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`; return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
} }
@@ -90,7 +92,7 @@ export function toBase58(data) {
} }
export function fromBase58(data) { export function fromBase58(data) {
return bs58.decode(data); return Buffer.from(bs58.decode(data));
} }
//base-x слишком тормозит, используем sjcl //base-x слишком тормозит, используем sjcl
@@ -107,6 +109,10 @@ export function fromBase64(data) {
)); ));
} }
export function hasProp(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
export function getObjDiff(oldObj, newObj, opts = {}) { export function getObjDiff(oldObj, newObj, opts = {}) {
const { const {
exclude = [], exclude = [],
@@ -126,7 +132,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
for (const key of Object.keys(oldObj)) { for (const key of Object.keys(oldObj)) {
const kp = `${keyPath}${key}`; const kp = `${keyPath}${key}`;
if (newObj.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(newObj, key)) {
if (ex.has(kp)) if (ex.has(kp))
continue; continue;
@@ -149,7 +155,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
if (exAdd.has(kp)) if (exAdd.has(kp))
continue; continue;
if (!oldObj.hasOwnProperty(key)) { if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
result.add[key] = _.cloneDeep(newObj[key]); result.add[key] = _.cloneDeep(newObj[key]);
} }
} }
@@ -213,7 +219,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
const change = diff.change; const change = diff.change;
for (const key of Object.keys(change)) { for (const key of Object.keys(change)) {
if (result.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(result, key)) {
if (_.isObject(change[key])) { if (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], opts); result[key] = applyObjDiff(result[key], change[key], opts);
} else { } else {
@@ -359,4 +365,54 @@ 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); });
}
export function makeDonation() {
window.open('https://donatty.com/liberama', '_blank');
}

View File

@@ -1,22 +1,22 @@
import Vue from 'vue'; import { createStore } from 'vuex';
import Vuex from 'vuex'; //import createPersistedState from 'vuex-persistedstate';
import createPersistedState from 'vuex-persistedstate'; import VuexPersistence from 'vuex-persist';
import root from './root.js'; import root from './root.js';
import uistate from './modules/uistate'; import uistate from './modules/uistate';
import config from './modules/config'; import config from './modules/config';
import reader from './modules/reader'; import reader from './modules/reader';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store(Object.assign({}, root, { const vuexLocal = new VuexPersistence();
export default createStore(Object.assign({}, root, {
modules: { modules: {
uistate, uistate,
config, config,
reader, reader,
}, },
strict: debug, strict: debug,
plugins: [createPersistedState()] plugins: [vuexLocal.plugin]
})); }));

View File

@@ -2,8 +2,10 @@ import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json'; import googleFonts from './fonts/fonts.json';
const readerActions = { const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки', 'loader': 'На страницу загрузки',
'loadFile': 'Загрузить файл с диска',
'loadBuffer': 'Загрузить из буфера обмена',
'help': 'Вызвать cправку',
'settings': 'Настроить', 'settings': 'Настроить',
'undoAction': 'Действие назад', 'undoAction': 'Действие назад',
'redoAction': 'Действие вперед', 'redoAction': 'Действие вперед',
@@ -15,10 +17,11 @@ const readerActions = {
'copyText': 'Скопировать текст со страницы', 'copyText': 'Скопировать текст со страницы',
'convOptions': 'Настроить конвертирование', 'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу', 'refresh': 'Принудительно обновить книгу',
'clickControl': 'Управление кликом',
'offlineMode': 'Автономный режим (без интернета)', 'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки', 'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека', 'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние', 'recentBooks': 'Показать загруженные',
'switchToolbar': 'Показать/скрыть панель управления', 'switchToolbar': 'Показать/скрыть панель управления',
'donate': '', 'donate': '',
'bookBegin': 'В начало книги', 'bookBegin': 'В начало книги',
@@ -35,6 +38,9 @@ const readerActions = {
//readerActions[name] //readerActions[name]
const toolButtons = [ const toolButtons = [
{name: 'loadFile', show: true},
{name: 'loadBuffer', show: true},
{name: 'help', show: true},
{name: 'undoAction', show: true}, {name: 'undoAction', show: true},
{name: 'redoAction', show: true}, {name: 'redoAction', show: true},
{name: 'fullScreen', show: true}, {name: 'fullScreen', show: true},
@@ -47,13 +53,16 @@ const toolButtons = [
{name: 'contents', show: true}, {name: 'contents', show: true},
{name: 'libs', show: true}, {name: 'libs', show: true},
{name: 'recentBooks', show: true}, {name: 'recentBooks', show: true},
{name: 'clickControl', show: false},
{name: 'offlineMode', show: false}, {name: 'offlineMode', show: false},
]; ];
//readerActions[name] //readerActions[name]
const hotKeys = [ const hotKeys = [
{name: 'help', codes: ['F1', 'H']},
{name: 'loader', codes: ['Escape']}, {name: 'loader', codes: ['Escape']},
{name: 'loadFile', codes: ['F3']},
{name: 'loadBuffer', codes: ['F4']},
{name: 'help', codes: ['F1', 'H']},
{name: 'settings', codes: ['S']}, {name: 'settings', codes: ['S']},
{name: 'undoAction', codes: ['Ctrl+BracketLeft']}, {name: 'undoAction', codes: ['Ctrl+BracketLeft']},
{name: 'redoAction', codes: ['Ctrl+BracketRight']}, {name: 'redoAction', codes: ['Ctrl+BracketRight']},
@@ -61,12 +70,13 @@ const hotKeys = [
{name: 'scrolling', codes: ['Z']}, {name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']}, {name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']}, {name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']}, {name: 'copyText', codes: ['Ctrl+Space']},
{name: 'convOptions', codes: ['Ctrl+M']}, {name: 'convOptions', codes: ['Ctrl+M']},
{name: 'refresh', codes: ['R']}, {name: 'refresh', codes: ['R']},
{name: 'contents', codes: ['C']}, {name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']}, {name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']}, {name: 'recentBooks', codes: ['X']},
{name: 'clickControl', codes: ['Ctrl+B']},
{name: 'offlineMode', codes: ['O']}, {name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']}, {name: 'switchToolbar', codes: ['Tab', 'Q']},
@@ -170,13 +180,28 @@ const settingDefaults = {
showServerStorageMessages: true, showServerStorageMessages: true,
showWhatsNewDialog: true, showWhatsNewDialog: true,
showDonationDialog2020: true, showDonationDialog: true,
showNeedUpdateNotify: true, showNeedUpdateNotify: true,
fontShifts: {}, fontShifts: {},
showToolButton: {}, showToolButton: {},
toolBarHideOnScroll: false,
userHotKeys: {}, userHotKeys: {},
userWallpapers: [], userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
//Book Update Checker
bucEnabled: true, // общее включение/выключение проверки обновлений
bucSizeDiff: 1, // разница в размерах файла, при которой показывать наличие обновления
bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
//для SettingsPage
needUpdateSettingsView: 0,
}; };
for (const font of fonts) for (const font of fonts)
@@ -212,9 +237,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: 'Миры Фэнтези'},
]}, ]},
@@ -233,6 +255,7 @@ const libsDefaults = {
// initial state // initial state
const state = { const state = {
toolBarActive: true, toolBarActive: true,
offlineModeActive: false,
serverSyncEnabled: false, serverSyncEnabled: false,
serverStorageKey: '', serverStorageKey: '',
profiles: {}, profiles: {},
@@ -258,6 +281,9 @@ const mutations = {
setToolBarActive(state, value) { setToolBarActive(state, value) {
state.toolBarActive = value; state.toolBarActive = value;
}, },
setOfflineModeActive(state, value) {
state.offlineModeActive = value;
},
setServerSyncEnabled(state, value) { setServerSyncEnabled(state, value) {
state.serverSyncEnabled = value; state.serverSyncEnabled = value;
}, },

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
server {
listen 80;
server_name beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass $liberama;
}
location /ws {
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
root /home/beta.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}
server {
listen 80;
server_name beta.omnireader.ru;
return 301 https://$host$request_uri;
}

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,15 +27,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 / {
@@ -43,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)$ {
@@ -61,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;
@@ -70,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 / {
@@ -87,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)$ {
@@ -139,5 +161,6 @@ server {
location / { location / {
proxy_pass http://fantasy-worlds.org; proxy_pass http://fantasy-worlds.org;
proxy_hide_header x-frame-options;
} }
} }

View File

@@ -2,6 +2,7 @@
### git, clone ### git, clone
``` ```
cd ~
sudo apt install ssh git sudo apt install ssh git
git clone https://github.com/bookpauk/liberama git clone https://github.com/bookpauk/liberama
``` ```
@@ -9,7 +10,7 @@ git clone https://github.com/bookpauk/liberama
### node.js ### node.js
``` ```
sudo apt install -y curl sudo apt install -y curl
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs sudo apt install -y nodejs
``` ```
@@ -25,7 +26,8 @@ sudo mkdir /home/liberama
sudo chown www-data.www-data /home/liberama sudo chown www-data.www-data /home/liberama
``` ```
### external converter `calibre`, download from https://download.calibre-ebook.com/ ### external converter `calibre`
#### download from https://download.calibre-ebook.com/
``` ```
wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz" wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre sudo -u www-data mkdir -p /home/liberama/data/calibre
@@ -34,20 +36,15 @@ sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/data/calibr
### external converters ### external converters
``` ```
sudo apt install rar sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graphicsmagick-imagemagick-compat
sudo apt install libreoffice
sudo apt install poppler-utils
sudo apt install djvulibre-bin
sudo apt install libtiff-tools
sudo apt install graphicsmagick-imagemagick-compat
``` ```
### nginx, server config ### nginx, server config
Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader. #### Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader и docs/omnireader.ru/omnireader_http
Можно также настроить сервер для HTTP, без SSL. Сначала настроим для HTTP:
``` ```
sudo apt install nginx sudo apt install nginx
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader sudo cp docs/omnireader.ru/omnireader_http /etc/nginx/sites-available/omnireader
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
sudo rm /etc/nginx/sites-enabled/default sudo rm /etc/nginx/sites-enabled/default
sudo service nginx reload sudo service nginx reload
@@ -55,8 +52,16 @@ sudo chown -R www-data.www-data /var/www
``` ```
### certbot ### certbot
Следовать инструкции установки certbot https://certbot.eff.org/lets-encrypt/ubuntubionic-nginx #### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20
После установки сертификата, можно использовать конфиг для nginx c ssl:
```
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
sudo service nginx reload
```
### old.omnireader ### old.omnireader
#### Старая версия omnireader на базе PHP, можно не устанавливать
``` ```
sudo apt install php7.4 php7.4-curl php7.4-mbstring php7.4-fpm sudo apt install php7.4 php7.4-curl php7.4-mbstring php7.4-fpm
sudo service php7.4-fpm restart sudo service php7.4-fpm restart
@@ -68,7 +73,7 @@ sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
## Запуск по крону ## Запуск по крону
``` ```
* * * * * /root/liberama/docs/omnireader.ru/cron_server.sh * * * * * ~/liberama/docs/omnireader.ru/cron_server.sh >>~/liberama_cron.log 2>&1
``` ```
## Деплой и запуск ## Деплой и запуск
@@ -78,8 +83,8 @@ cd docs/omnireader.ru
./deploy.sh ./deploy.sh
./start_server.sh ./start_server.sh
``` ```
После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`. После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
Необходимо переключить приложение в режим `omnireader`, отредактировав опцию `servers`: Необходимо переключить приложение в режим `omnireader`, отредактировав опцию `servers`:
``` ```
"servers": [ "servers": [
@@ -91,4 +96,8 @@ cd docs/omnireader.ru
} }
] ]
``` ```
и перезапустить сервер Для использования установленных внешних конвертеров можно также поправить:
```
"useExternalBookConverter": true,
```
и перезапустить сервер.

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,15 +16,20 @@ server {
gzip_proxied expired no-cache no-store private auth; gzip_proxied expired no-cache no-store private auth;
gzip_types *; gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api { location /api {
proxy_pass http://127.0.0.1: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";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -32,6 +38,11 @@ server {
location /tmp { location /tmp {
types { } default_type "application/xml; charset=utf-8"; types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip; add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
} }
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
@@ -51,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

27498
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.10.2", "version": "0.12.2",
"author": "Book Pauk <bookpauk@gmail.com>", "author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0", "license": "CC0-1.0",
"repository": "bookpauk/liberama", "repository": "bookpauk/liberama",
"engines": { "engines": {
"node": ">=14.4.0" "node": ">=16.16.0"
}, },
"scripts": { "scripts": {
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'", "dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
"build:client": "webpack --config build/webpack.prod.config.js", "build:client": "webpack --config build/webpack.prod.config.js",
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -o dist/linux/liberama .", "build:linux": "npm run build:client && node build/linux && pkg -t node16-linux-x64 -C GZip -o dist/linux/liberama .",
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -o dist/win/liberama .", "build:win": "npm run build:client && node build/win && pkg -t node16-win-x64 -C GZip -o dist/win/liberama .",
"lint": "eslint --ext=.js,.vue client server", "lint": "eslint --ext=.js,.vue client server",
"build:client-dev": "webpack --config build/webpack.dev.config.js", "build:client-dev": "webpack --config build/webpack.dev.config.js",
"postinstall": "npm run build:client-dev && node build/linux" "postinstall": "npm run build:client-dev && node build/linux"
@@ -21,73 +21,64 @@
"scripts": "server/config/*.js" "scripts": "server/config/*.js"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.22.1", "@babel/core": "^7.18.13",
"babel-eslint": "^10.1.0", "@babel/eslint-parser": "^7.18.9",
"babel-loader": "^7.1.1", "@babel/eslint-plugin": "^7.18.10",
"babel-plugin-component": "^1.1.1", "@babel/plugin-proposal-decorators": "^7.18.10",
"babel-plugin-syntax-dynamic-import": "^6.18.0", "@babel/preset-env": "^7.18.10",
"babel-plugin-transform-class-properties": "^6.24.1", "@vue/compiler-sfc": "^3.2.22",
"babel-plugin-transform-decorators-legacy": "^1.3.5", "babel-loader": "^8.2.5",
"babel-preset-env": "^1.3.2", "copy-webpack-plugin": "^11.0.0",
"clean-webpack-plugin": "^1.0.1", "css-loader": "^6.7.1",
"copy-webpack-plugin": "^5.1.2", "css-minimizer-webpack-plugin": "^4.0.0",
"css-loader": "^1.0.0", "eslint": "^8.23.0",
"eslint": "^5.16.0", "eslint-plugin-vue": "^9.4.0",
"eslint-plugin-html": "^5.0.5", "html-webpack-plugin": "^5.5.0",
"eslint-plugin-node": "^8.0.0", "mini-css-extract-plugin": "^2.6.1",
"eslint-plugin-vue": "^5.2.3", "pkg": "^5.8.0",
"file-loader": "^3.0.1", "terser-webpack-plugin": "^5.3.6",
"html-webpack-plugin": "^3.2.0", "vue-eslint-parser": "^9.0.3",
"mini-css-extract-plugin": "^0.5.0", "vue-loader": "^17.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.8",
"terser-webpack-plugin": "^1.4.5",
"url-loader": "^1.1.2",
"vue-class-component": "^6.3.2",
"vue-loader": "^15.9.8",
"vue-style-loader": "^4.1.3", "vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.14", "webpack": "^5.74.0",
"webpack": "^4.46.0", "webpack-cli": "^4.10.0",
"webpack-cli": "^3.3.12", "webpack-dev-middleware": "^5.3.3",
"webpack-dev-middleware": "^3.7.3", "webpack-hot-middleware": "^2.25.2",
"webpack-hot-middleware": "^2.25.1", "webpack-merge": "^5.8.0",
"webpack-merge": "^4.2.2", "workbox-webpack-plugin": "^6.5.4"
"workbox-webpack-plugin": "^5.1.4"
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.11.2", "@quasar/extras": "^1.15.2",
"axios": "^0.18.1", "@vue/compat": "^3.2.38",
"base-x": "^3.0.8", "axios": "^0.27.2",
"chardet": "^0.7.0", "base-x": "^4.0.0",
"chardet": "^1.4.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.17.1", "express": "^4.18.1",
"fg-loadcss": "^2.1.0", "fg-loadcss": "^3.1.0",
"fs-extra": "^7.0.1", "fs-extra": "^10.1.0",
"got": "^9.6.0",
"he": "^1.2.0", "he": "^1.2.0",
"iconv-lite": "^0.4.24", "iconv-lite": "^0.6.3",
"jembadb": "^4.2.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimist": "^1.2.5", "minimist": "^1.2.6",
"multer": "^1.4.3", "multer": "^1.4.5-lts.1",
"pako": "^1.0.11", "pako": "^2.0.4",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"pidusage": "^2.0.21", "pidusage": "^3.0.0",
"pkg": "^4.4.9", "quasar": "^2.7.7",
"quasar": "^1.16.0",
"safe-buffer": "^5.2.1", "safe-buffer": "^5.2.1",
"sanitize-html": "^2.7.1",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
"sql-template-strings": "^2.2.2",
"sqlite": "^4.0.23",
"sqlite3": "^5.0.2",
"tar-fs": "^2.1.1", "tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3", "unbzip2-stream": "^1.4.3",
"vue": "^2.6.14", "vue": "^3.2.37",
"vue-router": "^3.5.2", "vue-router": "^4.1.5",
"vue-sanitize": "^0.2.1", "vuex": "^4.0.2",
"vuex": "^3.6.2", "vuex-persist": "^3.1.3",
"vuex-persistedstate": "^2.7.1", "webdav": "^4.11.0",
"webdav": "^2.10.2", "ws": "^8.8.1",
"ws": "^7.5.5", "zip-stream": "^4.1.0"
"zip-stream": "^2.1.3"
} }
} }

View File

@@ -22,38 +22,62 @@ module.exports = {
maxUploadPublicDirSize: 200*1024*1024,//100Мб maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false, useExternalBookConverter: false,
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'], acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png',
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch'],
db: [ jembaDb: [
{ {
poolName: 'app', serverMode: ['reader', 'omnireader', 'liberama.top'],
connCount: 20, dbName: 'app',
fileName: 'app.sqlite', thread: true,
openAll: true,
}, },
{ {
poolName: 'readerStorage', serverMode: ['reader', 'omnireader', 'liberama.top'],
connCount: 20, dbName: 'reader-storage',
fileName: 'reader-storage.sqlite', thread: true,
} openAll: true,
},
{
serverMode: 'book_update_checker',
dbName: 'book-update-server',
thread: true,
openAll: true,
},
], ],
servers: [ servers: [
{ {
serverName: '1', serverName: '1',
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader' 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',
}, },
/*{
serverName: '2',
mode: 'book_update_checker', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
isHttps: true,
keysFile: 'server',
ip: '0.0.0.0',
port: '33443',
accessToken: '',
}*/
], ],
remoteWebDavStorage: false, remoteStorage: false,
/* /*
remoteWebDavStorage: { remoteStorage: {
url: '127.0.0.1:1900', url: 'wss://127.0.0.1:11900',
username: '', accessToken: '',
password: '',
}, },
*/ */
bucEnabled: false,
bucServer: false,
/*
bucServer: {
url: 'wss://127.0.0.1:33443',
accessToken: '',
}
*/
}; };

View File

@@ -10,7 +10,9 @@ const propsToSave = [
'useExternalBookConverter', 'useExternalBookConverter',
'servers', 'servers',
'remoteWebDavStorage', 'remoteStorage',
'bucEnabled',
'bucServer',
]; ];
let instance = null; let instance = null;

View File

@@ -0,0 +1,126 @@
const WebSocket = require('ws');
//const _ = require('lodash');
const BUCServer = require('../core/BookUpdateChecker/BUCServer');
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.accessToken = config.accessToken;
this.bucServer = new BUCServer(config);
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(`BUC-WebSocket-IN: ${message.substr(0, 4000)}`);
}
req = JSON.parse(message);
ws.lastActivity = Date.now();
//pong for WebSocketConnection
this.send({_rok: 1}, req, ws);
if (req.accessToken !== this.accessToken)
throw new Error('Access denied');
switch (req.action) {
case 'test':
await this.test(req, ws); break;
case 'get-buc':
await this.getBuc(req, ws); break;
case 'update-buc':
await this.updateBuc(req, ws); break;
default:
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(`BUC-WebSocket-OUT: ${message.substr(0, 4000)}`);
}
}
}
//Actions ------------------------------------------------------------------
async test(req, ws) {
this.send({message: 'Liberama project is awesome'}, req, ws);
}
async getBuc(req, ws) {
if (!req.fromCheckTime)
throw new Error(`key 'fromCheckTime' is empty`);
await this.bucServer.getBuc(req.fromCheckTime, (rows) => {
this.send({state: 'get', rows}, req, ws);
});
this.send({state: 'finish'}, req, ws);
}
async updateBuc(req, ws) {
if (!req.bookUrls)
throw new Error(`key 'bookUrls' is empty`);
if (!Array.isArray(req.bookUrls))
throw new Error(`key 'bookUrls' must be array`);
await this.bucServer.updateBuc(req.bookUrls);
this.send({state: 'success'}, req, ws);
}
}
module.exports = BookUpdateCheckerController;

View File

@@ -1,12 +1,12 @@
const BaseController = require('./BaseController'); const BaseController = require('./BaseController');
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
const WorkerState = require('../core/WorkerState');//singleton const WorkerState = require('../core/WorkerState');//singleton
class ReaderController extends BaseController { class ReaderController extends BaseController {
constructor(config) { constructor(config) {
super(config); super(config);
this.readerStorage = new ReaderStorage(); this.readerStorage = new JembaReaderStorage();
this.readerWorker = new ReaderWorker(config); this.readerWorker = new ReaderWorker(config);
this.workerState = new WorkerState(); this.workerState = new WorkerState();
} }
@@ -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

@@ -2,8 +2,9 @@ const WebSocket = require ('ws');
const _ = require('lodash'); const _ = require('lodash');
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
const WorkerState = require('../core/WorkerState');//singleton const WorkerState = require('../core/WorkerState');//singleton
const BUCClient = require('../core/BookUpdateChecker/BUCClient');//singleton
const log = new (require('../core/AppLogger'))().log;//singleton const log = new (require('../core/AppLogger'))().log;//singleton
const utils = require('../core/utils'); const utils = require('../core/utils');
@@ -15,15 +16,23 @@ class WebSocketController {
this.config = config; this.config = config;
this.isDevelopment = (config.branch == 'development'); this.isDevelopment = (config.branch == 'development');
this.readerStorage = new ReaderStorage(); this.readerStorage = new JembaReaderStorage();
this.readerWorker = new ReaderWorker(config); this.readerWorker = new ReaderWorker(config);
this.workerState = new WorkerState(); this.workerState = new WorkerState();
if (config.bucEnabled) {
this.bucClient = new BUCClient(config);
}
this.wss = wss; this.wss = wss;
wss.on('connection', (ws) => { wss.on('connection', (ws) => {
ws.on('message', (message) => { ws.on('message', (message) => {
this.onMessage(ws, message); this.onMessage(ws, message.toString());
});
ws.on('error', (err) => {
log(LM_ERR, err);
}); });
}); });
@@ -55,8 +64,7 @@ class WebSocketController {
ws.lastActivity = Date.now(); ws.lastActivity = Date.now();
//pong for WebSocketConnection //pong for WebSocketConnection
if (req._rpo === 1) this.send({_rok: 1}, req, ws);
this.send({_rok: 1}, req, ws);
switch (req.action) { switch (req.action) {
case 'test': case 'test':
@@ -67,10 +75,14 @@ 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;
case 'check-buc':
await this.checkBuc(req, ws); break;
default: default:
throw new Error(`Action not found: ${req.action}`); throw new Error(`Action not found: ${req.action}`);
@@ -150,15 +162,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`);
@@ -169,6 +172,35 @@ 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);
}
async checkBuc(req, ws) {
if (!this.config.bucEnabled)
throw new Error('BookUpdateChecker disabled');
if (!req.bookUrls)
throw new Error(`key 'bookUrls' is empty`);
if (!Array.isArray(req.bookUrls))
throw new Error(`key 'bookUrls' must be array`);
const data = await this.bucClient.checkBuc(req.bookUrls);
this.send({state: 'success', data}, req, ws);
}
} }
module.exports = WebSocketController; module.exports = WebSocketController;

View File

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

View File

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

View File

@@ -7,10 +7,14 @@ let instance = null;
class AppLogger { class AppLogger {
constructor() { constructor() {
if (!instance) { if (!instance) {
this.inited = false;
this.logFileName = '';
this.errLogFileName = '';
this.fatalLogFileName = '';
instance = this; instance = this;
} }
this.inited = false;
return instance; return instance;
} }
@@ -22,11 +26,16 @@ class AppLogger {
if (config.loggingEnabled) { if (config.loggingEnabled) {
await fs.ensureDir(config.logDir); await fs.ensureDir(config.logDir);
this.logFileName = `${config.logDir}/${config.name}.log`;
this.errLogFileName = `${config.logDir}/${config.name}.err.log`;
this.fatalLogFileName = `${config.logDir}/${config.name}.fatal.log`;
loggerParams = [ loggerParams = [
{log: 'ConsoleLog'}, {log: 'ConsoleLog'},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`}, {log: 'FileLog', fileName: this.logFileName},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO, LM_TOTAL]}, {log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.fatal.log`, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only {log: 'FileLog', fileName: this.fatalLogFileName, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
]; ];
} }

Some files were not shown because too many files have changed in this diff Show More