Compare commits

...

209 Commits

Author SHA1 Message Date
Book Pauk
4242a8679f Merge branch 'release/0.9.6-1' 2020-11-07 20:56:57 +07:00
Book Pauk
4ff9ff699b Добавил ссылку на flibs.in в предустановленные закладки 2020-11-07 20:56:07 +07:00
Book Pauk
7a76673274 Merge tag '0.9.6' into develop
0.9.6
2020-11-06 18:00:31 +07:00
Book Pauk
bd03ca5136 Merge branch 'release/0.9.6' 2020-11-06 18:00:22 +07:00
Book Pauk
b3e1e4b909 Версия 0.9.6 2020-11-06 17:59:54 +07:00
Book Pauk
4bb22183df Добавлен диалог с сообщением о новой читалке 2020-11-06 17:57:16 +07:00
Book Pauk
e72f8f4245 Мелкий рефакторинг 2020-11-06 17:36:05 +07:00
Book Pauk
5b52f48bce Поправка для автоматического отображения справки 2020-11-06 16:50:17 +07:00
Book Pauk
f07a157a2a Поправка бага при resize 2020-11-06 01:08:22 +07:00
Book Pauk
ca40854106 Рефакторинг, версия 0.9.6 2020-11-05 22:21:52 +07:00
Book Pauk
d6a8209b31 Работа над ExternalLibs 2020-11-05 21:52:34 +07:00
Book Pauk
731e1f1f15 Мелкая поправка 2020-11-05 20:18:23 +07:00
Book Pauk
b4a2a8fb98 Merge tag '0.9.5-2' into develop
0.9.5-2
2020-11-05 16:58:53 +07:00
Book Pauk
5a3e4ee5ca Merge branch 'release/0.9.5-2' 2020-11-05 16:58:37 +07:00
Book Pauk
ab2cf0aeec Работа над ExternalLibs 2020-11-05 16:56:27 +07:00
Book Pauk
9de6a02b30 Поправка безопасности 2020-11-05 16:42:00 +07:00
Book Pauk
9fb7892bfe Работа над ExternalLibs 2020-11-05 16:40:33 +07:00
Book Pauk
546f4556f6 Мелкий рефакторинг 2020-11-05 15:43:15 +07:00
Book Pauk
471de104bc Работа над ExternalLibs 2020-11-05 15:36:58 +07:00
Book Pauk
d30be1536d Работа над BookmarkSettings 2020-11-05 15:06:36 +07:00
Book Pauk
6c0678ed61 Рефакторинг 2020-11-05 14:08:15 +07:00
Book Pauk
4883b8a190 Работа над ExternalLibs 2020-11-05 03:21:54 +07:00
Book Pauk
14742ed4ad Работа над BookmarkSettings 2020-11-04 22:56:28 +07:00
Book Pauk
1d8bd56862 Поправки багов 2020-11-04 22:23:39 +07:00
Book Pauk
94b8f9fe1c Работа над BookmarkSettings 2020-11-04 19:06:11 +07:00
Book Pauk
27412211a5 Работа над BookmarkSettings 2020-11-04 18:53:14 +07:00
Book Pauk
f8c4960079 Работа над BookmarkSettings 2020-11-04 14:43:06 +07:00
Book Pauk
b2e0bcf995 Улучшение безопасности отображения списка 2020-11-04 14:42:35 +07:00
Book Pauk
fcf6639d38 Небольшие поправки обработки клавиш 2020-11-04 14:41:38 +07:00
Book Pauk
d540cb91a9 Добавлен пакет vue-sanitize 2020-11-04 14:41:11 +07:00
Book Pauk
f69cc6f1b1 Работа над BookmarkSettings 2020-11-03 23:04:06 +07:00
Book Pauk
607f2ff407 Работа над BookmarkSettings 2020-11-03 22:24:32 +07:00
Book Pauk
ba6bf8c091 Работа над BookmarkSettings 2020-11-03 17:03:57 +07:00
Book Pauk
7e4c938dfd Поправлен небольшой баг отображения окна 2020-11-03 14:42:02 +07:00
Book Pauk
7f36d55320 Мелкая поправка 2020-11-03 14:41:32 +07:00
Book Pauk
d9634a134c Обновление caniuse -lite 2020-11-03 00:35:44 +07:00
Book Pauk
4f8868d4b1 Merge tag '0.9.5-1' into develop
0.9.5-1
2020-11-03 00:29:22 +07:00
Book Pauk
956546585c Merge branch 'release/0.9.5-1' 2020-11-03 00:29:12 +07:00
Book Pauk
3ca0a92442 Временно отключил кнопку "Настроить закладки" 2020-11-03 00:28:15 +07:00
Book Pauk
213f7e48c9 Работа над BookmarkSettings 2020-11-03 00:26:25 +07:00
Book Pauk
8b66fd522d Добавлено восстановление дефолтных настроек в объекте settings при их отсутствии 2020-11-03 00:00:31 +07:00
Book Pauk
fdf5009999 Мелкая поправка 2020-11-02 23:51:41 +07:00
Book Pauk
bbdba0ef16 Поправки по результату тестирования 2020-11-02 23:45:59 +07:00
Book Pauk
a63602df7a Поправки багов 2020-11-02 14:33:15 +07:00
Book Pauk
587120f984 Merge tag '0.9.5' into develop
0.9.5
2020-11-01 19:55:53 +07:00
Book Pauk
e72ca0de7e Merge branch 'release/0.9.5' 2020-11-01 19:55:45 +07:00
Book Pauk
c44c27d3d2 Версия 0.9.5 2020-11-01 19:36:13 +07:00
Book Pauk
df4e201ccd Небольшие поправки 2020-11-01 19:33:02 +07:00
Book Pauk
c8c0e9ec1a Добавлена кнопка 'Обновить с разбиением на параграфы' 2020-11-01 16:42:20 +07:00
Book Pauk
9a4a84a367 Сертификаты для beta.liberama 2020-11-01 15:00:23 +07:00
Book Pauk
1dc3424411 Убрал лишнее 2020-11-01 14:53:51 +07:00
Book Pauk
c13745e913 Добавлен конфиг nginx для beta.liberama 2020-11-01 14:50:18 +07:00
Book Pauk
25c12309f2 Поправки поведения диалога на "Enter" 2020-11-01 14:41:40 +07:00
Book Pauk
4b632da5af Поправки поведения Select 2020-11-01 14:17:35 +07:00
Book Pauk
87c364b8ee Исправление поведения компонента select 2020-11-01 13:38:05 +07:00
Book Pauk
efa48fbc8a Поправки margin окна 2020-11-01 11:42:54 +07:00
Book Pauk
21df6c1d21 Поправка goToLink 2020-11-01 11:34:51 +07:00
Book Pauk
39d2ceb94b Мелкая поправка для fullscreen 2020-11-01 11:26:58 +07:00
Book Pauk
1dad013d60 Исправлен баг синхронизации при первом включении опции 2020-11-01 11:23:15 +07:00
Book Pauk
add7a03f88 Убираем ссылку на гитхаб для liberama.top 2020-10-31 21:20:30 +07:00
Book Pauk
0cefaa6d48 Мелкая поправка 2020-10-30 19:16:06 +07:00
Book Pauk
f08e73f359 Merge tag '0.9.4' into develop
Версия 0.9.4
2020-10-30 19:13:17 +07:00
Book Pauk
cf9ce26438 Merge branch 'release/0.9.4' 2020-10-30 19:13:00 +07:00
Book Pauk
fd74a5a82e 0.9.4 2020-10-30 19:12:49 +07:00
Book Pauk
3109104928 Замена event.code на event.key 2020-10-30 19:07:33 +07:00
Book Pauk
b1ec4df2e4 Вернул обратно хук на keydown 2020-10-30 18:15:12 +07:00
Book Pauk
1609e149a8 Поправлен конфиг 2020-10-30 17:01:58 +07:00
Book Pauk
298c483d0e Исправления скриптов запуска сервера для крона 2020-10-30 16:53:54 +07:00
Book Pauk
dc917b75b1 Убрал дебаг 2020-10-30 16:11:02 +07:00
Book Pauk
ad32bdab44 Исправление проблемы нескольких одновременных попыток подключения по WebSocket 2020-10-30 16:02:09 +07:00
Book Pauk
5e8b2e1c87 Отладка 2020-10-30 12:22:54 +07:00
Book Pauk
08b4afd287 Libs добавлены в синхронизацию 2020-10-30 12:01:22 +07:00
Book Pauk
0de31f643b Мелкая поправка 2020-10-30 11:35:41 +07:00
Book Pauk
5e815eb3c4 Мелкий рефакторинг 2020-10-30 11:30:21 +07:00
Book Pauk
48c93a2120 Небольшие поправки 2020-10-30 11:23:18 +07:00
Book Pauk
ad5de42172 Небольшие поправки 2020-10-30 11:13:09 +07:00
Book Pauk
32bafedaad Небольшие доработки 2020-10-30 00:10:42 +07:00
Book Pauk
c67fd11be9 Небольшие доделки 2020-10-30 00:00:05 +07:00
Book Pauk
8b59c72848 Небольшие доработки 2020-10-29 23:39:13 +07:00
Book Pauk
c35b2f3bfc Работа над LibsPage 2020-10-29 23:29:10 +07:00
Book Pauk
ac63ad4612 Версия 0.9.4 2020-10-29 23:28:42 +07:00
Book Pauk
e1dea1c752 Версия 0.9.4 2020-10-29 23:28:17 +07:00
Book Pauk
25648e2327 Работа над LibsPage 2020-10-29 23:05:26 +07:00
Book Pauk
18ac04bb0f Работа над ExternalLibs 2020-10-29 22:54:02 +07:00
Book Pauk
5263ee58b2 Работа над ExternalLibs 2020-10-29 21:09:23 +07:00
Book Pauk
af542b89f7 Работа над LibsPage 2020-10-29 16:31:09 +07:00
Book Pauk
684b675fca Поправка конфига 2020-10-29 15:40:32 +07:00
Book Pauk
c29044eca1 Работа над LibsPage 2020-10-29 15:35:09 +07:00
Book Pauk
a36510fcc8 Мелкая поправка 2020-10-29 15:27:28 +07:00
Book Pauk
bc21ace416 Работа над LibsPage 2020-10-29 15:14:27 +07:00
Book Pauk
57e521e2ff Работа над LibsPage 2020-10-29 14:45:10 +07:00
Book Pauk
ac6ebb9e8d Настройки перенаправления 2020-10-28 23:41:15 +07:00
Book Pauk
54bef54635 Поправки конфига nginx 2020-10-28 22:47:30 +07:00
Book Pauk
593e201f79 Работа над LibsPage (ExternalLibs) 2020-10-28 22:45:05 +07:00
Book Pauk
d7b24253fe Работа над LibsPage 2020-10-28 22:04:47 +07:00
Book Pauk
33961abd86 Поправлен баг редиректа 2020-10-28 21:06:07 +07:00
Book Pauk
37e0e1d42f Добавки для правильной работы с nginx 2020-10-28 20:42:47 +07:00
Book Pauk
1121f9c918 Работа над LibsPage 2020-10-28 20:30:57 +07:00
Book Pauk
582203f5da Работа над LibsPage (ExternalLibs) 2020-10-28 20:14:42 +07:00
Book Pauk
8c0f193738 Исправлен баг редиректа 2020-10-28 18:28:28 +07:00
Book Pauk
ebe42956ad Все компоненты загружаются динамически 2020-10-28 18:27:24 +07:00
Book Pauk
b8f8df8927 Поправил настройки nginx 2020-10-28 11:19:31 +07:00
Book Pauk
2c66ca4fdd Поправки конфига nginx 2020-10-27 23:46:44 +07:00
Book Pauk
49f813e880 Работа над LibsPage 2020-10-27 23:10:45 +07:00
Book Pauk
da6fed80d1 Работа над LibsPage 2020-10-27 23:02:28 +07:00
Book Pauk
b901d9b8c9 Убрал эксперимент 2020-10-27 17:45:08 +07:00
Book Pauk
b41d46ac57 Эксперимент 2020-10-27 17:40:56 +07:00
Book Pauk
4f0189f3e0 Эксперимент 2020-10-27 16:24:59 +07:00
Book Pauk
c956e7a802 Убрал дебаг 2020-10-27 14:34:12 +07:00
Book Pauk
dcbc8409e0 Работа над LibsPage 2020-10-27 14:31:07 +07:00
Book Pauk
fd58568cf0 Работа над LibsPage 2020-10-27 14:16:30 +07:00
Book Pauk
0f81fa53d2 Работа над LibsPage 2020-10-27 13:30:05 +07:00
Book Pauk
44655dc81c Quasar update 2020-10-27 13:28:54 +07:00
Book Pauk
749667aefd Работа над LibsPage 2020-10-27 11:17:49 +07:00
Book Pauk
dd94418c26 Процесс добавления нового окна: "Библиотеки" 2020-10-26 00:23:30 +07:00
Book Pauk
55a5375e46 Скрываем ссылки "Отзывы о читалке" и "Старая версия" в режиме "liberama.top" 2020-10-25 23:36:02 +07:00
Book Pauk
df76de7352 Поправки для нового сайта liberama.top 2020-10-25 18:35:42 +07:00
Book Pauk
1fb1a1b2b1 Временная настройка liberama.top на beta-среду 2020-10-25 18:08:04 +07:00
Book Pauk
f998edb2aa Добавлены сертификаты для ssl 2020-10-25 17:46:14 +07:00
Book Pauk
7c2cb9a0c7 Поправка дока в связи с переименованиями 2020-10-25 17:39:26 +07:00
Book Pauk
0690a365da Добавлена настройка прокси для flibusta.is 2020-10-25 17:35:53 +07:00
Book Pauk
a20d05aba8 Переименования в доках, добавлены настройки nginx для сайта liberama.top 2020-10-25 17:30:34 +07:00
Book Pauk
4362ae95ba Поправки по результату тестирования 2020-10-24 17:25:36 +07:00
Book Pauk
d658814399 Поправки по результатам тестирования 2020-10-24 13:11:17 +07:00
Book Pauk
39e14d70ee Исправление бага распаковки Zip-архивов с плохими именами файлов 2020-10-13 16:24:35 +07:00
Book Pauk
2e58cfdb75 Исправлен мелкий баг PageDown 2020-09-18 18:05:32 +07:00
Book Pauk
fcaa724c00 Убрал ya-share 2020-09-18 17:45:27 +07:00
Book Pauk
8806b4141e Поправки истории версий 2020-09-18 17:43:05 +07:00
Book Pauk
7bd159766b Исправление бага исчезновения картинок при включенной настройке "Убирать пустые строки" 2020-09-18 17:40:37 +07:00
Book Pauk
4df15d603f Merge tag '0.9.3-2' into develop
0.9.3-2
2020-06-30 16:54:31 +07:00
Book Pauk
b453c3efe5 Merge branch 'release/0.9.3-2' 2020-06-30 16:54:25 +07:00
Book Pauk
56590ef8a4 Исправление бага при определении пустых параграфов 2020-06-30 16:51:43 +07:00
Book Pauk
7c133136b9 Merge tag '0.9.3-1' into develop
0.9.3-1
2020-06-04 19:13:23 +07:00
Book Pauk
41881639aa Merge branch 'release/0.9.3-1' 2020-06-04 19:13:15 +07:00
Book Pauk
416003f078 Поправки сигнатур файлов 2020-06-04 19:12:05 +07:00
Book Pauk
bbfcd0efa3 Merge tag '0.9.3' into develop
0.9.3
2020-05-21 05:04:37 +07:00
Book Pauk
150e4332c3 Merge branch 'release/0.9.3' 2020-05-21 05:04:17 +07:00
Book Pauk
49649765c7 Версия 0.9.3 2020-05-21 05:03:08 +07:00
Book Pauk
726b7bfa93 Вернул обратно 2020-05-21 05:02:10 +07:00
Book Pauk
265f838868 Эксперимент 2020-05-21 04:58:32 +07:00
Book Pauk
6e2e5b5520 Вернул обратно 2020-05-21 04:54:39 +07:00
Book Pauk
100ea2f64a Эксперимент 2020-05-21 04:50:16 +07:00
Book Pauk
4e7ed1ee33 Поправки Content-Type 2020-05-21 04:36:03 +07:00
Book Pauk
8ab6aed1aa Поправлен баг 2020-05-21 04:04:57 +07:00
Book Pauk
4ff096014c Эксперимент 2020-05-21 02:56:44 +07:00
Book Pauk
03b60b6ca9 Эксперимент 2020-05-21 02:24:53 +07:00
Book Pauk
e30b832e05 Эксперимент 2020-05-21 02:06:17 +07:00
Book Pauk
e646de85a7 Поправка настроек SW 2020-05-21 01:51:47 +07:00
Book Pauk
70a7a0e344 Замена sw-precache-webpack-plugin на workbox-webpack-plugin 2020-05-21 01:31:39 +07:00
Book Pauk
b444abeb3e Исправлен баг перехвата клавиш в диалогах 2020-05-21 00:14:47 +07:00
Book Pauk
c72f56917d Quasar upgrade 2020-05-21 00:06:20 +07:00
Book Pauk
192283d6b2 Добавил gitignore 2020-05-20 23:13:08 +07:00
Book Pauk
6be6fa1966 Merge tag '0.9.2-2' into develop
0.9.2-2
2020-05-01 14:33:54 +07:00
Book Pauk
510553b055 Merge branch 'release/0.9.2-2' 2020-05-01 14:33:45 +07:00
Book Pauk
6c4616892e Поправлен баг распознавания html 2020-05-01 14:32:31 +07:00
Book Pauk
1e79a099b8 Merge tag '0.9.2-1' into develop
0.9.2-1
2020-04-15 16:37:17 +07:00
Book Pauk
31a22327f1 Merge branch 'release/0.9.2-1' 2020-04-15 16:37:10 +07:00
Book Pauk
c1712bebc6 0.9.2-1 2020-04-15 16:36:29 +07:00
Book Pauk
cd91541245 Исправлен баг "Не удалось определить формат файла" при загрузке html-страниц 2020-04-15 16:35:05 +07:00
Book Pauk
4c1fc83256 Merge tag '0.9.2' into develop
0.9.2
2020-04-15 15:37:59 +07:00
Book Pauk
34c7a33576 Merge branch 'release/0.9.2' 2020-04-15 15:37:21 +07:00
Book Pauk
23ecfeeb4f Версия 0.9.2 2020-04-15 15:36:42 +07:00
Book Pauk
9703f83eb3 Мелкая поправка 2020-04-15 15:32:32 +07:00
Book Pauk
0f3cc03d00 Мелкие поправки 2020-03-19 19:44:06 +07:00
Book Pauk
6f7ba1f9fc Окончание работы над хоткеями 2020-03-19 19:38:40 +07:00
Book Pauk
e1b85e4a1b Рефакторинг 2020-03-19 19:09:39 +07:00
Book Pauk
b308dd58cc Рефакторинг 2020-03-19 17:12:12 +07:00
Book Pauk
9f4c0479ce Рефакторинг 2020-03-19 17:05:29 +07:00
Book Pauk
2c57817dde Рефакторинг 2020-03-19 16:38:31 +07:00
Book Pauk
ba85c54d7c Рефакторинг 2020-03-19 15:57:56 +07:00
Book Pauk
a80e5c3a65 Поправки текста 2020-03-19 15:46:32 +07:00
Book Pauk
22e2c34da8 Работа над хоткеями 2020-03-18 20:20:06 +07:00
Book Pauk
00a8e4c2c5 Работа над хоткеями 2020-03-18 20:04:44 +07:00
Book Pauk
10d0a4079c Работа над хоткеями 2020-03-18 19:00:02 +07:00
Book Pauk
589f7f3c22 Работа над хоткеями 2020-03-18 18:20:57 +07:00
Book Pauk
d1126a7eb0 Работа над хоткеями, промежуточный коммит 2020-03-18 15:37:19 +07:00
Book Pauk
9f4e72a0e1 Работа над хоткеями, промежуточный коммит 2020-03-18 15:06:29 +07:00
Book Pauk
a024295379 Мелкий рефакторинг 2020-03-15 21:48:53 +07:00
Book Pauk
dc2b2ec488 Мелкая доработка 2020-03-15 21:46:35 +07:00
Book Pauk
0c5f5975aa Работа над хоткеями 2020-03-15 21:44:26 +07:00
Book Pauk
dc3f682d2d Работа над хоткеями 2020-03-15 21:29:58 +07:00
Book Pauk
2db8876c66 Рефакторинг, работа над хоткеями 2020-03-15 20:58:06 +07:00
Book Pauk
8f6201b0f7 Работа над хоткеями 2020-03-15 16:12:55 +07:00
Book Pauk
4b146c70ad Мелкий рефакторинг 2020-03-08 18:53:40 +07:00
Book Pauk
0118034b4b Запись в историю 2020-03-06 18:10:22 +07:00
Book Pauk
39217053ca Исправление багов 2020-03-06 18:00:10 +07:00
Book Pauk
fba190c826 Переход на ServiceWorker вместо appcache 2020-03-06 15:20:56 +07:00
Book Pauk
5e9d528e16 Поправка конфига nginx 2020-03-05 14:35:10 +07:00
Book Pauk
c5921d88fc Добавлена настройка certbot 2020-03-05 14:31:48 +07:00
Book Pauk
eb980b0ea1 Удалил ненужное 2020-03-05 14:23:16 +07:00
Book Pauk
de5b4216f7 Добавлены конфиги для среды beta 2020-03-05 13:46:52 +07:00
Book Pauk
495ff57b19 Добавлен пакет sw-precache-webpack-plugin 2020-03-05 13:46:21 +07:00
Book Pauk
57948cf6e3 Мелкий рефакторинг 2020-03-04 15:45:13 +07:00
Book Pauk
1aebbbcabd Мелкий рефакторинг 2020-03-04 15:32:05 +07:00
Book Pauk
25b4cb072d Обновление пакетов 2020-03-04 15:15:53 +07:00
Book Pauk
1cdacc3a08 Merge tag '0.9.1' into develop
0.9.1
2020-03-03 21:48:07 +07:00
Book Pauk
34d9466d09 Merge branch 'release/0.9.1' 2020-03-03 21:48:01 +07:00
Book Pauk
c182c4ce66 Версия 0.9.1 2020-03-03 21:47:13 +07:00
Book Pauk
dbb9bd1282 Добавлено сохранение валидных uploaded-файлов в удаленном хранилище 2020-03-03 18:06:46 +07:00
Book Pauk
8019d2d6cc Мелкая поправка 2020-03-03 16:44:21 +07:00
Book Pauk
459cdb2e0b Мелкие поправки 2020-03-03 16:41:38 +07:00
Book Pauk
a230cd9513 Добавлена настройка "Открывать оригинал по клику" 2020-03-03 12:51:40 +07:00
Book Pauk
0c44a25e85 Перенос чекбокса в другой раздел 2020-03-03 12:30:31 +07:00
Book Pauk
34f3d04370 Апдейт пакетов 2020-03-02 19:08:17 +07:00
Book Pauk
1f3e6b7e16 Удалены более ненужные пакеты 2020-03-02 17:24:39 +07:00
Book Pauk
47d49a200a Merge tag '0.9.0-1' into develop
0.9.0-1
2020-02-26 18:10:56 +07:00
Book Pauk
e1767d6e52 Merge branch 'release/0.9.0-1' 2020-02-26 18:10:48 +07:00
Book Pauk
0f8e343cd2 Поправлен баг 2020-02-26 18:10:02 +07:00
Book Pauk
23ab487baf Merge tag '0.9.0' into develop
0.9.0
2020-02-26 17:48:45 +07:00
87 changed files with 6933 additions and 1333 deletions

View File

@@ -8,7 +8,7 @@
![](docs/assets/reader.jpg)
## VPS
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
## Сборка проекта
Необходима версия node.js не ниже 10.

View File

@@ -9,7 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const AppCachePlugin = require('appcache-webpack-plugin');
const {GenerateSW} = require('workbox-webpack-plugin');
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
const clientDir = path.resolve(__dirname, '../client');
@@ -55,6 +55,12 @@ module.exports = merge(baseWpConfig, {
filename: `${publicDir}/index.html`
}),
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
new AppCachePlugin({exclude: ['../index.html']})
new GenerateSW({
cacheId: 'liberama',
swDest: `${publicDir}/service-worker.js`,
navigateFallback: '/index.html',
navigateFallbackDenylist: [new RegExp('^/api'), new RegExp('^/ws'), new RegExp('^/tmp'),],
skipWaiting: true,
}),
]
});

View File

@@ -1,3 +1,5 @@
import * as utils from '../share/utils';
const cleanPeriod = 60*1000;//1 минута
class WebSocketConnection {
@@ -9,6 +11,8 @@ class WebSocketConnection {
this.messageQueue = [];
this.messageLifeTime = messageLifeTime;
this.requestId = 0;
this.connecting = false;
}
addListener(listener) {
@@ -53,14 +57,22 @@ class WebSocketConnection {
}
open(url) {
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { (async() => {
//Ожидаем окончания процесса подключения, если open уже был вызван
let i = 0;
while (this.connecting && i < 200) {//10 сек
await utils.sleep(50);
i++;
}
if (i >= 200)
this.connecting = false;
//проверим подключение, и если нет, то подключимся заново
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
let protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:'
}
this.connecting = true;
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
url = url || `${protocol}//${window.location.host}/ws`;
@@ -71,9 +83,8 @@ class WebSocketConnection {
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
let resolved = false;
this.ws.onopen = (e) => {
resolved = true;
this.connecting = false;
resolve(e);
};
@@ -97,11 +108,13 @@ class WebSocketConnection {
this.ws.onerror = (e) => {
this.emit(e.message, true);
if (!resolved)
if (this.connecting) {
this.connecting = false;
reject(e);
}
};
}
});
})() });
}
//timeout в минутах (cleanPeriod)

View File

@@ -0,0 +1,5 @@
(function() {
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}
})();

View File

@@ -1,56 +1,4 @@
<template>
<!--q-layout view="lhr lpr lfr">
<q-drawer v-model="showAsideBar" :width="asideWidth">
<div class="app-name"><span v-html="appName"></span></div>
<q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
<q-list>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="inbox" />
</q-item-section>
<q-item-section>Inbox</q-item-section>
</q-item>
</q-list-->
<!--el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
<el-menu-item index="/cardindex">
<i class="el-icon-search"></i>
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
</el-menu-item>
<el-menu-item index="/reader">
<i class="el-icon-tickets"></i>
<span :class="itemTitleClass('/reader')" slot="title">{{ this.itemRuText['/reader'] }}</span>
</el-menu-item>
<el-menu-item index="/forum" disabled>
<i class="el-icon-message"></i>
<span :class="itemTitleClass('/forum')" slot="title">{{ this.itemRuText['/forum'] }}</span>
</el-menu-item>
<el-menu-item index="/income">
<i class="el-icon-upload"></i>
<span :class="itemTitleClass('/income')" slot="title">{{ this.itemRuText['/income'] }}</span>
</el-menu-item>
<el-menu-item index="/sources">
<i class="el-icon-menu"></i>
<span :class="itemTitleClass('/sources')" slot="title">{{ this.itemRuText['/sources'] }}</span>
</el-menu-item>
<el-menu-item index="/settings">
<i class="el-icon-setting"></i>
<span :class="itemTitleClass('/settings')" slot="title">{{ this.itemRuText['/settings'] }}</span>
</el-menu-item>
<el-menu-item index="/help">
<i class="el-icon-question"></i>
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
</el-menu-item>
</el-menu-->
<!--/q-drawer>
<q-page-container>
<keep-alive>
<router-view></router-view>
</keep-alive>
</q-page-container>
</q-layout-->
<div class="fit row">
<Notify ref="notify"/>
<StdDialog ref="stdDialog"/>
@@ -90,7 +38,8 @@ class App extends Vue {
'/sources': 'Источники',
'/settings': 'Параметры',
'/help': 'Справка',
}
};
created() {
this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
@@ -106,10 +55,20 @@ class App extends Vue {
cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i);
cachedRoute = (m ? m[1] : this.$route.path);
}
return cachedRoute;
}
this.$router.beforeEach((to, from, next) => {
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
if (window.location.host.indexOf('b.') == 0 && to.path != '/external-libs' && to.path != '/404') {
next('/404');
} else {
next();
}
});
// set-app-title
this.$root.$on('set-app-title', this.setAppTitle);
@@ -133,15 +92,26 @@ class App extends Vue {
document.addEventListener('keyup', (event) => {
this.keyHook(event);
});
});
document.addEventListener('keypress', (event) => {
this.keyHook(event);
});
document.addEventListener('keydown', (event) => {
this.keyHook(event);
});
});
window.addEventListener('resize', () => {
this.$root.$emit('resize');
});
}
routerReady() {
return new Promise ((resolve) => {
this.$router.onReady(() => {
resolve();
});
});
}
mounted() {
this.$root.notify = this.$refs.notify;
this.$root.stdDialog = this.$refs.stdDialog;
@@ -157,7 +127,10 @@ class App extends Vue {
});
this.setAppTitle();
this.redirectIfNeeded();
(async() => {
await this.routerReady();
this.redirectIfNeeded();
})();
}
toggleCollapse() {
@@ -202,7 +175,9 @@ class App extends Vue {
setAppTitle(title) {
if (!title) {
if (this.mode == 'omnireader') {
if (this.mode == 'liberama.top') {
document.title = `Liberama Reader - всегда с вами`;
} else if (this.mode == 'omnireader') {
document.title = `Omni Reader - всегда с вами`;
} else if (this.config && this.mode !== null) {
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
@@ -221,29 +196,32 @@ class App extends Vue {
}
get showAsideBar() {
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
}
set showAsideBar(value) {
}
get isReaderActive() {
return this.rootRoute == '/reader';
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
}
redirectIfNeeded() {
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
//старый url
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
const search = window.location.search.substr(1);
const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) {
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
//распознавание параметра url вида "?url=<link>" и редирект при необходимости
if (!this.isReaderActive) {
const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) {
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
}
}
}
}
@@ -270,7 +248,7 @@ body, html, #app {
}
.dborder {
border: 2px solid yellow !important;
border: 2px solid magenta !important;
}
.icon-rotate {

View File

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

View File

@@ -0,0 +1,857 @@
<template>
<Window ref="window" @close="close" margin="2px">
<template slot="header">
{{ header }}
</template>
<template slot="buttons">
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<q-icon name="la la-plus" size="16px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<q-icon name="la la-minus" size="16px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
<q-icon name="la la-question-circle" size="16px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
</span>
</template>
<div v-show="ready" class="col column" style="min-width: 600px">
<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"
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
style="width: 230px"
dropdown-icon="la la-angle-down la-sm"
rounded 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"
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>
</q-select>
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
>
<template v-slot:prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
</q-btn>
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
</q-btn>
</template>
<template v-slot:append>
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
</q-btn>
</template>
</q-input>
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
</q-btn>
</div>
<div class="separator"></div>
<div class="col fit" ref="frameBox" style="position: relative;">
<div ref="frameWrap" class="overflow-hidden">
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
</div>
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
</div>
<Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
<template slot="header">
<div class="row items-center">
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
<div v-else>Добавить закладку</div>
</div>
</template>
<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"
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus">
</q-input>
<q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
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-select>
</div>
<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"
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus">
</q-input>
</div>
<template slot="footer">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
</template>
</Dialog>
<Dialog ref="options" v-model="optionsVisible">
<template slot="header">
<div class="row items-center">
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
Опции
</div>
</template>
<div class="q-mx-md column">
<q-checkbox v-model="closeAfterSubmit" size="36px" label="Закрыть окно при отправке ссылки в читалку" />
<q-checkbox v-model="openInFrameOnEnter" size="36px" label="Открывать ссылку во фрейме при нажатии 'Enter'" />
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
</div>
<template slot="footer">
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
</template>
</Dialog>
</div>
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
@do-action="doAction" @close="closeBookmarkSettings">
</BookmarkSettings>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import Window from '../share/Window.vue';
import Dialog from '../share/Dialog.vue';
import BookmarkSettings from './BookmarkSettings/BookmarkSettings.vue';
import rstore from '../../store/modules/reader';
import * as utils from '../../share/utils';
import * as lu from './linkUtils';
const proxySubst = {
'http://flibusta.is': 'http://b.liberama.top:23480',
};
export default @Component({
components: {
Window,
Dialog,
BookmarkSettings
},
watch: {
libs: function() {
this.loadLibs();
},
defaultRootLink: function() {
this.updateBookmarkLink();
},
bookUrl: function(newValue) {
const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true);
if (value != subst) {
this.$nextTick(() => {
this.bookUrl = subst;
});
}
},
bookmarkLink: function(newValue) {
const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true);
if (value != subst) {
this.$nextTick(() => {
this.bookmarkLink = subst;
});
}
},
closeAfterSubmit: function(newValue) {
this.commitProp('closeAfterSubmit', newValue);
},
openInFrameOnEnter: function(newValue) {
this.commitProp('openInFrameOnEnter', newValue);
},
openInFrameOnAdd: function(newValue) {
this.commitProp('openInFrameOnAdd', newValue);
},
}
})
class ExternalLibs extends Vue {
ready = false;
frameVisible = false;
rootLink = '';
selectedLink = '';
frameSrc = '';
bookUrl = '';
libs = {};
fullScreenActive = false;
transparentLayoutVisible = false;
addBookmarkVisible = false;
optionsVisible = false;
addBookmarkMode = '';
bookmarkLink = '';
bookmarkDesc = '';
defaultRootLink = '';
bookmarkSettingsActive = false;
closeAfterSubmit = false;
openInFrameOnEnter = false;
openInFrameOnAdd = false;
frameScale = 1;
created() {
this.oldStartLink = '';
this.justOpened = true;
this.$root.addKeyHook(this.keyHook);
this.$root.$on('resize', async() => {
await utils.sleep(200);
this.frameResize();
});
document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null);
});
this.debouncedGoToLink = _.debounce((link) => {
this.goToLink(link);
}, 100, {'maxWait':200});
//this.commit = this.$store.commit;
//this.commit('reader/setLibs', rstore.libsDefaults);
}
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() => {
//подождем this.mode
let i = 0;
while(!this.mode && i < 100) {
await utils.sleep(100);
i++;
}
if (this.mode != 'liberama.top') {
this.$router.replace('/404');
return;
}
this.$refs.window.init();
this.opener = null;
const host = window.location.host;
const openerHost = (host.indexOf('b.') == 0 ? host.substring(2) : host);
const openerOrigin1 = `http://${openerHost}`;
const openerOrigin2 = `https://${openerHost}`;
window.addEventListener('message', (event) => {
if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
return;
if (!_.isObject(event.data) || event.data.from != 'LibsPage')
return;
if (event.origin == openerOrigin1)
this.opener = window.opener;
else
this.opener = event.source;
this.openerOrigin = event.origin;
//console.log(event);
this.recvMessage(event.data);
});
//Ожидаем родителя
i = 0;
while(!this.opener) {
await utils.sleep(1000);
i++;
if (i >= 5) {
await this.$root.stdDialog.alert('Нет связи с читалкой. Окно будет закрыто', 'Ошибка');
window.close();
}
}
//Проверка закрытия родительского окна
while(this.opener) {
await this.checkOpener();
await utils.sleep(1000);
}
})();
}
recvMessage(d) {
if (d.type == 'mes') {
switch(d.data) {
case 'hello': this.sendMessage({type: 'mes', data: 'ready'}); break;
}
} else if (d.type == 'libs') {
this.ready = true;
this.libs = _.cloneDeep(d.data);
} else if (d.type == 'notify') {
this.$root.notify.success(d.data, '', {position: 'bottom-right'});
}
}
sendMessage(d) {
(async() => {
await this.checkOpener();
if (this.opener && this.openerOrigin)
this.opener.postMessage(Object.assign({}, {from: 'ExternalLibs'}, d), this.openerOrigin);
})();
}
async checkOpener() {
if (this.opener.closed) {
await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
window.close();
}
}
commitLibs(libs) {
this.sendMessage({type: 'libs', data: libs});
}
commitProp(prop, value) {
let libs = _.cloneDeep(this.libs);
libs[prop] = value;
this.commitLibs(libs);
}
loadLibs() {
const libs = this.libs;
if (!libs.helpShowed) {
this.showHelp();
(async() => {
await utils.sleep(1000);
this.commitProp('helpShowed', true);
})();
}
this.selectedLink = libs.startLink;
this.closeAfterSubmit = libs.closeAfterSubmit || false;
this.openInFrameOnEnter = libs.openInFrameOnEnter || false;
this.openInFrameOnAdd = libs.openInFrameOnAdd || false;
this.frameScale = 1;
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
if (index >= 0)
this.frameScale = this.libs.groups[index].frameScale || 1;
this.updateStartLink();
}
doAction(event) {
switch (event.action) {
case 'setLibs': this.commitLibs(event.data); break;
case 'setRootLink': this.rootLink = event.data; this.rootLinkInput(); break;
case 'setSelectedLink': this.selectedLink = event.data; this.selectedLinkInput(); break;
case 'editBookmark': this.addBookmark('edit', event.data.link, event.data.desc); break;
case 'addBookmark': this.addBookmark('add'); break;
}
}
get mode() {
return this.$store.state.config.mode;
}
get header() {
let result = (this.ready ? 'Библиотека' : 'Загрузка...');
if (this.ready && this.selectedLink) {
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
}
this.$root.$emit('set-app-title', result);
return result;
}
get rootLinkWithoutProtocol() {
return lu.removeProtocol(this.rootLink);
}
updateSelectedLinkByRoot() {
if (!this.ready)
return;
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
if (index >= 0)
this.selectedLink = this.libs.groups[index].s;
else
this.selectedLink = '';
}
updateStartLink(force) {
if (!this.ready)
return;
let index = -1;
try {
this.rootLink = lu.getOrigin(this.selectedLink);
index = lu.getRootIndexByUrl(this.libs.groups, this.rootLink);
} catch(e) {
//
}
if (index >= 0) {
let libs = _.cloneDeep(this.libs);
const com = this.getCommentByLink(libs.groups[index].list, this.selectedLink);
if (libs.groups[index].s != this.selectedLink ||
libs.startLink != this.selectedLink ||
libs.comment != com) {
libs.groups[index].s = this.selectedLink;
libs.startLink = this.selectedLink;
libs.comment = com;
this.commitLibs(libs);
}
if (force || this.oldStartLink != libs.startLink) {
this.oldStartLink = libs.startLink;
this.debouncedGoToLink(this.selectedLink);
}
} else {
this.rootLink = '';
this.selectedLink = '';
this.debouncedGoToLink(this.selectedLink);
}
}
get rootLinkOptions() {
let result = [];
if (!this.ready)
return result;
this.libs.groups.forEach(group => {
result.push({label: lu.removeProtocol(group.r), value: group.r});
});
return result;
}
get defaultRootLinkOptions() {
let result = [];
rstore.libsDefaults.groups.forEach(group => {
result.push({label: lu.removeProtocol(group.r), value: group.r});
});
return result;
}
get selectedLinkOptions() {
let result = [];
if (!this.ready)
return result;
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.rootLink);
if (index >= 0) {
this.libs.groups[index].list.forEach(link => {
result.push({label: (link.c ? link.c + ' ': '') + lu.removeOrigin(link.l), value: link.l});
});
}
return result;
}
openBookUrlInFrame() {
if (this.bookUrl) {
this.goToLink(lu.addProtocol(this.bookUrl));
}
}
goToLink(link) {
if (!this.ready || !link)
return;
if (!link) {
this.frameVisible = false;
return;
}
this.frameSrc = this.makeProxySubst(link);
this.frameVisible = false;
this.$nextTick(() => {
this.frameVisible = true;
this.$nextTick(() => {
if (this.$refs.frame) {
this.$refs.frame.contentWindow.focus();
this.frameResize();
}
});
});
}
frameResize() {
this.$refs.frameWrap.style = 'width: 1px; height: 1px;';
this.$nextTick(() => {
if (this.$refs.frame) {
const w = this.$refs.frameBox.offsetWidth;
const h = this.$refs.frameBox.offsetHeight;
const normalSize = `width: ${w}px; height: ${h}px;`;
this.$refs.frameWrap.style = normalSize;
if (this.frameScale != 1) {
const s = this.frameScale;
this.$refs.frame.style = `width: ${w/s}px; height: ${h/s}px; transform: scale(${s}); transform-origin: 0 0;`;
} else {
this.$refs.frame.style = normalSize;
}
}
});
}
changeScale(delta) {
if ((this.frameScale > 0.1 && delta <= 0) || (this.frameScale < 5 && delta >= 0)) {
this.frameScale = _.round(this.frameScale + delta, 1);
const index = lu.getSafeRootIndexByUrl(this.libs.groups, this.selectedLink);
if (index >= 0) {
let libs = _.cloneDeep(this.libs);
libs.groups[index].frameScale = this.frameScale;
this.commitLibs(libs);
}
this.frameResize();
this.$root.notify.success(`Масштаб изменен: ${(this.frameScale*100).toFixed(0)}%`, '', {position: 'bottom-right'});
}
}
getCommentByLink(list, link) {
const item = lu.getListItemByLink(list, link);
return (item ? item.c : '');
}
makeProxySubst(url, reverse = false) {
for (const [key, value] of Object.entries(proxySubst)) {
if (reverse && value == url.substring(0, value.length)) {
return key + url.substring(value.length);
} else if (!reverse && key == url.substring(0, key.length)) {
return value + url.substring(key.length);
}
}
return url;
}
selectAllOnFocus(event) {
if (event.target.select)
event.target.select();
}
rootLinkInput() {
this.updateSelectedLinkByRoot();
this.updateStartLink(true);
}
selectedLinkInput() {
this.updateStartLink(true);
}
submitUrl() {
if (this.bookUrl) {
this.sendMessage({type: 'submitUrl', data: {
url: this.bookUrl,
force: true
}});
this.bookUrl = '';
if (this.closeAfterSubmit)
this.close();
}
}
addBookmark(mode = 'add', link = '', desc = '') {
if (mode == 'edit') {
this.editBookmarkLink = this.bookmarkLink = link;
this.editBookmarkDesc = this.bookmarkDesc = desc;
} else {
this.bookmarkLink = this.bookUrl;
this.bookmarkDesc = '';
}
this.addBookmarkMode = mode;
this.addBookmarkVisible = true;
this.$nextTick(() => {
this.$refs.bookmarkLink.focus();
this.$refs.defaultRootLink.toggleOption = this.toggleOption;
});
}
updateBookmarkLink() {
const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
if (index >= 0) {
this.bookmarkLink = rstore.libsDefaults.groups[index].s;
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
} else {
this.bookmarkLink = '';
this.bookmarkDesc = '';
}
}
defaultRootLinkInput() {
this.updateBookmarkLink();
}
bookmarkLinkKeyDown(event) {
if (event.key == 'Enter') {
this.$refs.bookmarkDesc.focus();
event.preventDefault();
}
}
bookmarkDescKeyDown(event) {
if (event.key == 'Enter') {
event.stopPropagation();
event.preventDefault();
this.okAddBookmark();
}
}
async okAddBookmark() {
if (!this.bookmarkLink)
return;
const link = (this.addBookmarkMode == 'edit' ? lu.addProtocol(this.editBookmarkLink) : lu.addProtocol(this.bookmarkLink));
let index = -1;
try {
index = lu.getRootIndexByUrl(this.libs.groups, link);
} catch (e) {
await this.$root.stdDialog.alert('Неверный формат ссылки', 'Ошибка');
return;
}
let libs = _.cloneDeep(this.libs);
//добавление
//есть группа в закладках
if (index >= 0) {
const item = lu.getListItemByLink(libs.groups[index].list, link);
//редактирование
if (item && this.addBookmarkMode == 'edit') {
if (item) {
//редактируем
item.l = link;
item.c = this.bookmarkDesc;
this.commitLibs(libs);
} else {
await this.$root.stdDialog.alert('Не удалось отредактировать закладку', 'Ошибка');
}
} else if (!item) {
//добавляем
if (libs.groups[index].list.length >= 100) {
await this.$root.stdDialog.alert('Достигнут предел количества закладок для этого сайта', 'Ошибка');
return;
}
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
if (this.openInFrameOnAdd) {
libs.startLink = link;
libs.comment = this.bookmarkDesc;
}
this.commitLibs(libs);
} else if (item.c != this.bookmarkDesc) {
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
item.c = this.bookmarkDesc;
this.commitLibs(libs);
} else
return;
} else {
await this.$root.stdDialog.alert('Такая закладка уже существует', ' ');
return;
}
} else {//нет группы в закладках
if (libs.groups.length >= 100) {
await this.$root.stdDialog.alert('Достигнут предел количества различных сайтов в закладках', 'Ошибка');
return;
}
//добавляем сначала группу
libs.groups.push({r: lu.getOrigin(link), s: link, list: []});
index = lu.getSafeRootIndexByUrl(libs.groups, link);
if (index >= 0)
libs.groups[index].list.push({l: link, c: this.bookmarkDesc});
if (this.openInFrameOnAdd) {
libs.startLink = link;
libs.comment = this.bookmarkDesc;
}
this.commitLibs(libs);
}
this.addBookmarkVisible = false;
}
fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) {
this.$q.fullscreen.request();
} else {
this.$q.fullscreen.exit();
}
}
transparentLayoutClick() {
this.transparentLayoutVisible = false;
}
onSelectPopupShow() {
this.transparentLayoutVisible = true;
}
onSelectPopupHide() {
this.transparentLayoutVisible = false;
}
close() {
this.sendMessage({type: 'close'});
}
bookUrlKeyDown(event) {
if (event.key == 'Enter') {
if (!this.openInFrameOnEnter) {
this.submitUrl();
} else {
if (this.bookUrl)
this.goToLink(this.bookUrl);
}
event.preventDefault();
}
}
bookmarkSettings() {
this.bookmarkSettingsActive = true;
this.$nextTick(() => {
this.$refs.bookmarkSettings.init();
});
}
closeBookmarkSettings() {
this.bookmarkSettingsActive = false;
}
showHelp() {
this.$root.stdDialog.alert(`
<p>Окно 'Библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
что особенно актуально для мобильных устройств.</p>
<p>'Библиотека' разрешает свободный доступ к сайту flibusta.is. Имеется возможность управлять закладками
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
к сожалению, в нем открываются не все страницы.
</p>
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
</p>
<p>Приятного пользования ;-)
</p>
`, 'Справка', {iconName: 'la la-info-circle'});
}
keyHook(event) {
if (this.$root.rootRoute() == '/external-libs') {
if (this.$root.stdDialog.active)
return false;
if (this.bookmarkSettingsActive && this.$refs.bookmarkSettings.keyHook(event))
return true;
if (this.addBookmarkVisible || this.optionsVisible)
return false;
if (event.type == 'keydown' && event.key == 'F4') {
this.addBookmark();
return true;
}
if (event.type == 'keydown' && event.key == 'Escape' &&
(document.activeElement != this.$refs.rootLink.$refs.target || !this.$refs.rootLink.menu) &&
(document.activeElement != this.$refs.selectedLink.$refs.target || !this.$refs.selectedLink.menu)
) {
this.close();
return true;
}
}
return false;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.separator {
height: 1px;
background-color: #A0A0A0;
}
.full-screen-button {
width: 30px;
height: 30px;
cursor: pointer;
}
.full-screen-button:hover {
background-color: #69C05F;
}
.transparent-layout {
top: 0;
left: 0;
position: absolute;
}
</style>

View File

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

View File

@@ -91,11 +91,11 @@ class CopyTextPage extends Vue {
close() {
this.stopInit = true;
this.$emit('copy-text-toggle');
this.$emit('do-action', {action: 'copyText'});
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;

View File

@@ -22,15 +22,15 @@
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
<div v-show="mode == 'omnireader'">
<div v-show="mode == 'omnireader' || mode == 'liberama.top'">
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
<q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
<br><strong>{{ bookmarkText }}</strong>
<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-icon>
<br>или перетащив на панель закладок следующую ссылку:
<br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
<br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
</p>
@@ -56,6 +56,10 @@ class CommonHelpPage extends Vue {
return this.$store.state.config.mode;
}
get bookmarkText() {
return `javascript:location.href='https://${window.location.host}/?url='+location.href;`
}
async copyText(text, mes) {
const result = await copyTextToClipboard(text);
const msg = (result ? mes : 'Копирование не удалось');

View File

@@ -43,8 +43,8 @@ const pages = {
const tabs = [
['CommonHelpPage', 'Общее'],
['HotkeysHelpPage', 'Клавиатура'],
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'],
];
@@ -56,7 +56,7 @@ class HelpPage extends Vue {
selectedTab = 'CommonHelpPage';
close() {
this.$emit('help-toggle');
this.$emit('do-action', {action: 'help'});
}
get activePage() {
@@ -81,7 +81,7 @@ class HelpPage extends Vue {
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;

View File

@@ -1,28 +1,13 @@
<template>
<div class="page">
<span class="text-h6 text-bold">Управление с помощью горячих клавиш:</span>
<ul>
<li><b>F1, H</b> - открыть справку</li>
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
<li><b>Tab, Q</b> - показать/скрыть панель управления</li>
<li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
<li><b>PageDown, Right, Space</b> - страницу вперед</li>
<li><b>Home</b> - в начало книги</li>
<li><b>End</b> - в конец книги</li>
<li><b>Up</b> - строчку назад</li>
<li><b>Down</b> - строчку вперёд</li>
<li><b>A, Shift+A</b> - изменить размер шрифта</li>
<li><b>Enter, F, F11, ` (апостроф)</b> - вкл./выкл. полный экран</li>
<li><b>Z</b> - вкл./выкл. плавный скроллинг текста</li>
<li><b>Shift+Down/Shift+Up</b> - увеличить/уменьшить скорость скроллинга
<li><b>P</b> - установить страницу</li>
<li><b>Ctrl+F</b> - найти в тексте</li>
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
<li><b>X</b> - открыть недавние</li>
<li><b>O</b> - автономный режим</li>
<li><b>S</b> - открыть окно настроек</li>
</ul>
<div style="font-size: 120%">
<div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
<br>
</div>
<div class="q-mb-md" style="width: 550px">
<div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div>
<UserHotKeys v-model="userHotKeys" readonly/>
</div>
</div>
</template>
@@ -31,11 +16,25 @@
import Vue from 'vue';
import Component from 'vue-class-component';
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
export default @Component({
components: {
UserHotKeys,
},
})
class HotkeysHelpPage extends Vue {
created() {
}
get userHotKeys() {
return this.$store.state.reader.settings.userHotKeys;
}
set userHotKeys(value) {
//no setter
}
}
//-----------------------------------------------------------------------------
</script>
@@ -44,7 +43,5 @@ class HotkeysHelpPage extends Vue {
.page {
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="hidden"></div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils';
//import rstore from '../../../store/modules/reader';
export default @Component({
components: {
Window
},
watch: {
libs: function() {
this.sendLibs();
},
}
})
class LibsPage extends Vue {
created() {
this.popupWindow = null;
this.commit = this.$store.commit;
this.messageListener = null;
//this.commit('reader/setLibs', rstore.libsDefaults);
}
init() {
if (this.mode != 'liberama.top')
return;
this.childReady = false;
const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
this.origin = `http://${subdomain}${window.location.host}`;
this.messageListener = (event) => {
if (event.origin !== this.origin)
return;
//console.log(event.data);
this.recvMessage(event.data);
};
this.popupWindow = window.open(`${this.origin}/#/external-libs`);
if (this.popupWindow) {
window.addEventListener('message', this.messageListener);
//Проверка закрытия окна
(async() => {
while(this.popupWindow) {
if (this.popupWindow && this.popupWindow.closed)
this.close();
await utils.sleep(1000);
}
})();
//Установление связи с окном
(async() => {
let i = 0;
while(!this.childReady && this.popupWindow && i < 100) {
this.sendMessage({type: 'mes', data: 'hello'});
await utils.sleep(100);
i++;
}
this.sendLibs();
})();
}
}
recvMessage(d) {
if (d.type == 'mes') {
switch(d.data) {
case 'ready':
this.childReady = true;
break;
}
} else if (d.type == 'libs') {
this.commit('reader/setLibs', d.data);
} else if (d.type == 'close') {
this.close();
} else if (d.type == 'submitUrl') {
this.$emit('load-book', d.data);
this.sendMessage({type: 'notify', data: 'Ссылка передана в читалку'});
}
}
sendMessage(d) {
if (this.popupWindow)
this.popupWindow.postMessage(Object.assign({}, {from: 'LibsPage'}, d), this.origin);
}
done() {
window.removeEventListener('message', this.messageListener);
if (this.popupWindow) {
this.popupWindow.close();
this.popupWindow = null;
}
}
get mode() {
return this.$store.state.config.mode;
}
get libs() {
return this.$store.state.reader.libs;
}
sendLibs() {
this.sendMessage({type: 'libs', data: this.libs});
}
close() {
this.$emit('libs-close');
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.separator {
height: 1px;
background-color: #A0A0A0;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div ref="main" class="column no-wrap" style="min-height: 500px">
<div class="relative-position">
<div v-if="mode != 'liberama.top'" class="relative-position">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
</div>
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
@@ -31,7 +31,7 @@
</q-btn>
<div class="q-my-md"></div>
<div v-if="mode == 'omnireader'">
<!--div v-if="mode == 'omnireader'">
<div ref="yaShare2" class="ya-share2"
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
@@ -39,7 +39,7 @@
data-url="https://omnireader.ru">
</div>
</div>
<div class="q-my-sm"></div>
<div class="q-my-sm"></div-->
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
</div>
@@ -82,8 +82,8 @@ class LoaderPage extends Vue {
mounted() {
this.progress = this.$refs.progress;
if (this.mode == 'omnireader')
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
/*if (this.mode == 'omnireader')
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
}
activated() {
@@ -93,6 +93,8 @@ class LoaderPage extends Vue {
get title() {
if (this.mode == 'omnireader')
return 'Omni Reader - браузерная онлайн-читалка.';
if (this.mode == 'liberama.top')
return 'Liberama Reader - браузерная онлайн-читалка.';
return 'Универсальная читалка книг и ресурсов интернета.';
}
@@ -148,12 +150,12 @@ class LoaderPage extends Vue {
this.pasteTextActive = !this.pasteTextActive;
}
openHelp() {
this.$emit('help-toggle');
openHelp(event) {
this.$emit('do-action', {action: 'help', event});
}
openDonate() {
this.$emit('donate-toggle');
this.$emit('do-action', {action: 'donate'});
}
openComments() {
@@ -171,23 +173,21 @@ class LoaderPage extends Vue {
//недостатки сторонних ui
const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
this.submitUrl();
}
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
this.$emit('help-toggle');
event.preventDefault();
event.stopPropagation();
return true;
}
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
this.$emit('tool-bar-toggle');
event.preventDefault();
event.stopPropagation();
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;
}
}
//-----------------------------------------------------------------------------

View File

@@ -78,7 +78,7 @@ class PasteTextPage extends Vue {
keyHook(event) {
if (event.type == 'keydown') {
switch (event.code) {
switch (event.key) {
case 'F2':
this.loadBuffer();
break;

View File

@@ -2,60 +2,72 @@
<div class="column no-wrap">
<div ref="header" class="header" v-show="toolBarActive">
<div ref="buttons" class="row justify-between no-wrap">
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
<q-icon name="la la-arrow-left" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">Загрузить книгу</q-tooltip>
</button>
<div>
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
<q-icon name="la la-arrow-left" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
</button>
</div>
<div>
<button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
<q-icon name="la la-angle-left" size="32px"/>
<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%">{{ rstore.readerActions['undoAction'] }}</q-tooltip>
</button>
<button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
<q-icon name="la la-angle-right" size="32px"/>
<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%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
</button>
<div class="space"></div>
<button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
<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%">{{ rstore.readerActions['fullScreen'] }}</q-tooltip>
</button>
<button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
<q-icon name="la la-film" size="32px"/>
<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%">{{ rstore.readerActions['scrolling'] }}</q-tooltip>
</button>
<button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
<q-icon name="la la-angle-double-right" size="32px"/>
<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%">{{ rstore.readerActions['setPosition'] }}</q-tooltip>
</button>
<button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
<q-icon name="la la-search" size="32px"/>
<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%">{{ rstore.readerActions['search'] }}</q-tooltip>
</button>
<button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
<q-icon name="la la-copy" size="32px"/>
<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%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
</button>
<button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
<q-icon name="la la-retweet" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip>
</button>
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
<q-icon name="la la-sync" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Принудительно обновить книгу в обход кэша</q-tooltip>
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
</button>
<div class="space"></div>
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
<q-icon name="la la-unlink" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Автономный режим (без интернета)</q-tooltip>
<button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
<q-icon name="la la-sitemap" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
</button>
<button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
<q-icon name="la la-book-open" size="32px"/>
<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%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
</button>
</div>
<button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
<q-icon name="la la-cog" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">Настроить</q-tooltip>
</button>
<div>
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
<q-icon name="la la-unlink" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
</button>
<button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
<q-icon name="la la-cog" size="32px"/>
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
</button>
</div>
</div>
</div>
@@ -65,87 +77,26 @@
@load-book="loadBook"
@load-file="loadFile"
@book-pos-changed="bookPosChanged"
@tool-bar-toggle="toolBarToggle"
@full-screen-toogle="fullScreenToggle"
@stop-scrolling="stopScrolling"
@scrolling-toggle="scrollingToggle"
@help-toggle="helpToggle"
@donate-toggle="donateToggle"
@do-action="doAction"
></component>
</keep-alive>
<SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
<SearchPage v-show="searchActive" ref="searchPage"
@search-toggle="searchToggle"
@do-action="doAction"
@book-pos-changed="bookPosChanged"
@start-text-search="startTextSearch"
@stop-text-search="stopTextSearch">
</SearchPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
<LibsPage v-show="libsActive" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
<SettingsPage v-show="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<Dialog ref="dialog1" v-model="whatsNewVisible">
<template slot="header">
Что нового:
</template>
<div style="line-height: 20px" v-html="whatsNewContent"></div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
</span>
</Dialog>
<Dialog ref="dialog2" v-model="donationVisible">
<template slot="header">
Здравствуйте, уважаемые читатели!
</template>
<div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div>
<span slot="footer">
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
</div>
</div>
</template>
@@ -164,17 +115,18 @@ import ProgressPage from './ProgressPage/ProgressPage.vue';
import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
import SearchPage from './SearchPage/SearchPage.vue';
import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
import LibsPage from './LibsPage/LibsPage.vue';
import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
import SettingsPage from './SettingsPage/SettingsPage.vue';
import HelpPage from './HelpPage/HelpPage.vue';
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
import ServerStorage from './ServerStorage/ServerStorage.vue';
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager';
import rstore from '../../store/modules/reader';
import readerApi from '../../api/reader';
import * as utils from '../../share/utils';
import {versionHistory} from './versionHistory';
import Dialog from '../share/Dialog.vue';
export default @Component({
components: {
@@ -185,12 +137,13 @@ export default @Component({
SetPositionPage,
SearchPage,
CopyTextPage,
LibsPage,
RecentBooksPage,
SettingsPage,
HelpPage,
ClickMapPage,
ServerStorage,
Dialog,
ReaderDialogs,
},
watch: {
bookPos: function(newValue) {
@@ -232,7 +185,9 @@ export default @Component({
},
})
class Reader extends Vue {
rstore = {};
loaderActive = false;
offlineModeActive = false;
progressActive = false;
fullScreenActive = false;
@@ -240,8 +195,8 @@ class Reader extends Vue {
setPositionActive = false;
searchActive = false;
copyTextActive = false;
libsActive = false;
recentBooksActive = false;
offlineModeActive = false;
settingsActive = false;
helpActive = false;
clickMapActive = false;
@@ -261,6 +216,7 @@ class Reader extends Vue {
donationVisible = false;
created() {
this.rstore = rstore;
this.loading = true;
this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
@@ -317,10 +273,15 @@ class Reader extends Vue {
this.checkActivateDonateHelpPage();
this.loading = false;
//проверим состояние Settings, и обновим, если надо
const newSettings = rstore.addDefaultsToSettings(this.settings);
if (newSettings) {
this.commit('reader/setSettings', newSettings);
}
this.updateRoute();
await this.showWhatsNew();
await this.showDonation();
await this.$refs.dialogs.init();
})();
}
@@ -331,11 +292,14 @@ class Reader extends Vue {
this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showToolButton = settings.showToolButton;
this.enableSitesFilter = settings.enableSitesFilter;
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
this.$root.readerActionByKeyEvent = (event) => {
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
}
this.updateHeaderMinWidth();
}
@@ -391,64 +355,6 @@ class Reader extends Vue {
}
}
async showWhatsNew() {
await utils.sleep(2000);
const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) {
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
this.whatsNewVisible = true;
}
}
async showDonation() {
await utils.sleep(3000);
const today = utils.formatDate(new Date(), 'coDate');
if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
this.donationVisible = true;
}
}
donationDialogDisable() {
this.donationVisible = false;
if (this.showDonationDialog2020) {
const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
this.commit('reader/setSettings', newSettings);
}
}
donationDialogRemind() {
this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
}
openDonate() {
this.donationVisible = false;
this.donateToggle();
}
async copyLink(link) {
const result = await utils.copyTextToClipboard(link);
if (result)
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
else
this.$root.notify.error('Копирование не удалось');
}
openVersionHistory() {
this.whatsNewVisible = false;
this.versionHistoryToggle();
}
whatsNewDisable() {
this.whatsNewVisible = false;
const whatsNew = versionHistory[0];
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
}
get routeParamPos() {
let result = undefined;
const q = this.$route.query;
@@ -552,14 +458,6 @@ class Reader extends Vue {
return this.$store.state.reader.settings;
}
get whatsNewContentHash() {
return this.$store.state.reader.whatsNewContentHash;
}
get donationRemindDate() {
return this.$store.state.reader.donationRemindDate;
}
addAction(pos) {
let a = this.actionList;
if (!a.length || a[a.length - 1] != pos) {
@@ -584,7 +482,7 @@ class Reader extends Vue {
}
}
closeAllTextPages() {
closeAllWindows() {
this.setPositionActive = false;
this.copyTextActive = false;
this.recentBooksActive = false;
@@ -597,7 +495,7 @@ class Reader extends Vue {
loaderToggle() {
this.loaderActive = !this.loaderActive;
if (this.loaderActive) {
this.closeAllTextPages();
this.closeAllWindows();
}
}
@@ -605,7 +503,7 @@ class Reader extends Vue {
this.setPositionActive = !this.setPositionActive;
const page = this.$refs.page;
if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
this.closeAllTextPages();
this.closeAllWindows();
this.setPositionActive = true;
this.$nextTick(() => {
@@ -657,7 +555,7 @@ class Reader extends Vue {
this.searchActive = !this.searchActive;
const page = this.$refs.page;
if (this.searchActive && this.activePage == 'TextPage' && page.parsed) {
this.closeAllTextPages();
this.closeAllWindows();
this.searchActive = true;
this.$nextTick(() => {
@@ -673,7 +571,7 @@ class Reader extends Vue {
this.copyTextActive = !this.copyTextActive;
const page = this.$refs.page;
if (this.copyTextActive && this.activePage == 'TextPage' && page.parsed) {
this.closeAllTextPages();
this.closeAllWindows();
this.copyTextActive = true;
this.$nextTick(() => {
@@ -684,6 +582,12 @@ class Reader extends Vue {
}
}
refreshBookSplitToPara() {
if (this.mostRecentBook()) {
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
}
}
recentBooksClose() {
this.recentBooksActive = false;
}
@@ -691,7 +595,7 @@ class Reader extends Vue {
recentBooksToggle() {
this.recentBooksActive = !this.recentBooksActive;
if (this.recentBooksActive) {
this.closeAllTextPages();
this.closeAllWindows();
this.$refs.recentBooksPage.init();
this.recentBooksActive = true;
} else {
@@ -699,6 +603,20 @@ class Reader extends Vue {
}
}
libsClose() {
if (this.libsActive)
this.libsToogle();
}
libsToogle() {
this.libsActive = !this.libsActive;
if (this.libsActive) {
this.$refs.libsPage.init();
} else {
this.$refs.libsPage.done();
}
}
offlineModeToggle() {
this.offlineModeActive = !this.offlineModeActive;
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
@@ -707,7 +625,7 @@ class Reader extends Vue {
settingsToggle() {
this.settingsActive = !this.settingsActive;
if (this.settingsActive) {
this.closeAllTextPages();
this.closeAllWindows();
this.settingsActive = true;
this.$nextTick(() => {
@@ -721,7 +639,7 @@ class Reader extends Vue {
helpToggle() {
this.helpActive = !this.helpActive;
if (this.helpActive) {
this.closeAllTextPages();
this.closeAllWindows();
this.helpActive = true;
}
}
@@ -750,79 +668,52 @@ class Reader extends Vue {
}
}
buttonClick(button) {
const activeClass = this.buttonActiveClass(button);
undoAction() {
if (this.actionCur > 0) {
this.actionCur--;
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
}
}
this.$refs[button].blur();
redoAction() {
if (this.actionCur < this.actionList.length - 1) {
this.actionCur++;
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
}
}
buttonClick(action) {
const activeClass = this.buttonActiveClass(action);
this.$refs[action].blur();
if (activeClass['tool-button-disabled'])
return;
switch (button) {
case 'loader':
this.loaderToggle();
break;
case 'undoAction':
if (this.actionCur > 0) {
this.actionCur--;
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
}
break;
case 'redoAction':
if (this.actionCur < this.actionList.length - 1) {
this.actionCur++;
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
}
break;
case 'fullScreen':
this.fullScreenToggle();
break;
case 'setPosition':
this.setPositionToggle();
break;
case 'scrolling':
this.scrollingToggle();
break;
case 'search':
this.searchToggle();
break;
case 'copyText':
this.copyTextToggle();
break;
case 'refresh':
this.refreshBook();
break;
case 'recentBooks':
this.recentBooksToggle();
break;
case 'offlineMode':
this.offlineModeToggle();
break;
case 'settings':
this.settingsToggle();
break;
}
this.doAction({action});
}
buttonActiveClass(button) {
buttonActiveClass(action) {
const classActive = { 'tool-button-active': true, 'tool-button-active:hover': true };
const classDisabled = { 'tool-button-disabled': true, 'tool-button-disabled:hover': true };
let classResult = {};
switch (button) {
switch (action) {
case 'loader':
case 'fullScreen':
case 'setPosition':
case 'scrolling':
case 'search':
case 'copyText':
case 'splitToPara':
case 'refresh':
case 'offlineMode':
case 'libs':
case 'recentBooks':
case 'offlineMode':
case 'settings':
if (this.progressActive) {
classResult = classDisabled;
} else if (this[`${button}Active`]) {
} else if (this[`${action}Active`]) {
classResult = classActive;
}
break;
@@ -837,7 +728,7 @@ class Reader extends Vue {
}
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
switch (button) {
switch (action) {
case 'undoAction':
case 'redoAction':
case 'setPosition':
@@ -846,8 +737,9 @@ class Reader extends Vue {
case 'copyText':
classResult = classDisabled;
break;
case 'recentBooks':
case 'splitToPara':
case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive)
classResult = classDisabled;
break;
@@ -922,7 +814,7 @@ class Reader extends Vue {
return;
}
this.closeAllTextPages();
this.closeAllWindows();
let url = encodeURI(decodeURI(opts.url));
@@ -1000,9 +892,16 @@ class Reader extends Vue {
// не удалось, скачиваем книгу полностью с конвертацией
let loadCached = true;
if (!book) {
book = await readerApi.loadBook({url, enableSitesFilter: this.enableSitesFilter}, (state) => {
progress.setState(state);
});
book = await readerApi.loadBook({
url,
skipCheck: (opts.skipCheck ? true : false),
isText: (opts.isText ? true : false),
enableSitesFilter: this.enableSitesFilter
},
(state) => {
progress.setState(state);
}
);
loadCached = false;
}
@@ -1030,7 +929,7 @@ class Reader extends Vue {
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
@@ -1054,7 +953,7 @@ class Reader extends Vue {
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
@@ -1086,83 +985,162 @@ class Reader extends Vue {
}
}
doAction(opts) {
let result = true;
let {action = '', event = false} = opts;
switch (action) {
case 'loader':
this.loaderToggle();
break;
case 'help':
this.helpToggle();
break;
case 'undoAction':
this.undoAction();
break;
case 'redoAction':
this.redoAction();
break;
case 'fullScreen':
this.fullScreenToggle();
break;
case 'scrolling':
this.scrollingToggle();
break;
case 'stopScrolling':
this.stopScrolling();
break;
case 'setPosition':
this.setPositionToggle();
break;
case 'search':
this.searchToggle();
break;
case 'copyText':
this.copyTextToggle();
break;
case 'splitToPara':
this.refreshBookSplitToPara();
break;
case 'refresh':
this.refreshBook();
break;
case 'libs':
this.libsToogle();
break;
case 'recentBooks':
this.recentBooksToggle();
break;
case 'offlineMode':
this.offlineModeToggle();
break;
case 'settings':
this.settingsToggle();
break;
case 'switchToolbar':
this.toolBarToggle();
break;
case 'donate':
this.donateToggle();
break;
default:
result = false;
break;
}
if (!result && this.activePage == 'TextPage' && this.$refs.page) {
result = true;
const textPage = this.$refs.page;
switch (action) {
case 'bookBegin':
textPage.doHome();
break;
case 'bookEnd':
textPage.doEnd();
break;
case 'pageBack':
textPage.doPageUp();
break;
case 'pageForward':
textPage.doPageDown();
break;
case 'lineBack':
textPage.doUp();
break;
case 'lineForward':
textPage.doDown();
break;
case 'incFontSize':
textPage.doFontSizeInc();
break;
case 'decFontSize':
textPage.doFontSizeDec();
break;
case 'scrollingSpeedUp':
textPage.doScrollingSpeedUp();
break;
case 'scrollingSpeedDown':
textPage.doScrollingSpeedDown();
break;
default:
result = false;
break;
}
}
if (result && event) {
event.preventDefault();
event.stopPropagation();
}
return result;
}
keyHook(event) {
let result = false;
if (this.$root.rootRoute() == '/reader') {
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
return;
if (this.$root.stdDialog.active)
return result;
let handled = false;
if (!handled && this.helpActive)
handled = this.$refs.helpPage.keyHook(event);
if (!result)
result = this.$refs.dialogs.keyHook(event);
if (!handled && this.settingsActive)
handled = this.$refs.settingsPage.keyHook(event);
if (!result && this.helpActive)
result = this.$refs.helpPage.keyHook(event);
if (!handled && this.recentBooksActive)
handled = this.$refs.recentBooksPage.keyHook(event);
if (!result && this.settingsActive)
result = this.$refs.settingsPage.keyHook(event);
if (!handled && this.setPositionActive)
handled = this.$refs.setPositionPage.keyHook(event);
if (!result && this.recentBooksActive)
result = this.$refs.recentBooksPage.keyHook(event);
if (!handled && this.searchActive)
handled = this.$refs.searchPage.keyHook(event);
if (!result && this.setPositionActive)
result = this.$refs.setPositionPage.keyHook(event);
if (!handled && this.copyTextActive)
handled = this.$refs.copyTextPage.keyHook(event);
if (!result && this.searchActive)
result = this.$refs.searchPage.keyHook(event);
if (!handled && this.$refs.page && this.$refs.page.keyHook)
handled = this.$refs.page.keyHook(event);
if (!result && this.copyTextActive)
result = this.$refs.copyTextPage.keyHook(event);
if (!handled && event.type == 'keydown') {
if (event.code == 'Escape')
this.loaderToggle();
if (!result && this.$refs.page && this.$refs.page.keyHook)
result = this.$refs.page.keyHook(event);
if (this.activePage == 'TextPage') {
switch (event.code) {
case 'KeyH':
case 'F1':
this.helpToggle();
event.preventDefault();
event.stopPropagation();
break;
case 'KeyZ':
this.scrollingToggle();
break;
case 'KeyP':
this.setPositionToggle();
break;
case 'KeyF':
if (event.ctrlKey) {
this.searchToggle();
event.preventDefault();
event.stopPropagation();
}
break;
case 'KeyC':
if (event.ctrlKey) {
this.copyTextToggle();
event.preventDefault();
event.stopPropagation();
}
break;
case 'KeyR':
this.refreshBook();
break;
case 'KeyX':
this.recentBooksToggle();
event.preventDefault();
event.stopPropagation();
break;
case 'KeyO':
this.offlineModeToggle();
break;
case 'KeyS':
this.settingsToggle();
break;
}
if (!result && event.type == 'keydown') {
const action = this.$root.readerActionByKeyEvent(event);
if (action == 'loader') {
result = this.doAction({action, event});
}
if (!result && this.activePage == 'TextPage') {
result = this.doAction({action, event});
}
}
}
return result;
}
}
//-----------------------------------------------------------------------------
@@ -1237,16 +1215,4 @@ class Reader extends Vue {
.clear {
color: rgba(0,0,0,0);
}
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.copy-icon {
cursor: pointer;
font-size: 120%;
color: blue;
}
</style>

View File

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

View File

@@ -5,7 +5,7 @@
<span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
</template>
<a ref="download" style='display: none;'></a>
<a ref="download" style='display: none;' target="_blank"></a>
<q-table
class="recent-books-table col"
@@ -298,12 +298,13 @@ class RecentBooksPage extends Vue {
const d = this.$refs.download;
d.href = fb2path;
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
d.click();
} catch (e) {
let errMes = e.message;
if (errMes.indexOf('404') >= 0)
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
this.$root.stdDialog.alert(errMes, 'Ошибка', {type: 'negative'});
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
}
}
@@ -340,7 +341,7 @@ class RecentBooksPage extends Vue {
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;

View File

@@ -164,7 +164,7 @@ class SearchPage extends Vue {
close() {
this.stopInit = true;
this.$emit('search-toggle');
this.$emit('do-action', {action: 'search'});
}
inputKeyDown(event) {
@@ -174,7 +174,7 @@ class SearchPage extends Vue {
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;

View File

@@ -1,5 +1,5 @@
<template>
<div></div>
<div class="hidden"></div>
</template>
<script>
@@ -35,6 +35,9 @@ export default @Component({
currentProfile: function() {
this.currentProfileChanged(true);
},
libs: function() {
this.debouncedSaveLibs();
},
},
})
class ServerStorage extends Vue {
@@ -49,27 +52,32 @@ class ServerStorage extends Vue {
this.saveSettings();
}, 500);
this.debouncedSaveLibs = _.debounce(() => {
this.saveLibs();
}, 500);
this.debouncedNotifySuccess = _.debounce(() => {
this.success('Данные синхронизированы с сервером');
}, 1000);
this.oldProfiles = {};
this.oldSettings = {};
this.oldLibs = {};
}
async init() {
try {
this.cachedRecent = await ssCacheStore.getItem('recent');
if (!this.cachedRecent)
await this.setCachedRecent({rev: 0, data: {}});
await this.cleanCachedRecent('cachedRecent');
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
if (!this.cachedRecentPatch)
await this.setCachedRecentPatch({rev: 0, data: {}});
await this.cleanCachedRecent('cachedRecentPatch');
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
if (!this.cachedRecentMod)
await this.setCachedRecentMod({rev: 0, data: {}});
await this.cleanCachedRecent('cachedRecentMod');
if (!this.serverStorageKey) {
//генерируем новый ключ
@@ -97,6 +105,15 @@ class ServerStorage extends Vue {
this.cachedRecentMod = value;
}
async cleanCachedRecent(whatToClean) {
if (whatToClean == 'cachedRecent' || whatToClean == 'all')
await this.setCachedRecent({rev: 0, data: {}});
if (whatToClean == 'cachedRecentPatch' || whatToClean == 'all')
await this.setCachedRecentPatch({rev: 0, data: {}});
if (whatToClean == 'cachedRecentMod' || whatToClean == 'all')
await this.setCachedRecentMod({rev: 0, data: {}});
}
async generateNewServerStorageKey() {
const key = utils.toBase58(utils.randomArray(32));
this.commit('reader/setServerStorageKey', key);
@@ -124,9 +141,14 @@ class ServerStorage extends Vue {
await this.loadProfiles(force);
this.checkCurrentProfile();
await this.currentProfileChanged(force);
await this.loadLibs(force);
if (force)
await this.cleanCachedRecent('all');
const loadSuccess = await this.loadRecent();
if (loadSuccess && force)
if (loadSuccess && force) {
await this.saveRecent();
}
}
}
@@ -169,6 +191,14 @@ class ServerStorage extends Vue {
return this.settings.showServerStorageMessages;
}
get libs() {
return this.$store.state.reader.libs;
}
get libsRev() {
return this.$store.state.reader.libsRev;
}
checkCurrentProfile() {
if (!this.profiles[this.currentProfile]) {
this.commit('reader/setCurrentProfile', '');
@@ -338,13 +368,85 @@ class ServerStorage extends Vue {
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else if (result.state == 'success') {
this.oldProfiles = _.cloneDeep(this.profiles);
this.commit('reader/setProfilesRev', this.profilesRev + 1);
this.commit('reader/setProfilesRev', this.profilesRev + 1);
}
} finally {
this.savingProfiles = false;
}
}
async loadLibs(force = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled)
return;
const oldRev = this.libsRev;
//проверим ревизию на сервере
if (!force) {
try {
const revs = await this.storageCheck({libs: {}});
if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let libs = null;
try {
libs = await this.storageGet({libs: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (libs.state == 'success') {
libs = libs.items.libs;
if (libs.rev == 0)
libs.data = {};
this.oldLibs = _.cloneDeep(libs.data);
this.commit('reader/setLibs', libs.data);
this.commit('reader/setLibsRev', libs.rev);
if (doNotifySuccess)
this.debouncedNotifySuccess();
} else {
this.warning(`Неверный ответ сервера: ${libs.state}`);
}
}
async saveLibs() {
if (!this.keyInited || !this.serverSyncEnabled || this.savingLibs)
return;
const diff = utils.getObjDiff(this.oldLibs, this.libs);
if (utils.isEmptyObjDiff(diff))
return;
this.savingLibs = true;
try {
let result = {state: ''};
try {
result = await this.storageSet({libs: {rev: this.libsRev + 1, data: this.libs}});
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
await this.loadLibs(true, false);
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else if (result.state == 'success') {
this.oldLibs = _.cloneDeep(this.libs);
this.commit('reader/setLibsRev', this.libsRev + 1);
}
} finally {
this.savingLibs = false;
}
}
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
return;
@@ -403,7 +505,7 @@ class ServerStorage extends Vue {
const md = newRecentMod.data;
if (md.key && result[md.key])
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, true);
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией');
@@ -467,7 +569,7 @@ class ServerStorage extends Vue {
let applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true);
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
needSaveRecentPatch = true;

View File

@@ -58,8 +58,11 @@ class SetPositionPage extends Vue {
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
this.close();
if (event.type == 'keydown') {
const action = this.$root.readerActionByKeyEvent(event);
if (event.key == 'Escape' || action == 'setPosition') {
this.close();
}
}
return true;
}

View File

@@ -46,7 +46,7 @@
@@include('./include/ButtonsTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit tab-panel">
<div v-if="selectedTab == 'keys'" class="fit column">
@@include('./include/KeysTab.inc');
</div>
<!-- Листание -------------------------------------------------------------------->
@@ -76,6 +76,8 @@ import _ from 'lodash';
import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
@@ -85,6 +87,7 @@ export default @Component({
components: {
Window,
NumInput,
UserHotKeys,
},
data: function() {
return Object.assign({}, rstore.settingDefaults);
@@ -105,8 +108,10 @@ export default @Component({
},
vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName);
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
if (this.fontShifts[font] != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
}
},
fontName: function(newValue) {
const font = (this.webFontName ? this.webFontName : newValue);
@@ -139,6 +144,7 @@ export default @Component({
class SettingsPage extends Vue {
selectedTab = 'profiles';
selectedViewTab = 'color';
selectedKeysTab = 'mouse';
form = {};
fontBold = false;
fontItalic = false;
@@ -152,12 +158,14 @@ class SettingsPage extends Vue {
serverStorageKeyVisible = false;
toolButtons = [];
rstore = {};
created() {
this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
this.form = {};
this.rstore = rstore;
this.toolButtons = rstore.toolButtons;
this.settingsChanged();
}
@@ -179,10 +187,18 @@ class SettingsPage extends Vue {
settingsChanged() {
if (_.isEqual(this.form, this.settings))
return;
this.form = Object.assign({}, this.settings);
if (!this.unwatch)
this.unwatch = {};
for (let prop in rstore.settingDefaults) {
if (this.unwatch && this.unwatch[prop])
this.unwatch[prop]();
this[prop] = this.form[prop];
this.$watch(prop, (newValue) => {
this.unwatch[prop] = this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue});
});
}
@@ -339,7 +355,7 @@ class SettingsPage extends Vue {
}
close() {
this.$emit('settings-toggle');
this.$emit('do-action', {action: 'settings'});
}
async setDefaults() {
@@ -391,7 +407,7 @@ class SettingsPage extends Vue {
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.currentProfile}' необратимо.` +
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
@@ -493,7 +509,7 @@ class SettingsPage extends Vue {
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;

View File

@@ -0,0 +1,248 @@
<template>
<div class="table col column no-wrap">
<!-- header -->
<div class="table-row row">
<div class="desc q-pa-sm bg-blue-2">Команда</div>
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
<div style="width: 80px">Сочетание клавиш</div>
<q-input ref="input" class="q-ml-sm col"
outlined dense rounded
bg-color="grey-4"
placeholder="Найти"
v-model="search"
@click.stop
/>
<div v-show="!readonly" class="q-ml-sm column justify-center">
<q-btn class="bg-grey-4 text-grey-6" style="height: 35px; width: 35px" rounded flat icon="la la-broom" @click="defaultHotKeyAll">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Установить все сочетания по умолчанию
</q-tooltip>
</q-btn>
</div>
</div>
</div>
<!-- body -->
<div class="table-row row" v-for="(action, index) in tableData" :key="index">
<div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div>
<div class="hotKeys col q-pa-sm">
<q-chip
:color="collisions[code] ? 'red' : 'grey-7'"
:removable="!readonly" :clickable="collisions[code] ? true : false"
text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)"
@click="collisionWarning(code)"
>
{{ code }}
</q-chip>
</div>
<div v-show="!readonly" class="column q-pa-xs">
<q-icon
name="la la-plus-circle"
class="button bg-green-8 text-white"
@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>
</q-icon>
<q-icon
name="la la-broom"
class="button text-grey-5"
@click="defaultHotKey(action)"
v-ripple
>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
По умолчанию
</q-tooltip>
</q-icon>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import rstore from '../../../../store/modules/reader';
//import * as utils from '../../share/utils';
const UserHotKeysProps = Vue.extend({
props: {
value: Object,
readonly: Boolean,
}
});
export default @Component({
watch: {
search: function() {
this.updateTableData();
},
value: function() {
this.checkCollisions();
this.updateTableData();
}
},
})
class UserHotKeys extends UserHotKeysProps {
search = '';
rstore = {};
tableData = [];
collisions = {};
maxCodesLength = 10;
created() {
this.rstore = rstore;
}
mounted() {
this.checkCollisions();
this.updateTableData();
}
updateTableData() {
let result = rstore.hotKeys.map(hk => hk.name);
const search = this.search.toLowerCase();
const codesIncludeSearch = (action) => {
for (const code of this.value[action]) {
if (code.toLowerCase().includes(search))
return true;
}
return false;
};
result = result.filter(item => {
return !search ||
rstore.readerActions[item].toLowerCase().includes(search) ||
codesIncludeSearch(item)
});
this.tableData = result;
}
checkCollisions() {
const cols = {};
for (const [action, codes] of Object.entries(this.value)) {
codes.forEach(code => {
if (!cols[code])
cols[code] = [];
if (cols[code].indexOf(action) < 0)
cols[code].push(action);
});
}
const result = {};
for (const [code, actions] of Object.entries(cols)) {
if (actions.length > 1)
result[code] = actions;
}
this.collisions = result;
}
collisionWarning(code) {
if (this.collisions[code]) {
const descs = this.collisions[code].map(action => `<b>${rstore.readerActions[action]}</b>`);
this.$root.stdDialog.alert(`Сочетание '${code}' одновременно назначено<br>следующим командам:<br>${descs.join('<br>')}<br><br>
Возможно неожиданное поведение.`, 'Предупреждение');
}
}
removeCode(action, code) {
let codes = Array.from(this.value[action]);
const index = codes.indexOf(code);
if (index >= 0) {
codes.splice(index, 1);
const newValue = Object.assign({}, this.value, {[action]: codes});
this.$emit('input', newValue);
}
}
async addHotKey(action) {
if (this.value[action].length >= this.maxCodesLength)
return;
try {
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
if (result) {
let codes = Array.from(this.value[action]);
if (codes.indexOf(result) < 0) {
codes.push(result);
const newValue = Object.assign({}, this.value, {[action]: codes});
this.$emit('input', newValue);
this.$nextTick(() => {
this.collisionWarning(result);
});
}
}
} catch (e) {
//
}
}
async defaultHotKey(action) {
try {
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
const newValue = Object.assign({}, this.value, {[action]: codes});
this.$emit('input', newValue);
}
} catch (e) {
//
}
}
async defaultHotKeyAll() {
try {
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
this.$emit('input', newValue);
}
} catch (e) {
//
}
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.table {
border-left: 1px solid grey;
border-top: 1px solid grey;
}
.table-row {
border-right: 1px solid grey;
border-bottom: 1px solid grey;
}
.table-row:nth-child(even) {
background-color: #f7f7f7;
}
.table-row:hover {
background-color: #f0f0f0;
}
.desc {
width: 130px;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
}
.hotKeys {
border-left: 1px solid grey;
}
.button {
font-size: 25px;
border-radius: 25px;
cursor: pointer;
}
</style>

View File

@@ -1,8 +1,10 @@
<div class="part-header">Показывать кнопки панели</div>
<div class="item row" v-for="item in toolButtons" :key="item.name">
<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="item.text" />
<q-checkbox size="xs" @input="changeShowToolButton(item.name)"
:value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>
</div>

View File

@@ -1,8 +1,33 @@
<div class="part-header">Управление</div>
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedKeysTab"
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="mouse" label="Мышь/тачскрин" />
<q-tab name="keyboard" label="Клавиатура" />
</q-tabs>
</div>
<div class="item row">
<div class="label-4"></div>
<div class="col row">
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
<div class="q-mb-sm"/>
<div class="col tab-panel">
<div v-if="selectedKeysTab == 'mouse'">
<div class="item row">
<div class="label-4"></div>
<div class="col row">
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
</div>
</div>
</div>
<div v-if="selectedKeysTab == 'keyboard'">
<div class="item row">
<UserHotKeys v-model="userHotKeys" />
</div>
</div>
</div>

View File

@@ -54,6 +54,34 @@
<!---------------------------------------------->
<div class="part-header">Другое</div>
<div class="item row">
<div class="label-6">Обработка</div>
<div class="col row">
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Html-фильтр вырезает лишние элементы со<br>
страницы для определенных сайтов, таких как:<br>
samlib.ru<br>
www.fanfiction.net<br>
archiveofourown.org<br>
и других
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-6">Обработка</div>
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Включение этой опции позволяет делать предварительную<br>
подготовку всего текста в ленивом режиме сразу после<br>
загрузки книги. Это может повысить отзывчивость читалки,<br>
но нагружает процессор каждый раз при открытии книги.
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Парам. в URL</div>
<q-checkbox size="xs" v-model="allowUrlParamBookPos">
@@ -68,18 +96,6 @@
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Парсинг</div>
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная обработка текста">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Включение этой опции позволяет делать предварительную<br>
обработку текста в ленивом режиме сразу после загрузки<br>
книги. Это может повысить отзывчивость читалки, но<br>
нагружает процессор каждый раз при открытии книги.
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Копирование</div>
<q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">

View File

@@ -22,7 +22,7 @@
<q-select v-model="currentProfile" :options="currentProfileOptions"
style="width: 275px"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
outlined dense emit-value map-options display-value-sanitize options-sanitize
/>
</div>
</div>
@@ -68,7 +68,7 @@
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
<div v-if="mode == 'omnireader'">
<div v-if="mode == 'omnireader' || mode == 'liberama.top'">
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
<br><div class="text-center" style="margin-top: 5px">
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>

View File

@@ -22,3 +22,15 @@
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
</div>
</div>
<div class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
По клику на автора-название в строке статуса<br>
открывать оригинал произведения в новой вкладке
</q-tooltip>
</q-checkbox>
</div>
</div>

View File

@@ -108,22 +108,6 @@
<NumInput class="col" v-model="addEmptyParagraphs" :min="0" :max="2"/>
</div>
<div class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Html-фильтр вырезает лишние элементы со<br>
страницы для определенных сайтов, таких как:<br>
samlib.ru<br>
www.fanfiction.net<br>
archiveofourown.org<br>
и других
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-2">Изображения</div>
<div class="col row">

View File

@@ -21,11 +21,12 @@
@wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
oncontextmenu="return false;">
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div>
</div>
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div>
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick">
</div>
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
@@ -422,7 +423,7 @@ class TextPage extends Vue {
if (this.lazyParseEnabled)
this.lazyParsePara();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {type: 'negative'});
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
})();
}
@@ -503,7 +504,7 @@ class TextPage extends Vue {
async startTextScrolling() {
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
this.linesDown.length <= this.pageLineCount) {
this.$emit('stop-scrolling');
this.doStopScrolling();
return;
}
@@ -544,7 +545,7 @@ class TextPage extends Vue {
}
this.resolveTransition1Finish = null;
this.doingScrolling = false;
this.$emit('stop-scrolling');
this.doStopScrolling();
this.draw();
}
@@ -838,7 +839,7 @@ class TextPage extends Vue {
let i = this.pageLineCount;
if (this.keepLastToFirst)
i--;
if (i >= 0 && this.linesDown.length >= 2*i) {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.bookPos = this.linesDown[i].begin;
@@ -883,24 +884,27 @@ class TextPage extends Vue {
}
}
doToolBarToggle() {
this.$emit('tool-bar-toggle');
doToolBarToggle(event) {
this.$emit('do-action', {action: 'switchToolbar', event});
}
doScrollingToggle() {
this.$emit('scrolling-toggle');
this.$emit('do-action', {action: 'scrolling', event});
}
doFullScreenToggle() {
this.$emit('full-screen-toogle');
this.$emit('do-action', {action: 'fullScreen', event});
}
doStopScrolling() {
this.$emit('do-action', {action: 'stopScrolling', event});
}
async doFontSizeInc() {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newSize = (this.settings.fontSize + 1 < 100 ? this.settings.fontSize + 1 : 100);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
this.commit('reader/setSettings', newSettings);
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
this.commit('reader/setSettings', {fontSize: newSize});
await sleep(50);
this.settingsChanging = false;
}
@@ -910,8 +914,7 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
this.commit('reader/setSettings', newSettings);
this.commit('reader/setSettings', {fontSize: newSize});
await sleep(50);
this.settingsChanging = false;
}
@@ -921,8 +924,7 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
this.commit('reader/setSettings', newSettings);
this.commit('reader/setSettings', {scrollingDelay: newDelay});
await sleep(50);
this.settingsChanging = false;
}
@@ -932,76 +934,12 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
this.commit('reader/setSettings', newSettings);
this.commit('reader/setSettings', {scrollingDelay: newDelay});
await sleep(50);
this.settingsChanging = false;
}
}
keyHook(event) {
let result = false;
if (event.type == 'keydown' && !event.ctrlKey && !event.altKey) {
result = true;
switch (event.code) {
case 'ArrowDown':
if (event.shiftKey)
this.doScrollingSpeedUp();
else
this.doDown();
break;
case 'ArrowUp':
if (event.shiftKey)
this.doScrollingSpeedDown();
else
this.doUp();
break;
case 'PageDown':
case 'ArrowRight':
this.doPageDown();
break;
case 'Space':
if (event.shiftKey)
this.doPageUp();
else
this.doPageDown();
break;
case 'PageUp':
case 'ArrowLeft':
case 'Backspace':
this.doPageUp();
break;
case 'Home':
this.doHome();
break;
case 'End':
this.doEnd();
break;
case 'KeyA':
if (event.shiftKey)
this.doFontSizeDec();
else
this.doFontSizeInc();
break;
case 'Enter':
case 'Backquote'://`
case 'KeyF':
this.doFullScreenToggle();
break;
case 'Tab':
case 'KeyQ':
this.doToolBarToggle();
event.preventDefault();
event.stopPropagation();
break;
default:
result = false;
break;
}
}
return result;
}
async startClickRepeat(pointX, pointY) {
this.repX = pointX;
this.repY = pointY;
@@ -1079,7 +1017,7 @@ class TextPage extends Vue {
//движение вправо
this.doScrollingSpeedUp();
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
this.doToolBarToggle();
this.doToolBarToggle(event);
}
this.startTouch = null;
@@ -1106,7 +1044,7 @@ class TextPage extends Vue {
} else if (event.button == 1) {
this.doScrollingToggle();
} else if (event.button == 2) {
this.doToolBarToggle();
this.doToolBarToggle(event);
}
}
@@ -1131,7 +1069,7 @@ class TextPage extends Vue {
if (url && url.indexOf('file://') != 0) {
window.open(url, '_blank');
} else {
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {type: 'info'});
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
}
}

View File

@@ -59,7 +59,6 @@ export default class BookParser {
offset: Number, //сумма всех length до этого параграфа
length: Number, //длина text без тегов
text: String, //текст параграфа с вложенными тегами
cut: Boolean, //параграф - кандидат на сокрытие (cutEmptyParagraphs)
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
}
*/
@@ -116,7 +115,6 @@ export default class BookParser {
offset: paraOffset,
length: len,
text: text,
cut: (!addIndex && (len == 1 && text[0] == ' ')),
addIndex: (addIndex ? addIndex : 0),
};
@@ -132,10 +130,10 @@ export default class BookParser {
}
let p = para[paraIndex];
//добавление пустых (addEmptyParagraphs) параграфов
paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
paraIndex--;
paraOffset -= p.length;
for (let i = 0; i < 2; i++) {
newParagraph(' ', 1, i + 1);
}
@@ -144,15 +142,10 @@ export default class BookParser {
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
paraOffset += p.length;
}
paraOffset -= p.length;
//параграф оказался непустой
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
//уберем начальный пробел
p.length = 0;
p.text = p.text.substr(1);
p.cut = (len == 1 && text[0] == ' ');
}
p.length += len;
@@ -633,10 +626,7 @@ export default class BookParser {
imageHeightLines: this.imageHeightLines,
imageFitWidth: this.imageFitWidth,
compactTextPerc: this.compactTextPerc,
visible: !(
(this.cutEmptyParagraphs && para.cut) ||
(para.addIndex > this.addEmptyParagraphs)
)
visible: true, //вычисляется позже
};
@@ -652,9 +642,12 @@ export default class BookParser {
text: String,
}
}*/
let parts = this.splitToStyle(para.text);
//инициализация парсера
let line = {begin: para.offset, parts: []};
let paragraphText = '';//текст параграфа
let partText = '';//накапливаемый кусок со стилем
let str = '';//измеряемая строка
@@ -663,14 +656,16 @@ export default class BookParser {
let style = {};
let ofs = 0;//смещение от начала параграфа para.offset
let imgW = 0;
let imageInPara = false;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) {
style = part.style;
paragraphText += part.text;
//изображения
if (part.image.id && !part.image.inline) {
parsed.visible = this.showImages;
imageInPara = true;
let bin = this.binary[part.image.id];
if (!bin)
bin = {h: 1, w: 1};
@@ -837,6 +832,16 @@ export default class BookParser {
}
}
//parsed.visible
if (imageInPara) {
parsed.visible = this.showImages;
} else {
parsed.visible = !(
(para.addIndex > this.addEmptyParagraphs) ||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
);
}
parsed.lines = lines;
para.parsed = parsed;

View File

@@ -1,4 +1,77 @@
export const versionHistory = [
{
showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content:
`
<ul>
<li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)',
content:
`
<ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-10-28',
header: '0.9.4 (2020-10-29)',
content:
`
<ul>
<li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-05-20',
header: '0.9.3 (2020-05-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content:
`
<ul>
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content:
`
<ul>
<li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',

View File

@@ -32,7 +32,7 @@ const DialogProps = Vue.extend({
props: {
value: Boolean,
}
})
});
export default @Component({
})

View File

@@ -19,11 +19,12 @@ class Notify extends Vue {
iconColor = 'white',
message = '',
messageColor = 'black',
position = 'top-right',
} = opts;
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
return this.$q.notify({
position: 'top-right',
position,
color,
textColor: iconColor,
icon,
@@ -38,20 +39,20 @@ class Notify extends Vue {
});
}
success(message, caption) {
this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
success(message, caption, options) {
this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options));
}
warning(message, caption) {
this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
warning(message, caption, options) {
this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options));
}
error(message, caption) {
this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
error(message, caption, options) {
this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options));
}
info(message, caption) {
this.notify({color: 'info', icon: 'la la-bell', message, caption});
info(message, caption, options) {
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
}
}
//-----------------------------------------------------------------------------

View File

@@ -3,10 +3,10 @@
<slot></slot>
<!--------------------------------------------------->
<div v-show="type == 'alert'" class="dialog column bg-white no-wrap" style="min-height: 150px">
<div v-show="type == 'alert'" 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="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
@@ -16,7 +16,7 @@
</div>
</div>
<div class="col q-mx-md">
<div class="q-mx-md">
<div v-html="message"></div>
</div>
@@ -26,10 +26,10 @@
</div>
<!--------------------------------------------------->
<div v-show="type == 'confirm'" class="dialog column bg-white no-wrap" style="min-height: 150px">
<div v-show="type == 'confirm'" 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="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
@@ -39,7 +39,7 @@
</div>
</div>
<div class="col q-mx-md">
<div class="q-mx-md">
<div v-html="message"></div>
</div>
@@ -50,10 +50,10 @@
</div>
<!--------------------------------------------------->
<div v-show="type == 'prompt'" class="dialog column bg-white no-wrap" style="min-height: 250px">
<div v-show="type == 'prompt'" 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="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
@@ -63,7 +63,7 @@
</div>
</div>
<div class="col q-mx-md">
<div class="q-mx-md">
<div v-html="message"></div>
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
@@ -74,6 +74,34 @@
<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 == 'hotKey'" 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 flat round dense v-close-popup>
<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 class="q-my-md text-center">
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
<div>{{ hotKeyCode }}</div>
</div>
</div>
<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 class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
</div>
</div>
</q-dialog>
</template>
@@ -82,7 +110,7 @@
import Vue from 'vue';
import Component from 'vue-class-component';
//import * as utils from '../../share/utils';
import * as utils from '../../share/utils';
export default @Component({
watch: {
@@ -99,6 +127,8 @@ class StdDialog extends Vue {
inputValue = '';
error = '';
iconColor = '';
iconName = '';
hotKeyCode = '';
created() {
if (this.$root.addKeyHook) {
@@ -115,10 +145,21 @@ class StdDialog extends Vue {
this.inputValidator = null;
this.inputValue = '';
this.error = '';
this.showed = false;
this.iconColor = 'text-warning';
if (opts && opts.type) {
this.iconColor = `text-${opts.type}`;
if (opts && opts.color) {
this.iconColor = `text-${opts.color}`;
}
this.iconName = 'las la-exclamation-circle';
if (opts && opts.iconName) {
this.iconName = opts.iconName;
}
this.hotKeyCode = '';
if (opts && opts.hotKeyCode) {
this.hotKeyCode = opts.hotKeyCode;
}
}
@@ -127,6 +168,7 @@ class StdDialog extends Vue {
this.hideTrigger();
this.hideTrigger = null;
}
this.showed = false;
}
onShow() {
@@ -136,6 +178,7 @@ class StdDialog extends Vue {
this.validate(this.inputValue);
this.$refs.input.focus();
}
this.showed = true;
}
validate(value) {
@@ -158,6 +201,12 @@ class StdDialog extends Vue {
this.$refs.dialog.shake();
return;
}
if (this.type == 'hotKey' && this.hotKeyCode == '') {
this.$refs.dialog.shake();
return;
}
this.ok = true;
this.$refs.dialog.hide();
}
@@ -218,11 +267,49 @@ class StdDialog extends Vue {
});
}
getHotKey(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(this.hotKeyCode);
} else {
resolve(false);
}
};
this.type = 'hotKey';
this.active = true;
});
}
keyHook(event) {
if (this.active && event.code == 'Enter') {
this.okClick();
event.stopPropagation();
event.preventDefault();
if (this.active && this.showed) {
let handled = false;
if (this.type == 'hotKey') {
if (event.type == 'keydown') {
this.hotKeyCode = utils.keyEventToCode(event);
handled = true;
}
} else {
if (event.key == 'Enter') {
this.okClick();
handled = true;
}
if (event.key == 'Escape') {
this.$nextTick(() => {
this.$refs.dialog.hide();
});
handled = true;
}
}
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
}
}

View File

@@ -1,10 +1,11 @@
<template>
<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 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"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
<span class="header-text col"><slot name="header"></slot></span>
<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>
</div>
@@ -25,11 +26,15 @@ export default @Component({
width: { type: String, default: '100%' },
maxWidth: { type: String, default: '' },
topShift: { type: Number, default: 0 },
margin: '',
}
})
class Window extends Vue {
init() {
this.$nextTick(() => {
this.$refs.main.style.top = 0;
this.$refs.main.style.left = 0;
this.$refs.windowBox.style.height = this.height;
this.$refs.windowBox.style.width = this.width;
if (this.maxWidth)
@@ -39,6 +44,9 @@ class Window extends Vue {
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
if (this.margin)
this.$refs.window.style.margin = this.margin;
});
}

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html>
<html manifest="/app/manifest.appcache">
<html>
<head>
<title></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
<title></title>
<script src="/sw-register.js"></script>
</head>
<body>
<div id="app"></div>
<script src="https://yastatic.net/share2/share.js" async="async"></script>
</body>
</html>

View File

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

View File

@@ -30,6 +30,8 @@ import {QSelect} from 'quasar/src/components/select';
import {QColor} from 'quasar/src/components/color';
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
const components = {
//QLayout,
@@ -55,6 +57,8 @@ const components = {
QColor,
QPopupProxy,
QDialog,
QChip,
QTree
};
//directives
@@ -80,7 +84,6 @@ Vue.use(Quasar, { config, components, directives, plugins });
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
import '@quasar/extras/line-awesome/line-awesome.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'

View File

@@ -2,42 +2,41 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import _ from 'lodash';
//немедленная загрузка
import CardIndex from './components/CardIndex/CardIndex.vue';
//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
const Search = () => import('./components/CardIndex/Search/Search.vue');
const Card = () => import('./components/CardIndex/Card/Card.vue');
const Book = () => import('./components/CardIndex/Book/Book.vue');
const History = () => import('./components/CardIndex/History/History.vue');
//немедленная загрузка
//const Reader = () => import('./components/Reader/Reader.vue');
import Reader from './components/Reader/Reader.vue';
//import Reader from './components/Reader/Reader.vue';
const Reader = () => import('./components/Reader/Reader.vue');
const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
//const Forum = () => import('./components/Forum/Forum.vue');
const Income = () => import('./components/Income/Income.vue');
const Sources = () => import('./components/Sources/Sources.vue');
const Settings = () => import('./components/Settings/Settings.vue');
const Help = () => import('./components/Help/Help.vue');
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
const myRoutes = [
['/', null, null, '/cardindex'],
['/cardindex', CardIndex ],
['/cardindex~search', Search ],
['/cardindex~card', Card ],
['/cardindex~card/:authorId', Card ],
['/cardindex~book', Book ],
['/cardindex~book/:bookId', Book ],
['/cardindex~history', History ],
['/cardindex', CardIndex],
['/cardindex~search', Search],
['/cardindex~card', Card],
['/cardindex~card/:authorId', Card],
['/cardindex~book', Book],
['/cardindex~book/:bookId', Book],
['/cardindex~history', History],
['/reader', Reader ],
['/income', Income ],
['/sources', Sources ],
['/settings', Settings ],
['/help', Help ],
['*', null, null, '/cardindex' ],
['/reader', Reader],
['/external-libs', ExternalLibs],
['/income', Income],
['/sources', Sources],
['/settings', Settings],
['/help', Help],
['/404', NotFound404],
['*', null, null, '/cardindex'],
];
let routes = {};

View File

@@ -130,20 +130,53 @@ export function getObjDiff(oldObj, newObj) {
}
export function isObjDiff(diff) {
return (_.isObject(diff) && diff.__isDiff);
return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
}
export function isEmptyObjDiff(diff) {
return (!_.isObject(diff) || !diff.__isDiff ||
(!Object.keys(diff.change).length &&
!Object.keys(diff.add).length &&
!diff.del.length
return (!isObjDiff(diff) ||
!(Object.keys(diff.change).length ||
Object.keys(diff.add).length ||
diff.del.length
)
);
}
export function applyObjDiff(obj, diff, isAddChanged) {
const result = _.cloneDeep(obj);
export function isEmptyObjDiffDeep(diff, opts = {}) {
if (!isObjDiff(diff))
return true;
const {
isApplyChange = true,
isApplyAdd = true,
isApplyDel = true,
} = opts;
let notEmptyDeep = false;
const change = diff.change;
for (const key of Object.keys(change)) {
if (_.isObject(change[key]))
notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
else if (isApplyChange)
notEmptyDeep = true;
}
return !(
notEmptyDeep ||
(isApplyAdd && Object.keys(diff.add).length) ||
(isApplyDel && diff.del.length)
);
}
export function applyObjDiff(obj, diff, opts = {}) {
const {
isAddChanged = false,
isApplyChange = true,
isApplyAdd = true,
isApplyDel = true,
} = opts;
let result = _.cloneDeep(obj);
if (!diff.__isDiff)
return result;
@@ -151,21 +184,28 @@ export function applyObjDiff(obj, diff, isAddChanged) {
for (const key of Object.keys(change)) {
if (result.hasOwnProperty(key)) {
if (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], isAddChanged);
result[key] = applyObjDiff(result[key], change[key], opts);
} else {
result[key] = _.cloneDeep(change[key]);
if (isApplyChange)
result[key] = _.cloneDeep(change[key]);
}
} else if (isAddChanged) {
result[key] = _.cloneDeep(change[key]);
}
}
for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[key]);
if (isApplyAdd) {
for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[key]);
}
}
for (const key of diff.del) {
delete result[key];
if (isApplyDel && diff.del.length) {
for (const key of diff.del) {
delete result[key];
}
if (_.isArray(result))
result = result.filter(v => v);
}
return result;
@@ -202,4 +242,38 @@ export function escapeXml(str) {
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
;
}
}
export function keyEventToCode(event) {
let result = [];
let code = event.code;
const modCode = code.substring(0, 3);
if (event.metaKey && modCode != 'Met')
result.push('Meta');
if (event.ctrlKey && modCode != 'Con')
result.push('Ctrl');
if (event.shiftKey && modCode != 'Shi')
result.push('Shift');
if (event.altKey && modCode != 'Alt')
result.push('Alt');
if (modCode == 'Dig') {
code = code.substring(5, 6);
} else if (modCode == 'Key') {
code = code.substring(3, 4);
}
result.push(code);
return result.join('+');
}
export function userHotKeysObjectSwap(userHotKeys) {
let result = {};
for (const [name, codes] of Object.entries(userHotKeys)) {
for (const code of codes) {
result[code] = name;
}
}
return result;
}

View File

@@ -12,11 +12,11 @@ Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store(Object.assign({}, root, {
modules: {
uistate,
config,
reader,
},
strict: debug,
plugins: [createPersistedState()]
modules: {
uistate,
config,
reader,
},
strict: debug,
plugins: [createPersistedState()]
}));

View File

@@ -1,15 +1,81 @@
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
import * as utils from '../../share/utils';
const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки',
'settings': 'Настроить',
'undoAction': 'Действие назад',
'redoAction': 'Действие вперед',
'fullScreen': 'На весь экран',
'scrolling': 'Плавный скроллинг',
'stopScrolling': '',
'setPosition': 'Установить позицию',
'search': 'Найти в тексте',
'copyText': 'Скопировать текст со страницы',
'splitToPara': 'Обновить с разбиением на параграфы',
'refresh': 'Принудительно обновить книгу',
'offlineMode': 'Автономный режим (без интернета)',
'libs': 'Библиотека',
'recentBooks': 'Открыть недавние',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
'bookBegin': 'В начало книги',
'bookEnd': 'В конец книги',
'pageBack': 'Страницу назад',
'pageForward': 'Страницу вперед',
'lineBack': 'Строчку назад',
'lineForward': 'Строчку вперед',
'incFontSize': 'Увеличить размер шрифта',
'decFontSize': 'Уменьшить размер шрифта',
'scrollingSpeedUp': 'Увеличить скорость скроллинга',
'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
};
//readerActions[name]
const toolButtons = [
{name: 'undoAction', show: true, text: 'Действие назад'},
{name: 'redoAction', show: true, text: 'Действие вперед'},
{name: 'fullScreen', show: true, text: 'На весь экран'},
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
{name: 'setPosition', show: true, text: 'На страницу'},
{name: 'search', show: true, text: 'Найти в тексте'},
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
{name: 'undoAction', show: true},
{name: 'redoAction', show: true},
{name: 'fullScreen', show: true},
{name: 'scrolling', show: false},
{name: 'setPosition', show: true},
{name: 'search', show: true},
{name: 'copyText', show: false},
{name: 'splitToPara', show: false},
{name: 'refresh', show: true},
{name: 'libs', show: true},
{name: 'recentBooks', show: true},
{name: 'offlineMode', show: false},
];
//readerActions[name]
const hotKeys = [
{name: 'help', codes: ['F1', 'H']},
{name: 'loader', codes: ['Escape']},
{name: 'settings', codes: ['S']},
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
{name: 'fullScreen', codes: ['Enter', 'Backquote', 'F']},
{name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'splitToPara', codes: ['Shift+R']},
{name: 'refresh', codes: ['R']},
{name: 'offlineMode', codes: ['O']},
{name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']},
{name: 'switchToolbar', codes: ['Tab', 'Q']},
{name: 'bookBegin', codes: ['Home']},
{name: 'bookEnd', codes: ['End']},
{name: 'pageBack', codes: ['PageUp', 'ArrowLeft', 'Backspace', 'Shift+Space']},
{name: 'pageForward', codes: ['PageDown', 'ArrowRight', 'Space']},
{name: 'lineBack', codes: ['ArrowUp']},
{name: 'lineForward', codes: ['ArrowDown']},
{name: 'incFontSize', codes: ['A']},
{name: 'decFontSize', codes: ['Shift+A']},
{name: 'scrollingSpeedUp', codes: ['Shift+ArrowDown']},
{name: 'scrollingSpeedDown', codes: ['Shift+ArrowUp']},
];
const fonts = [
@@ -136,6 +202,7 @@ const webFonts = [
];
//----------------------------------------------------------------------------------------------------------
const settingDefaults = {
textColor: '#000000',
backgroundColor: '#EBE2C9',
@@ -160,6 +227,7 @@ const settingDefaults = {
statusBarTop: false,// top, bottom
statusBarHeight: 19,// px
statusBarColorAlpha: 0.4,
statusBarClickOpen: true,
scrollingDelay: 3000,// замедление, ms
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
@@ -183,10 +251,12 @@ const settingDefaults = {
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
showLiberamaTopDialog2020: true,
enableSitesFilter: true,
fontShifts: {},
showToolButton: {},
userHotKeys: {},
};
for (const font of fonts)
@@ -195,6 +265,46 @@ for (const font of webFonts)
settingDefaults.fontShifts[font.name] = font.fontVertShift;
for (const button of toolButtons)
settingDefaults.showToolButton[button.name] = button.show;
for (const hotKey of hotKeys)
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults);
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
}
return false;
}
const libsDefaults = {
startLink: 'http://flibusta.is',
comment: 'Флибуста | Книжное братство',
closeAfterSubmit: false,
openInFrameOnEnter: false,
openInFrameOnAdd: false,
groups: [
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
{l: 'https://flibs.in', c: 'Flibs'},
]},
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
]},
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},
]
};
// initial state
const state = {
@@ -209,6 +319,8 @@ const state = {
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settingsRev: {},
libs: Object.assign({}, libsDefaults),
libsRev: 0,
};
// getters
@@ -247,18 +359,34 @@ const mutations = {
state.currentProfile = value;
},
setSettings(state, value) {
state.settings = Object.assign({}, state.settings, value);
const newSettings = Object.assign({}, state.settings, value);
const added = addDefaultsToSettings(newSettings);
if (added) {
state.settings = added;
} else {
state.settings = newSettings;
}
},
setSettingsRev(state, value) {
state.settingsRev = Object.assign({}, state.settingsRev, value);
},
setLibs(state, value) {
state.libs = value;
},
setLibsRev(state, value) {
state.libsRev = value;
},
};
export default {
readerActions,
toolButtons,
hotKeys,
fonts,
webFonts,
settingDefaults,
addDefaultsToSettings,
libsDefaults,
namespaced: true,
state,

85
docs/beta/beta.liberama Normal file
View File

@@ -0,0 +1,85 @@
server {
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/beta.liberama.top/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/beta.liberama.top/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.liberama.top;
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 /api {
proxy_pass http://127.0.0.1:34082;
}
location /ws {
proxy_pass http://127.0.0.1:34082;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/beta.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}
server {
listen 80;
server_name beta.liberama.top;
return 301 https://$host$request_uri;
}
server {
listen 80;
server_name b.beta.liberama.top;
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 /api {
proxy_pass http://127.0.0.1:34082;
}
location /ws {
proxy_pass http://127.0.0.1:34082;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/beta.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}

48
docs/beta/beta.omnireader Normal file
View File

@@ -0,0 +1,48 @@
server {
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/beta.omnireader.ru/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/beta.omnireader.ru/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.omnireader.ru;
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 /api {
proxy_pass http://127.0.0.1:34081;
}
location /ws {
proxy_pass http://127.0.0.1:34081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/beta.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}
server {
listen 80;
server_name beta.omnireader.ru;
return 301 https://$host$request_uri;
}

4
docs/beta/deploy.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
npm run build:linux
sudo -u www-data cp -r ../../dist/linux/* /home/beta.liberama

3
docs/beta/run_server.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
sudo -H -u www-data /home/beta.liberama/liberama

128
docs/liberama.top/liberama Normal file
View File

@@ -0,0 +1,128 @@
server {
server_name _;
listen 80 default_server;
listen 443 ssl default_server;
#openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
return 403;
}
server {
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/liberama.top/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/liberama.top/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name liberama.top;
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 /api {
proxy_pass http://127.0.0.1:55081;
}
location /ws {
proxy_pass http://127.0.0.1:55081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}
server {
listen 80;
server_name liberama.top;
return 301 https://$host$request_uri;
}
server {
listen 80;
server_name b.liberama.top;
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 /api {
proxy_pass http://127.0.0.1:55081;
}
location /ws {
proxy_pass http://127.0.0.1:55081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}
server {
listen 23480;
server_name flibusta_proxy;
valid_referers liberama.top b.liberama.top;
if ($invalid_referer) {
return 403;
}
location / {
proxy_pass http://flibusta.is;
proxy_redirect http://static.flibusta.is:443 http://b.liberama.top:23481;
}
}
server {
listen 23481;
server_name flibusta_proxy_static;
valid_referers liberama.top b.liberama.top;
if ($invalid_referer) {
return 403;
}
location / {
proxy_pass http://static.flibusta.is:443;
proxy_set_header Referer "";
}
}

View File

@@ -39,11 +39,11 @@ sudo apt install poppler-utils
```
### nginx, server config
Для своего домена необходимо будет подправить docs/omnireader/omnireader.
Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader.
Можно также настроить сервер для HTTP, без SSL.
```
sudo apt install nginx
sudo cp docs/omnireader/omnireader /etc/nginx/sites-available/omnireader
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
sudo rm /etc/nginx/sites-enabled/default
sudo service nginx reload
@@ -59,14 +59,20 @@ sudo service php7.2-fpm restart
sudo mkdir /home/oldreader
sudo chown www-data.www-data /home/oldreader
sudo -u www-data cp -r docs/omnireader/old/* /home/oldreader
sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
```
## Запуск по крону
```
* * * * * /root/liberama/docs/omnireader.ru/cron_server.sh
```
## Деплой и запуск
```
cd docs/omnireader
cd docs/omnireader.ru
./stop_server.sh
./deploy.sh
./run_server.sh
./start_server.sh
```
После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
@@ -81,4 +87,4 @@ cd docs/omnireader
}
]
```
и перезапустить `run_server.sh`
и перезапустить сервер

View File

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

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 227 B

View File

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 246 B

View File

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -30,7 +30,7 @@ server {
root /home/liberama/public;
location /tmp {
add_header Content-Type text/xml;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}

View File

@@ -25,7 +25,7 @@ server {
root /home/liberama/public;
location /tmp {
add_header Content-Type text/xml;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}

View File

@@ -0,0 +1,4 @@
#!/bin/bash
sudo -H -u www-data /home/liberama/liberama &
sudo service cron start

View File

@@ -0,0 +1,4 @@
#!/bin/bash
sudo service cron stop
sudo killall liberama

View File

@@ -1,11 +0,0 @@
#!/bin/bash
sudo -H -u www-data bash -c "\
while true; do\
trap '' 2;\
cd /var/www;\
/home/liberama/liberama;\
trap 2;\
echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
sleep 5;\
done;"

3960
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.9.0",
"version": "0.9.6",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -22,7 +22,7 @@
},
"devDependencies": {
"babel-core": "^6.22.1",
"babel-eslint": "^10.0.3",
"babel-eslint": "^10.1.0",
"babel-loader": "^7.1.1",
"babel-plugin-component": "^1.1.1",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
@@ -32,7 +32,6 @@
"clean-webpack-plugin": "^1.0.1",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^1.0.0",
"element-theme-chalk": "^2.12.0",
"eslint": "^5.16.0",
"eslint-plugin-html": "^5.0.5",
"eslint-plugin-node": "^8.0.0",
@@ -41,27 +40,26 @@
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"pkg": "^4.4.2",
"pkg": "^4.4.4",
"terser-webpack-plugin": "^1.4.1",
"url-loader": "^1.1.2",
"vue-class-component": "^6.3.2",
"vue-loader": "^15.7.1",
"vue-loader": "^15.9.0",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"webpack-dev-middleware": "^3.7.1",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-dev-middleware": "^3.7.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.2"
"webpack-merge": "^4.2.2",
"workbox-webpack-plugin": "^5.1.3"
},
"dependencies": {
"@quasar/extras": "^1.5.0",
"appcache-webpack-plugin": "^1.4.0",
"@quasar/extras": "^1.5.2",
"axios": "^0.18.1",
"base-x": "^3.0.6",
"base-x": "^3.0.8",
"chardet": "^0.7.0",
"compression": "^1.7.4",
"element-ui": "^2.12.0",
"express": "^4.17.1",
"fg-loadcss": "^2.1.0",
"fs-extra": "^7.0.1",
@@ -72,21 +70,22 @@
"lodash": "^4.17.15",
"minimist": "^1.2.0",
"multer": "^1.4.2",
"pako": "^1.0.10",
"pako": "^1.0.11",
"path-browserify": "^1.0.0",
"quasar": "^1.8.5",
"quasar": "^1.14.3",
"safe-buffer": "^5.2.0",
"sjcl": "^1.0.8",
"sql-template-strings": "^2.2.2",
"sqlite": "^3.0.3",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"vue": "github:paulkamer/vue#fix_palemoon_clickhandlers_dist",
"vue-router": "^3.1.3",
"vuex": "^3.1.1",
"vuex-persistedstate": "^2.5.4",
"webdav": "^2.10.1",
"vue": "github:bookpauk/vue",
"vue-router": "^3.1.6",
"vue-sanitize": "^0.2.1",
"vuex": "^3.1.2",
"vuex-persistedstate": "^2.7.1",
"webdav": "^2.10.2",
"ws": "^7.2.1",
"zip-stream": "^2.1.2"
"zip-stream": "^2.1.3"
}
}

View File

@@ -19,7 +19,9 @@ class ReaderController extends BaseController {
throw new Error(`key 'url' is empty`);
const workerId = this.readerWorker.loadBookUrl({
url: request.url,
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true)
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
isText: (request.hasOwnProperty('isText') ? request.isText : false),
});
const state = this.workerState.getState(workerId);
return (state ? state : {});

View File

@@ -119,14 +119,19 @@ class FileDecompressor {
try {
return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize,
limitFileCount: 1000
});
limitFileCount: 1000,
decodeEntryNameCallback: (nameRaw) => {
return utils.bufferRemoveZeroes(nameRaw);
}
}
);
} catch (e) {
fs.emptyDir(outputDir);
return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize,
limitFileCount: 1000,
decodeEntryNameCallback: (nameRaw) => {
nameRaw = utils.bufferRemoveZeroes(nameRaw);
const enc = textUtils.getEncodingLite(nameRaw);
if (enc.indexOf('ISO-8859') < 0) {
return iconv.decode(nameRaw, enc);
@@ -241,27 +246,29 @@ class FileDecompressor {
});
}
async gzipFileIfNotExists(filename, outDir) {
async gzipFileIfNotExists(filename, outDir, isMaxCompression) {
const hash = await utils.getFileHash(filename, 'sha256', 'hex');
const outFilename = `${outDir}/${hash}`;
if (!await fs.pathExists(outFilename)) {
await this.gzipFile(filename, outFilename, 1);
await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
// переупакуем через некоторое время на максималках
const filenameCopy = `${filename}.copy`;
await fs.copy(filename, filenameCopy);
// переупакуем через некоторое время на максималках, если упаковали плохо
if (!isMaxCompression) {
const filenameCopy = `${filename}.copy`;
await fs.copy(filename, filenameCopy);
(async() => {
await utils.sleep(5000);
const filenameGZ = `${filename}.gz`;
await this.gzipFile(filenameCopy, filenameGZ, 9);
(async() => {
await utils.sleep(5000);
const filenameGZ = `${filename}.gz`;
await this.gzipFile(filenameCopy, filenameGZ, 9);
await fs.move(filenameGZ, outFilename, {overwrite: true});
await fs.move(filenameGZ, outFilename, {overwrite: true});
await fs.remove(filenameCopy);
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
await fs.remove(filenameCopy);
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
}
} else {
await utils.touchFile(outFilename);
}

View File

@@ -4,7 +4,7 @@ const signatures = require('./signatures.json');
class FileDetector {
detectFile(filename) {
return new Promise((resolve, reject) => {
this.fromFile(filename, 2000, (err, result) => {
this.fromFile(filename, 10000, (err, result) => {
if (err) reject(err);
resolve(result);
});

View File

@@ -653,40 +653,6 @@
]
},
{
"type": "svg",
"ext": "svg",
"mime": "image/svg+xml",
"rules": [
{ "type": "contains", "bytes": "3c737667" }
]
},
{
"type": "html",
"ext": "html",
"mime": "text/html",
"rules": [
{ "type": "or", "rules":
[
{ "type": "contains", "bytes": "3c68746d6c" },
{ "type": "contains", "bytes": "3c00680074006d006c00" },
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
]
}
]
},
{
"type": "docx",
"ext": "docx",
@@ -708,7 +674,9 @@
{ "type": "or", "rules":
[
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" }
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" }
]
}
]
@@ -721,6 +689,53 @@
"rules": [
{ "type": "equal", "start": 64, "end": 68, "bytes": "4d4f4249" }
]
}
},
{
"type": "djvu",
"ext": "djvu",
"mime": "image/vnd.djvu",
"rules": [
{ "type": "equal", "start": 0, "end": 8, "bytes": "41542654464f524d" },
{ "type": "equal", "start": 12, "end": 15, "bytes": "444a56" }
]
},
{
"type": "html",
"ext": "html",
"mime": "text/html",
"rules": [
{ "type": "or", "rules":
[
{ "type": "contains", "bytes": "3c68746d6c" },
{ "type": "contains", "bytes": "3c00680074006d006c00" },
{ "type": "contains", "bytes": "3c48544d4c" },
{ "type": "contains", "bytes": "3c00480054004d004c00" },
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
{ "type": "equal", "end": 9, "bytes": "3c21444f4354595045" },
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
]
}
]
},
{
"type": "svg",
"ext": "svg",
"mime": "image/svg+xml",
"rules": [
{ "type": "contains", "bytes": "3c737667" }
]
}
]

View File

@@ -0,0 +1 @@
test

View File

@@ -200,7 +200,7 @@ class ConvertHtml extends ConvertBase {
titleInfo['book-title'] = title;
//подозрение на чистый текст, надо разбить на параграфы
if (isText || pars.length < buf.length/2000) {
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
let total = 0;
let count = 1;
for (let i = 0; i < spaceCounter.length; i++) {

View File

@@ -54,6 +54,7 @@ class ReaderWorker {
let decompDir = '';
let downloadedFilename = '';
let isUploaded = false;
let isRestored = false;
let convertFilename = '';
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
@@ -88,9 +89,17 @@ class ReaderWorker {
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
await fs.writeFile(downloadedFilename, downdata);
} else {//uploaded file
downloadedFilename = `${this.config.uploadDir}/${url.substr(7)}`;
if (!await fs.pathExists(downloadedFilename))
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
const fileHash = url.substr(7);
downloadedFilename = `${this.config.uploadDir}/${fileHash}`;
if (!await fs.pathExists(downloadedFilename)) {
//если удалено из upload, попробуем восстановить из удаленного хранилища
try {
downloadedFilename = await this.restoreRemoteFile(fileHash);
isRestored = true;
} catch(e) {
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
}
}
await utils.touchFile(downloadedFilename);
isUploaded = true;
}
@@ -146,6 +155,20 @@ class ReaderWorker {
})();
}
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
(async() => {
await utils.sleep(30*1000);
try {
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
} catch (e) {
log(LM_ERR, e.stack);
}
})();
}
} catch (e) {
log(LM_ERR, e.stack);
if (e.message == 'abort')
@@ -188,6 +211,24 @@ class ReaderWorker {
return `file://${hash}`;
}
async restoreRemoteFile(filename) {
const basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`;
if (!await fs.pathExists(targetName)) {
let found = false;
if (this.remoteWebDavStorage) {
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
}
if (!found) {
throw new Error('404 Файл не найден');
}
}
return targetName;
}
restoreCachedFile(filename) {
const workerId = this.workerState.generateWorkerId();
const wState = this.workerState.getControl(workerId);
@@ -197,21 +238,10 @@ class ReaderWorker {
try {
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
const basename = path.basename(filename);
const targetName = `${this.config.tempPublicDir}/${basename}`;
if (!await fs.pathExists(targetName)) {
let found = false;
if (this.remoteWebDavStorage) {
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
}
if (!found) {
throw new Error('404 Файл не найден');
}
}
const targetName = await this.restoreRemoteFile(filename);
const stat = await fs.stat(targetName);
const basename = path.basename(filename);
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
} catch (e) {
if (e.message.indexOf('404') < 0)

View File

@@ -766,7 +766,7 @@ ZipEntry.prototype.readDataHeader = function(data) {
};
ZipEntry.prototype.read = function(data, offset) {
this.nameRaw = data.slice(offset, offset += this.fnameLen);
this.nameRaw = Buffer.from(data.slice(offset, offset += this.fnameLen));
this.name = this.nameRaw.toString();
var lastChar = data[offset - 1];
this.isDirectory = (lastChar == 47) || (lastChar == 92);

View File

@@ -93,6 +93,12 @@ function parseSync(xstr, options) {
}
tag = tag.toLowerCase();
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
if (!cutCounter)
cutTag = tag;
cutCounter++;
}
let endTag = (singleTag ? tag : '');
if (tag === '' || tag[0] !== '/') {
_onStartNode(tag, tail, singleTag, cutCounter, cutTag);
@@ -103,12 +109,6 @@ function parseSync(xstr, options) {
if (endTag)
_onEndNode(endTag, tail, singleTag, cutCounter, cutTag);
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
if (!cutCounter)
cutTag = tag;
cutCounter++;
}
if (cutTag === endTag) {
cutCounter = (cutCounter > 0 ? cutCounter - 1 : 0);
if (!cutCounter)
@@ -125,9 +125,9 @@ function parseSync(xstr, options) {
if (i < len) {
if (inCdata) {
_onCdata(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
_onCdata(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
} else if (inComment) {
_onComment(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
_onComment(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
} else {
_onTextNode(xstr.substr(i, len - i), cutCounter, cutTag);
}
@@ -233,6 +233,12 @@ async function parse(xstr, options) {
}
tag = tag.toLowerCase();
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
if (!cutCounter)
cutTag = tag;
cutCounter++;
}
let endTag = (singleTag ? tag : '');
if (tag === '' || tag[0] !== '/') {
await _onStartNode(tag, tail, singleTag, cutCounter, cutTag);
@@ -243,12 +249,6 @@ async function parse(xstr, options) {
if (endTag)
await _onEndNode(endTag, tail, singleTag, cutCounter, cutTag);
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
if (!cutCounter)
cutTag = tag;
cutCounter++;
}
if (cutTag === endTag) {
cutCounter = (cutCounter > 0 ? cutCounter - 1 : 0);
if (!cutCounter)
@@ -265,9 +265,9 @@ async function parse(xstr, options) {
if (i < len) {
if (inCdata) {
await _onCdata(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
await _onCdata(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
} else if (inComment) {
await _onComment(xstr.substr(leftData, len - leftData), cutCounter, cutTag);
await _onComment(xstr.substr(leftData + 1, len - leftData - 1), cutCounter, cutTag);
} else {
await _onTextNode(xstr.substr(i, len - i), cutCounter, cutTag);
}

View File

@@ -14,6 +14,14 @@ function fromBase36(data) {
return bs36.decode(data);
}
function bufferRemoveZeroes(buf) {
const i = buf.indexOf(0);
if (i >= 0) {
return buf.slice(0, i);
}
return buf;
}
function getFileHash(filename, hashName, enc) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(hashName);
@@ -86,6 +94,7 @@ function spawnProcess(cmd, opts) {
module.exports = {
toBase36,
fromBase36,
bufferRemoveZeroes,
getFileHash,
sleep,
randomHexString,

View File

@@ -72,7 +72,7 @@ async function main() {
maxAge: '30d',
setHeaders: (res, filePath) => {
if (path.basename(path.dirname(filePath)) == 'tmp') {
res.set('Content-Type', 'text/xml');
res.set('Content-Type', 'application/xml');
res.set('Content-Encoding', 'gzip');
}
}