Compare commits

...

334 Commits

Author SHA1 Message Date
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
Book Pauk
22e5d38ef5 Merge branch 'release/0.9.0' 2020-02-26 17:48:37 +07:00
Book Pauk
5819ccb528 Версия 0.9.0 2020-02-26 17:47:26 +07:00
Book Pauk
42a2fd77cf Удаление element-ui 2020-02-26 17:44:03 +07:00
Book Pauk
ab93a8b0b3 Merge branch 'feature/quasar' into develop 2020-02-26 17:31:50 +07:00
Book Pauk
84437eafa6 Поправки для смартфонов 2020-02-26 17:31:08 +07:00
Book Pauk
0107d848e0 Мелкая поправка 2020-02-26 16:58:32 +07:00
Book Pauk
5eeac96a0d Поправки цветов 2020-02-26 16:52:38 +07:00
Book Pauk
9351c115be Переход на quasar 2020-02-26 16:44:01 +07:00
Book Pauk
f95a11096c Переход на quasar 2020-02-26 16:40:26 +07:00
Book Pauk
4203d179e6 Переход на quasar 2020-02-26 14:34:30 +07:00
Book Pauk
78dfc9cb1c Переход на quasar 2020-02-26 14:32:47 +07:00
Book Pauk
0bef307d77 Переход на quasar 2020-02-26 14:16:17 +07:00
Book Pauk
b0da806f7a Переход на quasar 2020-02-26 13:45:03 +07:00
Book Pauk
badecd1d81 Переход на quasar 2020-02-26 13:42:22 +07:00
Book Pauk
6418e8ee30 Переход на quasar 2020-02-26 13:40:06 +07:00
Book Pauk
09115c9658 Переход на quasar 2020-02-26 13:26:15 +07:00
Book Pauk
74e3866bd7 Поправки цветов таба 2020-02-26 13:08:25 +07:00
Book Pauk
408de78c13 Переход на quasar 2020-02-25 22:02:18 +07:00
Book Pauk
c0451c18b3 Переход на quasar 2020-02-25 21:59:22 +07:00
Book Pauk
f303d26c1e Переход на quasar 2020-02-25 21:52:55 +07:00
Book Pauk
1b58a34859 Переход на quasar 2020-02-25 21:51:11 +07:00
Book Pauk
82ea416e67 Переход на quasar 2020-02-25 21:46:30 +07:00
Book Pauk
efd4fbad70 Переход на quasar 2020-02-25 21:07:12 +07:00
Book Pauk
01bd15121b Мелкий рефакторинг 2020-02-25 20:46:58 +07:00
Book Pauk
a9c2495349 Переход на quasar 2020-02-25 20:42:34 +07:00
Book Pauk
e7c50b50ed Переход на quasar 2020-02-25 17:37:41 +07:00
Book Pauk
6e25b289d2 Переход на quasar 2020-02-24 13:09:10 +07:00
Book Pauk
157267eaf7 Мелкая поправка 2020-02-24 12:56:03 +07:00
Book Pauk
a317f9137a Переход на quasar 2020-02-24 12:32:58 +07:00
Book Pauk
5dad3d22ea Переход на quasar 2020-02-24 12:29:47 +07:00
Book Pauk
be85df456b Переход на quasar 2020-02-24 12:21:52 +07:00
Book Pauk
2e172a08c7 Доработка NumInput 2020-02-20 21:06:21 +07:00
Book Pauk
bb1069ca60 Переход на quasar 2020-02-20 18:45:31 +07:00
Book Pauk
d8141a1628 Переход на quasar 2020-02-20 18:30:40 +07:00
Book Pauk
de9f7c4baf Переход на quasar 2020-02-20 18:24:00 +07:00
Book Pauk
fa9b3116f1 Переход на quasar 2020-02-20 18:20:59 +07:00
Book Pauk
dcf9d52961 Переход на quasar 2020-02-20 18:00:03 +07:00
Book Pauk
1da93e2cc7 Доработка NumInput 2020-02-20 16:46:58 +07:00
Book Pauk
1d1bab988e Переход на quasar 2020-02-20 16:44:28 +07:00
Book Pauk
dcc6ad3af3 Доработка NumInput 2020-02-19 15:11:54 +07:00
Book Pauk
d57f266789 Доработка NumInput 2020-02-19 15:11:15 +07:00
Book Pauk
c3395e1eff Переход на quasar 2020-02-19 11:43:29 +07:00
Book Pauk
ca59ec2dbe Переход на quasar 2020-02-18 21:06:44 +07:00
Book Pauk
79788125f3 Убрал дебаг 2020-02-18 20:52:42 +07:00
Book Pauk
2154f20fa4 Мелкая поправка 2020-02-18 20:52:12 +07:00
Book Pauk
afe40b6a89 Доработка NumInput 2020-02-18 20:52:00 +07:00
Book Pauk
ba4b3bd6b8 Доработки NumInput 2020-02-18 13:49:55 +07:00
Book Pauk
e423b5d745 Переход на quasar 2020-02-18 13:29:15 +07:00
Book Pauk
6de8eca7ea Переход на quasar 2020-02-18 12:46:18 +07:00
Book Pauk
9d68cfcaf0 Улучшена загрузка внешних шрифтов 2020-02-17 11:53:03 +07:00
Book Pauk
225de11e6a Переход на quasar 2020-02-17 11:26:05 +07:00
Book Pauk
916581bbd0 Поправлен баг 2020-02-16 11:30:48 +07:00
Book Pauk
1cbb35840f Переход на quasar 2020-02-15 20:58:17 +07:00
Book Pauk
7a1d769e39 Мелкие поправки 2020-02-15 20:58:07 +07:00
Book Pauk
8254bf934c Переход на quasar 2020-02-14 21:24:05 +07:00
Book Pauk
5e2f20542f Переход на quasar 2020-02-14 21:09:49 +07:00
Book Pauk
551a707ee4 Переход на quasar 2020-02-14 19:32:00 +07:00
Book Pauk
024b15b4f9 Мелкие поправки 2020-02-14 12:53:15 +07:00
Book Pauk
1935df4143 Merge branch 'develop' into feature/quasar 2020-02-14 11:51:50 +07:00
Book Pauk
3f99f90076 Merge tag '0.8.4-1' into develop
0.8.4-1
2020-02-14 11:46:37 +07:00
Book Pauk
53cb445dde Merge branch 'release/0.8.4-1' 2020-02-14 11:46:28 +07:00
Book Pauk
6e46947220 Исправлен баг 2020-02-14 11:44:39 +07:00
Book Pauk
9b65e1671b Переход на quasar 2020-02-14 11:35:35 +07:00
Book Pauk
d5c741db35 Переход на quasar 2020-02-12 16:05:31 +07:00
Book Pauk
11e0780b6e Переход на quasar 2020-02-12 16:03:42 +07:00
Book Pauk
f153541570 Переход на quasar 2020-02-11 15:17:47 +07:00
Book Pauk
f066af88e7 Поправка формата заголовка 2020-02-11 15:05:43 +07:00
Book Pauk
97e1eef799 Поправлено дефолтное значение для pageChangeAnimation 2020-02-11 15:00:10 +07:00
Book Pauk
1bcd902817 Закрыта дыра безопасности 2020-02-11 13:02:43 +07:00
Book Pauk
2484568b21 Доработка общения по вебсокету 2020-02-11 11:16:32 +07:00
Book Pauk
085cc47ea5 Переход на quasar 2020-02-10 17:13:12 +07:00
Book Pauk
aac36a88f3 Переход на quasar 2020-02-10 16:39:14 +07:00
Book Pauk
1f2ebc82b7 Поправлен мелкий баг 2020-02-10 13:56:11 +07:00
Book Pauk
9781949064 Переход на quasar 2020-02-09 22:06:57 +07:00
Book Pauk
b06ef3781a Переход на quasar 2020-02-09 21:22:18 +07:00
Book Pauk
b32213cb7b Небольшие доработки 2020-02-09 17:11:39 +07:00
Book Pauk
ac4c7d2421 Переход на quasar 2020-02-09 17:11:11 +07:00
Book Pauk
824a49b80f Переход на quasar 2020-02-09 16:49:08 +07:00
Book Pauk
13efd50d80 Добавлен универсальный includer для включения файлов в исходник 2020-02-09 16:43:36 +07:00
Book Pauk
6fb091d20f Мелкие доработки 2020-02-09 11:13:35 +07:00
Book Pauk
518ab85cae Небольшие доработки 2020-02-08 20:50:47 +07:00
Book Pauk
f5124ad8b5 Мелкие поправки 2020-02-08 20:24:15 +07:00
Book Pauk
6f80900aa8 Добавлена анимация слеша на страницу 2020-02-08 20:12:55 +07:00
Book Pauk
06b80e9281 Поправлены размеры иконок на кнопках 2020-02-08 11:59:59 +07:00
Book Pauk
51b39d9365 Мелкие поправки 2020-02-07 19:59:47 +07:00
Book Pauk
f7d2d8fc95 Мелкие поправки 2020-02-07 19:57:53 +07:00
Book Pauk
f34fb94c1a Убрал лишнее 2020-02-07 19:52:42 +07:00
Book Pauk
3107224e50 Переход на quasar 2020-02-07 19:51:23 +07:00
Book Pauk
e1c481c534 Поправлена сортировка 2020-02-07 19:26:00 +07:00
Book Pauk
945a2dd3eb Переход на quasar 2020-02-07 18:56:51 +07:00
Book Pauk
e318945eb1 Переход на quasar 2020-02-07 16:18:32 +07:00
Book Pauk
926709568d Merge branch 'develop' into feature/quasar 2020-02-06 22:04:31 +07:00
Book Pauk
da040e799c Merge tag '0.8.4' into develop
0.8.4
2020-02-06 21:55:56 +07:00
Book Pauk
694976cb6e Merge branch 'release/0.8.4' 2020-02-06 21:55:48 +07:00
Book Pauk
3f7bd1846a Версия 0.8.4 2020-02-06 21:50:58 +07:00
Book Pauk
714898b4c3 Добавлен paypal-адрес для пожертвований 2020-02-06 21:49:20 +07:00
Book Pauk
4efc9b6990 Merge tag '0.8.3-4' into develop
0.8.3-4
2020-02-06 21:07:35 +07:00
Book Pauk
73c3beaff1 Merge branch 'release/0.8.3-4' 2020-02-06 21:07:28 +07:00
Book Pauk
a6bdccd4ef Доработки конвертирования из буфера обмена 2020-02-06 21:04:40 +07:00
Book Pauk
8007991e7d Merge tag '0.8.3-3' into develop
0.8.3-3
2020-02-06 20:27:29 +07:00
Book Pauk
0e5d1ed1c3 Merge branch 'release/0.8.3-3' 2020-02-06 20:27:20 +07:00
Book Pauk
91dc2f4f71 Поправки логирования 2020-02-06 20:25:49 +07:00
Book Pauk
950bab3023 Добавлено декодирование имен файлов при распаковке Zip-архива в случае,
если кодировка имени не дает создать файл на диске
2020-02-06 20:20:29 +07:00
Book Pauk
29082a10e6 Рефакторинг 2020-02-06 20:13:33 +07:00
Book Pauk
65c1227d88 Удален node-stream-zip, т.к. в него внесены ручные правки 2020-02-06 20:12:01 +07:00
Book Pauk
5d121a68cf Поправки скриптов деплоя и запуска, добавлен авторестарт при падении сервера 2020-02-06 16:40:13 +07:00
Book Pauk
ad07d2b8b1 Переход на quasar 2020-02-05 21:16:05 +07:00
Book Pauk
c5aef78085 Поправки иконки 2020-02-05 19:46:48 +07:00
Book Pauk
522ebc8aa2 К предыдущему 2020-02-05 15:36:24 +07:00
Book Pauk
199b3761b5 Мелкая поправка 2020-02-05 15:34:57 +07:00
Book Pauk
daf7b45e45 Поправка иконки 2020-02-05 15:31:24 +07:00
Book Pauk
fc71b953c7 update quasar 2020-02-05 15:28:09 +07:00
Book Pauk
74ccd4a001 Переход на иконки line-awesome 2020-02-05 15:27:26 +07:00
Book Pauk
3c09f6ca55 Переход на quasar 2020-02-05 14:21:50 +07:00
Book Pauk
c7dbe8599d Поправки вида слайдера 2020-02-05 14:20:46 +07:00
Book Pauk
ca036b6676 Поправки копирования в буфер обмена, поправки текста 2020-02-04 19:51:19 +07:00
Book Pauk
5ae87c8e03 Рефакторинг 2020-02-04 19:45:06 +07:00
Book Pauk
9774fc4f65 Небольшие доработки 2020-02-04 19:17:43 +07:00
Book Pauk
d0891fb652 Поправлен баг вычисления rootRoute 2020-02-04 16:29:10 +07:00
Book Pauk
e388e2a1c7 Переименование 2020-02-04 16:16:31 +07:00
Book Pauk
d9ab354338 Небольшая поправка 2020-02-04 14:36:45 +07:00
Book Pauk
9ea0a0e214 К предыдущему 2020-02-04 13:39:46 +07:00
Book Pauk
131ddf0355 Поправил цвет 2020-02-04 13:37:52 +07:00
Book Pauk
8abe71a0fe Поправлен баг загрузки шрифтов 2020-02-04 13:30:48 +07:00
Book Pauk
43e27a7e68 Переход на quasar 2020-02-04 13:08:49 +07:00
Book Pauk
b784d277e4 Небольшие поправки 2020-02-04 12:29:54 +07:00
Book Pauk
cb443157da Переход на quasar 2020-02-04 12:20:12 +07:00
Book Pauk
c886015d92 Поправлен tooltip delay 2020-02-03 16:57:54 +07:00
Book Pauk
3161247da9 Рефакторинг 2020-02-03 16:57:39 +07:00
Book Pauk
743a250131 Поправлена иконка 2020-02-03 16:16:13 +07:00
Book Pauk
4fb4b21a9e Поправил цвет кнопки 2020-02-03 16:07:25 +07:00
Book Pauk
e1a7d3ebc5 Мелкие поправки 2020-02-03 16:05:36 +07:00
Book Pauk
72b8b156ac Переход на quasar 2020-02-03 15:49:27 +07:00
Book Pauk
134dafb608 Поправлен баг 2020-02-03 15:39:09 +07:00
Book Pauk
d5102b6422 Мелкая поправка 2020-02-03 15:38:20 +07:00
Book Pauk
a2cfb9d423 Переход на quasar 2020-02-03 15:33:54 +07:00
Book Pauk
bef70f94ab Переход на quasar 2020-02-03 15:12:28 +07:00
Book Pauk
4233fffe74 Переход на quasar 2020-02-03 14:23:07 +07:00
Book Pauk
81c214748d Закомментировал пока неиспользуемые компоненты 2020-02-03 02:22:16 +07:00
Book Pauk
c6a61dc8c8 Переход на quasar 2020-02-03 02:20:46 +07:00
Book Pauk
483092d40d Небольшие поправки 2020-02-03 01:57:04 +07:00
Book Pauk
88cb02f6bc Мелкие поправки 2020-02-03 01:39:52 +07:00
Book Pauk
9628188730 Переход на quasar 2020-02-03 01:25:57 +07:00
Book Pauk
2e66134bf8 Переход на quasar 2020-02-03 01:10:58 +07:00
Book Pauk
424fe4d1e9 Переход на quasar 2020-02-03 01:01:10 +07:00
Book Pauk
2b6f9568de Мелкий рефакторинг 2020-02-02 23:25:28 +07:00
Book Pauk
4b270bce8b Переход на quasar 2020-02-02 23:15:59 +07:00
Book Pauk
6b077e67db Мелкие поправки 2020-02-02 20:17:31 +07:00
Book Pauk
4c79ea0679 Мелкая поправка 2020-02-02 18:20:25 +07:00
Book Pauk
8c4c4c25aa Переход на quasar 2020-02-02 18:17:30 +07:00
Book Pauk
a37dbe2c06 Переход на quasar 2020-02-02 16:49:12 +07:00
Book Pauk
5e10cb2d16 Переход на quasar 2020-02-02 15:55:04 +07:00
Book Pauk
58316c5c1d Переход на quasar 2020-02-02 15:37:36 +07:00
Book Pauk
55f092f161 Переход на quasar 2020-02-02 15:14:22 +07:00
Book Pauk
ab5049127a Переход на quasar 2020-02-02 15:03:50 +07:00
Book Pauk
5f99067e56 Переход на quasar 2020-02-02 14:24:43 +07:00
Book Pauk
3a89e61bd8 Небольшие поправки 2020-02-02 12:58:13 +07:00
Book Pauk
06edfa2fee Поправки цветов заголовка 2020-02-02 12:48:07 +07:00
Book Pauk
77bfd72458 Переход на quasar 2020-01-31 21:14:22 +07:00
Book Pauk
5ddf19be4d Удалил мусор 2020-01-31 21:14:09 +07:00
Book Pauk
6657b47746 Небольшие поправки 2020-01-31 20:41:17 +07:00
Book Pauk
5690efb07a Переход на quasar 2020-01-31 20:21:25 +07:00
Book Pauk
05600cba08 Переход на quasar 2020-01-31 19:51:53 +07:00
Book Pauk
e3b4120b2c Переход на quasar 2020-01-31 18:35:30 +07:00
Book Pauk
1059245fd9 Мелкая поправка 2020-01-31 18:17:39 +07:00
Book Pauk
87c8d310b3 Переход на quasar, добавлены иконки 2020-01-31 18:14:38 +07:00
Book Pauk
fdc4999556 Merge branch 'develop' into feature/quasar 2020-01-31 17:03:46 +07:00
Book Pauk
d28a8db4ff Добавлен альтернативный метод вычисления ширины строки в пикселях 2020-01-31 16:59:34 +07:00
Book Pauk
ab9e7d10dd Добавлен отлов ошибок при инициализации, добавлена генерация ошибки measureText 2020-01-31 16:08:37 +07:00
Book Pauk
3ff72b26b9 0.8.3 2020-01-31 14:52:49 +07:00
Book Pauk
107ae70651 Переход на quasar 2020-01-31 14:49:59 +07:00
Book Pauk
04de19033e Переход на quasar 2020-01-31 14:49:44 +07:00
Book Pauk
089ac70cd3 Используем css-классы quasar 2020-01-30 18:53:01 +07:00
Book Pauk
ae40a9ead9 К предыдущему 2020-01-30 18:03:10 +07:00
Book Pauk
152806b7f6 Рефакторинг 2020-01-30 18:01:02 +07:00
Book Pauk
06beb8e704 Рефакторинг 2020-01-30 17:55:24 +07:00
Book Pauk
64f2b94685 0.8.3 2020-01-30 16:44:34 +07:00
Book Pauk
5a42eb98ab Merge branch 'develop' into feature/quasar 2020-01-30 16:36:17 +07:00
Book Pauk
404b87d78d Небольшие поправки 2020-01-30 16:34:05 +07:00
Book Pauk
dcb8fbdbf4 Merge tag '0.8.3-2' into develop
0.8.3-2
2020-01-29 01:03:20 +07:00
Book Pauk
0fe513d7f5 Merge branch 'release/0.8.3-2' 2020-01-29 01:02:56 +07:00
Book Pauk
0be05325e4 Исправлен баг 2020-01-29 01:02:05 +07:00
Book Pauk
75b39308cd Merge tag '0.8.3-1' into develop
0.8.3-1
2020-01-28 21:32:25 +07:00
Book Pauk
35ded81713 Merge branch 'release/0.8.3-1' 2020-01-28 21:32:02 +07:00
Book Pauk
07c85280cd Исправлены таймауты для конвертера calibre, добавлен флаг запуска -vv, соответственно поправлено вычисление прогресса 2020-01-28 21:27:54 +07:00
Book Pauk
43f1d86be0 Merge tag '0.8.3' into develop
0.8.3
2020-01-28 20:21:40 +07:00
Book Pauk
82f5ed4c44 Merge branch 'release/0.8.3' 2020-01-28 20:21:31 +07:00
Book Pauk
0b53ad4b4d Версия 0.8.3 2020-01-28 20:20:10 +07:00
Book Pauk
56ad41d10c Поправки текста объявления 2020-01-28 20:18:02 +07:00
Book Pauk
249a4564e0 Добавлено уведомление "Оплатим хостинг вместе" 2020-01-28 19:46:34 +07:00
Book Pauk
efb2413720 Небольшое изменение содержимого страницы 2020-01-28 19:44:52 +07:00
Book Pauk
1226acefd6 Небольшие исправления, queue теперь в одном экземпляре на класс 2020-01-28 14:51:09 +07:00
Book Pauk
76f7d7bc90 Мелкая поправка 2020-01-27 19:52:56 +07:00
Book Pauk
a5cb2641fd Мелкая поправка 2020-01-27 19:42:30 +07:00
Book Pauk
57fc64af79 Добавлен abort конвертеров при истечении времени ожидания подвижек очереди 2020-01-27 19:34:10 +07:00
Book Pauk
f8b7b8b698 Исправления LimitedQueue, исправления багов, добавлена проверка флага abort 2020-01-27 18:57:42 +07:00
Book Pauk
3da6befe10 Добавлен класс LimitedQueue для организации очередей 2020-01-26 18:38:09 +07:00
Book Pauk
a50d61c3ce Добавлена очередь скачивания и конвертирования 2020-01-26 18:37:14 +07:00
Book Pauk
b7568975e7 Добавлена обработка state = 'queue' 2020-01-26 18:31:31 +07:00
Book Pauk
4b9475310f Убрал ненужный this.taken 2020-01-26 16:23:20 +07:00
Book Pauk
639f726c83 Добавлен лимит на размер файла при распаковке 2020-01-26 15:17:45 +07:00
Book Pauk
7997c486cf Мелкий рефакторинг 2020-01-26 15:07:14 +07:00
Book Pauk
2569d00bd0 Мелкие поправки 2020-01-26 13:47:25 +07:00
Book Pauk
2cd80d8fa1 Merge tag '0.8.2-5' into develop
0.8.2-5
2020-01-23 17:12:46 +07:00
Book Pauk
eedca4db9b Merge branch 'release/0.8.2-5' 2020-01-23 17:12:37 +07:00
Book Pauk
1d352a76ce Поправка опечаток 2020-01-23 17:00:17 +07:00
Book Pauk
17670aabf9 WebSocket: добавлен метод reader-storage, поправки багов 2020-01-23 16:59:08 +07:00
Book Pauk
3456b3d90e WebSocket: добавлен метод worker-get-state-finish, небольшой рефакторинг 2020-01-23 16:25:06 +07:00
Book Pauk
f3da5a9026 Поправил комментарий 2020-01-23 15:56:26 +07:00
Book Pauk
00cc63b7cd WebSocket: добавлен метод get-config 2020-01-23 15:54:46 +07:00
Book Pauk
8df80ce738 Мелкая поправка 2020-01-23 15:16:49 +07:00
Book Pauk
12e7a783b0 Небольшие изменения блокирования кнопок панели 2020-01-22 22:06:12 +07:00
Book Pauk
be86a15351 Добавил настройку proxy_read_timeout 2020-01-22 21:37:28 +07:00
Book Pauk
2c5022e7b4 Merge tag '0.8.2-4' into develop
0.8.2-4
2020-01-22 21:17:58 +07:00
Book Pauk
f4a996fcb9 Merge branch 'release/0.8.2-4' 2020-01-22 21:17:52 +07:00
Book Pauk
fdbf508bbf Используем протокол WSS при необходимости 2020-01-22 21:17:10 +07:00
Book Pauk
500fafa5b2 Merge tag '0.8.2-3' into develop
0.8.2-3
2020-01-22 21:05:36 +07:00
Book Pauk
bfa315c68b Merge branch 'release/0.8.2-3' 2020-01-22 21:05:27 +07:00
Book Pauk
4972f085a3 Мелкая поправка 2020-01-22 20:59:52 +07:00
Book Pauk
9c13261929 Добавлена настройка для вебсокетов, добавлен конфиг nginx omnireader_http 2020-01-22 20:58:57 +07:00
Book Pauk
e36dc4a913 Небольшие поправки 2020-01-22 20:28:46 +07:00
Book Pauk
4cccb56ee3 Поправил комментарий 2020-01-22 20:15:33 +07:00
Book Pauk
3199af570d Добавлен WebSocketServer и контроллер для него 2020-01-22 20:06:51 +07:00
Book Pauk
7dad47b3c8 Добавлено использование WebSocketConnection 2020-01-22 20:02:42 +07:00
Book Pauk
fbd50bad1d Исправления багов 2020-01-22 20:02:05 +07:00
Book Pauk
10469bae7b Мелкая поправка 2020-01-22 20:01:21 +07:00
Book Pauk
b6a000a001 Добавлен пакет ws 2020-01-22 20:00:52 +07:00
Book Pauk
59539e7e90 Добавлен класс WebSocketConnection 2020-01-22 19:32:11 +07:00
Book Pauk
a2c41bc5ec Merge tag '0.8.2-2' into develop
0.8.2-2
2020-01-21 16:56:20 +07:00
Book Pauk
c4a06858fb Merge branch 'release/0.8.2-2' 2020-01-21 16:56:12 +07:00
Book Pauk
15b0f05a05 Добавил комментарий 2020-01-21 16:55:41 +07:00
Book Pauk
67feee9aa1 Поправлен баг 2020-01-21 16:53:34 +07:00
Book Pauk
185fb57b8c Удален нерабочий код 2020-01-21 16:25:30 +07:00
Book Pauk
e9039f8208 Merge tag '0.8.2-1' into develop
0.8.2-1
2020-01-21 16:14:21 +07:00
Book Pauk
440d1b3ba0 Merge branch 'release/0.8.2-1' 2020-01-21 16:14:15 +07:00
Book Pauk
9c7a6c64b0 Небольшие поправки 2020-01-21 16:13:38 +07:00
Book Pauk
7cc63fe849 Добавлена автоматическая отправка загруженной книги удаленное хранилище 2020-01-21 15:53:23 +07:00
Book Pauk
5647e8219d Мелкий рефакторинг 2020-01-21 14:58:42 +07:00
Book Pauk
81629fab7a Замена webdav-fs на webdav 2020-01-21 13:54:21 +07:00
Book Pauk
992d2033f3 Merge tag '0.8.2' into develop
0.8.2
2020-01-20 21:49:08 +07:00
Book Pauk
d52d4a1278 Merge branch 'release/0.8.2' 2020-01-20 21:49:00 +07:00
Book Pauk
57a44c5952 Версия 0.8.2 2020-01-20 21:48:31 +07:00
Book Pauk
a04161ac7c Добавил принудительную загрузку книги в обход кэша, если указан URL 2020-01-20 21:44:09 +07:00
Book Pauk
47e46f13c3 Добавлен работа с RemoteWebDavStorage, в т.ч. через api 2020-01-20 21:39:55 +07:00
Book Pauk
5535bd91c8 В конфиг добавлена опция remoteWebDavStorage 2020-01-20 21:37:31 +07:00
Book Pauk
8747a00de6 Поправлен баг 2020-01-20 21:36:44 +07:00
Book Pauk
c926b86926 Добавлен пакет webdav-fs 2020-01-20 21:22:27 +07:00
Book Pauk
010ac9aa7c Доработка api, восстановление кэшированного файла из хранилища 2020-01-20 21:21:13 +07:00
Book Pauk
4ab0c337f1 Рефакторинг 2020-01-15 16:20:46 +07:00
Book Pauk
f814c42fdd Поправлен баг в getStateFinish 2020-01-15 16:06:28 +07:00
Book Pauk
02aee3e625 Добавлена переупаковка файла книги по максимуму через 5 сек после загрузки и конвертирования 2020-01-15 15:49:45 +07:00
Book Pauk
52a32cfdd1 Добавлена обработка ошибок JSON.parse 2020-01-12 20:06:50 +07:00
Book Pauk
6faa7b2efe Уменьшение запросов get-state к api, добавлен метод get-state-finish 2020-01-12 18:51:12 +07:00
Book Pauk
f8481413c9 Мелкий рефакторинг 2020-01-12 17:03:34 +07:00
Book Pauk
7d4baa7046 Поправил цвет github corner 2020-01-11 01:01:18 +07:00
Book Pauk
0951d01383 Merge tag '0.8.1-1' into develop
0.8.1-1
2020-01-10 21:47:58 +07:00
Book Pauk
a24eaaed50 К предыдущему 2020-01-10 20:39:04 +07:00
Book Pauk
26813c582f Небольшие поправки 2020-01-10 20:37:18 +07:00
Book Pauk
6067ac73e2 Линкуем только необходимые компоненты 2020-01-09 22:00:41 +07:00
Book Pauk
b1d94b67f4 Мелкая поправка 2020-01-09 22:00:22 +07:00
Book Pauk
452f4e69fd Начало перехода от Element-UI на Quasar 2020-01-09 21:07:59 +07:00
111 changed files with 9039 additions and 2329 deletions

31
build/includer.js Normal file
View File

@@ -0,0 +1,31 @@
const path = require('path');
const fs = require('fs');
//пример в коде:
// @@include('./test/testFile.inc');
function includeRecursive(self, parentFile, source, depth) {
depth = (depth ? depth : 0);
if (depth > 50)
throw new Error('includer: stack too big');
const lines = source.split('\n');
let result = [];
for (const line of lines) {
const trimmed = line.trim();
const m = trimmed.match(/^@@[\s]*?include[\s]*?\(['"](.*)['"]\)/);
if (m) {
const includedFile = path.resolve(path.dirname(parentFile), m[1]);
self.addDependency(includedFile);
const fileContent = fs.readFileSync(includedFile, 'utf8');
result = result.concat(includeRecursive(self, includedFile, fileContent, depth + 1));
} else {
result.push(line);
}
}
return result;
}
exports.default = function includer(source) {
return includeRecursive(this, this.resourcePath, source).join('\n');
}

View File

@@ -16,6 +16,11 @@ module.exports = {
test: /\.vue$/,
loader: "vue-loader"
},
{
test: /\.includer$/,
resourceQuery: /^\?vue/,
use: path.resolve('build/includer.js')
},
{
test: /\.js$/,
loader: 'babel-loader',

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,4 +1,5 @@
import axios from 'axios';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api'
@@ -6,9 +7,23 @@ const api = axios.create({
class Misc {
async loadConfig() {
const response = await api.post('/config', {params: [
const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
]});
]};
try {
await wsc.open();
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error)
throw new Error(config.error);
return config;
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const response = await api.post('/config', query);
return response.data;
}
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import * as utils from '../share/utils';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api/reader'
@@ -11,8 +11,72 @@ const workerApi = axios.create({
});
class Reader {
constructor() {
}
async getWorkerStateFinish(workerId, callback) {
if (!callback) callback = () => {};
let response = {};
try {
await wsc.open();
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
let prevResponse = false;
while (1) {// eslint-disable-line no-constant-condition
response = await wsc.message(requestId);
if (!response.state && prevResponse !== false) {//экономия траффика
callback(prevResponse);
} else {//были изменения worker state
if (!response.state)
throw new Error('Неверный ответ api');
callback(response);
prevResponse = response;
}
if (response.state == 'finish' || response.state == 'error') {
break;
}
}
return response;
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const refreshPause = 500;
let i = 0;
response = {};
while (1) {// eslint-disable-line no-constant-condition
const prevProgress = response.progress || 0;
const prevState = response.state || 0;
response = await workerApi.post('/get-state', {workerId});
response = response.data;
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
if (i > 0)
await utils.sleep(refreshPause);
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
}
return response;
}
async loadBook(opts, callback) {
const refreshPause = 300;
if (!callback) callback = () => {};
let response = await api.post('/load-book', opts);
@@ -22,62 +86,93 @@ class Reader {
throw new Error('Неверный ответ api');
callback({totalSteps: 4});
callback(response.data);
let i = 0;
while (1) {// eslint-disable-line no-constant-condition
callback(response.data);
response = await this.getWorkerStateFinish(workerId, callback);
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
if (response) {
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
callback({step: 4});
const book = await this.loadCachedBook(response.data.path, callback);
return Object.assign({}, response.data, {data: book.data});
const book = await this.loadCachedBook(response.path, callback, response.size);
return Object.assign({}, response, {data: book.data});
}
if (response.data.state == 'error') {
let errMes = response.data.error;
if (response.state == 'error') {
let errMes = response.error;
if (errMes.indexOf('getaddrinfo') >= 0 ||
errMes.indexOf('ECONNRESET') >= 0 ||
errMes.indexOf('EINVAL') >= 0 ||
errMes.indexOf('404') >= 0)
errMes = `Ресурс не найден по адресу: ${response.data.url}`;
errMes = `Ресурс не найден по адресу: ${response.url}`;
throw new Error(errMes);
}
if (i > 0)
await utils.sleep(refreshPause);
} else {
throw new Error('Пустой ответ сервера');
}
}
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
async checkCachedBook(url) {
let estSize = -1;
try {
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
} catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака
let response = null
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
if (response.state == 'error') {
throw new Error(response.error);
}
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
//проверка воркера
const prevProgress = response.data.progress;
const prevState = response.data.state;
response = await workerApi.post('/get-state', {workerId});
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
}
return estSize;
}
async checkUrl(url) {
return await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
}
async loadCachedBook(url, callback) {
const response = await axios.head(url);
let estSize = 1000000;
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
async loadCachedBook(url, callback, estSize = -1) {
if (!callback) callback = () => {};
callback({state: 'loading', progress: 0});
//получение размера файла
if (estSize && estSize < 0) {
estSize = await this.checkCachedBook(url);
}
//получение файла
estSize = (estSize > 0 ? estSize : 1000000);
const options = {
onDownloadProgress: progress => {
onDownloadProgress: (progress) => {
while (progress.loaded > estSize) estSize *= 1.5;
if (callback)
callback({progress: Math.round((progress.loaded*100)/estSize)});
}
}
//загрузка
return await axios.get(url, options);
}
@@ -114,13 +209,25 @@ class Reader {
}
async storage(request) {
let response = await api.post('/storage', request);
let response = null;
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/storage', request);
response = response.data;
}
const state = response.data.state;
const state = response.state;
if (!state)
throw new Error('Неверный ответ api');
if (response.state == 'error') {
throw new Error(response.error);
}
return response.data;
return response;
}
}

View File

@@ -0,0 +1,172 @@
const cleanPeriod = 60*1000;//1 минута
class WebSocketConnection {
//messageLifeTime в минутах (cleanPeriod)
constructor(messageLifeTime = 5) {
this.ws = null;
this.timer = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTime;
this.requestId = 0;
}
addListener(listener) {
if (this.listeners.indexOf(listener) < 0)
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
}
//рассылаем сообщение и удаляем те обработчики, которые его получили
emit(mes, isError) {
const len = this.listeners.length;
if (len > 0) {
let newListeners = [];
for (const listener of this.listeners) {
let emitted = false;
if (isError) {
if (listener.onError)
listener.onError(mes);
emitted = true;
} else {
if (listener.onMessage) {
if (listener.requestId) {
if (listener.requestId === mes.requestId) {
listener.onMessage(mes);
emitted = true;
}
} else {
listener.onMessage(mes);
emitted = true;
}
} else {
emitted = true;
}
}
if (!emitted)
newListeners.push(listener);
}
this.listeners = newListeners;
}
return this.listeners.length != len;
}
open(url) {
return new Promise((resolve, reject) => {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
let protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:'
}
url = url || `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(url);
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
let resolved = false;
this.ws.onopen = (e) => {
resolved = true;
resolve(e);
};
this.ws.onmessage = (e) => {
try {
const mes = JSON.parse(e.data);
this.messageQueue.push({regTime: Date.now(), mes});
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (!this.emit(message.mes)) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} catch (e) {
this.emit(e.message, true);
}
};
this.ws.onerror = (e) => {
this.emit(e.message, true);
if (!resolved)
reject(e);
};
}
});
}
//timeout в минутах (cleanPeriod)
message(requestId, timeout = 2) {
return new Promise((resolve, reject) => {
this.addListener({
requestId,
timeout,
onMessage: (mes) => {
resolve(mes);
},
onError: (e) => {
reject(e);
}
});
});
}
send(req) {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
const requestId = ++this.requestId;
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
return requestId;
} else {
throw new Error('WebSocket connection is not ready');
}
}
close() {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
this.ws.close();
}
}
periodicClean() {
try {
this.timer = null;
const now = Date.now();
//чистка listeners
let newListeners = [];
for (const listener of this.listeners) {
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
newListeners.push(listener);
} else {
if (listener.onError)
listener.onError('Время ожидания ответа истекло');
}
}
this.listeners = newListeners;
//чистка messageQueue
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} finally {
if (this.ws.readyState == WebSocket.OPEN) {
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
}
}
export default new WebSocketConnection();

View File

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

View File

@@ -1,9 +1,19 @@
<template>
<el-container>
<el-aside v-if="showAsideBar" :width="asideWidth">
<!--q-layout view="lhr lpr lfr">
<q-drawer v-model="showAsideBar" :width="asideWidth">
<div class="app-name"><span v-html="appName"></span></div>
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
<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>
@@ -32,24 +42,37 @@
<i class="el-icon-question"></i>
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
</el-menu-item>
</el-menu>
</el-aside>
</el-menu-->
<!--/q-drawer>
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
<q-page-container>
<keep-alive>
<router-view></router-view>
</keep-alive>
</el-main>
</el-container>
</q-page-container>
</q-layout-->
<div class="fit row">
<Notify ref="notify"/>
<StdDialog ref="stdDialog"/>
<keep-alive>
<router-view class="col"></router-view>
</keep-alive>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import Notify from './share/Notify.vue';
import StdDialog from './share/StdDialog.vue';
import * as utils from '../share/utils';
export default @Component({
components: {
Notify,
StdDialog,
},
watch: {
mode: function() {
this.setAppTitle();
@@ -75,6 +98,18 @@ class App extends Vue {
this.uistate = this.$store.state.uistate;
this.config = this.$store.state.config;
//root route
let cachedRoute = '';
let cachedPath = '';
this.$root.rootRoute = () => {
if (this.$route.path != cachedPath) {
cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i);
cachedRoute = (m ? m[1] : this.$route.path);
}
return cachedRoute;
}
// set-app-title
this.$root.$on('set-app-title', this.setAppTitle);
@@ -108,17 +143,16 @@ class App extends Vue {
}
mounted() {
this.$root.notify = this.$refs.notify;
this.$root.stdDialog = this.$refs.stdDialog;
this.dispatch('config/loadConfig');
this.$watch('apiError', function(newError) {
if (newError) {
let mes = newError.message;
if (newError.response && newError.response.config)
mes = newError.response.config.url + '<br>' + newError.response.statusText;
this.$notify.error({
title: 'Ошибка API',
dangerouslyUseHTMLString: true,
message: mes
});
this.$root.notify.error(mes, 'Ошибка API');
}
});
@@ -137,9 +171,9 @@ class App extends Vue {
get asideWidth() {
if (this.uistate.asideBarCollapse) {
return '64px';
return 64;
} else {
return '170px';
return 170;
}
}
@@ -163,10 +197,7 @@ class App extends Vue {
}
get rootRoute() {
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
this.$root.rootRoute = (m ? m[1] : this.$route.path);
return this.$root.rootRoute;
return this.$root.rootRoute();
}
setAppTitle(title) {
@@ -193,12 +224,11 @@ class App extends Vue {
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
}
get isReaderActive() {
return this.rootRoute == '/reader';
set showAsideBar(value) {
}
get showMain() {
return (this.showAsideBar || this.isReaderActive);
get isReaderActive() {
return this.rootRoute == '/reader';
}
redirectIfNeeded() {
@@ -228,68 +258,28 @@ class App extends Vue {
line-height: 140%;
font-weight: bold;
}
.bold-font {
font-weight: bold;
}
.el-container {
height: 100%;
}
.el-aside {
line-height: 1;
background-color: #ccc;
color: #000;
}
.el-main {
padding: 0;
background-color: #E6EDF4;
color: #000;
}
.el-menu-vertical:not(.el-menu--collapse) {
background-color: inherit;
color: inherit;
text-align: left;
width: 100%;
border: 0;
}
.el-menu--collapse {
background-color: inherit;
color: inherit;
border: 0;
}
.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
background-color: inherit;
color: inherit;
margin-top: 5px;
width: 100%;
height: 64px;
border: 0;
}
.el-menu-item {
font-size: 85%;
}
</style>
<style>
body, html, #app {
body, html, #app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font: normal 12pt ReaderDefault;
}
.el-tabs__content {
flex: 1;
padding: 0 !important;
display: flex;
flex-direction: column;
overflow: hidden;
.dborder {
border: 2px solid yellow !important;
}
.icon-rotate {
vertical-align: middle;
animation: rotating 2s linear infinite;
}
.notify-button-icon {
font-size: 16px !important;
}
@font-face {

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Book в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Card в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,15 +1,9 @@
<template>
<el-container direction="vertical">
<el-tabs type="border-card" style="height: 100%;" v-model="selectedTab">
<el-tab-pane label="Поиск"></el-tab-pane>
<el-tab-pane label="Автор"></el-tab-pane>
<el-tab-pane label="Книга"></el-tab-pane>
<el-tab-pane label="История"></el-tab-pane>
<keep-alive>
<router-view></router-view>
</keep-alive>
</el-tabs>
</el-container>
<div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
<script>
@@ -18,7 +12,7 @@ import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
const rootRoute = '/cardindex';
const selfRoute = '/cardindex';
const tab2Route = [
'/cardindex/search',
'/cardindex/card',
@@ -51,7 +45,7 @@ class CardIndex extends Vue {
if (t !== this.selectedTab)
this.selectedTab = t.toString();
} else {
if (route == rootRoute && lastActiveTab !== null)
if (route == selfRoute && lastActiveTab !== null)
this.setRouteByTab(lastActiveTab);
}
}

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел History в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Search в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Help в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Income в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Страница не найдена
</el-container>
</div>
</template>
<script>

View File

@@ -91,7 +91,7 @@ class CopyTextPage extends Vue {
close() {
this.stopInit = true;
this.$emit('copy-text-toggle');
this.$emit('do-action', {action: 'copyText'});
}
keyHook(event) {

View File

@@ -1,12 +1,12 @@
<template>
<div class="page">
<h4>Возможности читалки:</h4>
<span class="text-h6 text-bold">Возможности читалки:</span>
<ul>
<li>загрузка любой страницы интернета</li>
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
<li>работа в автономном режиме (без связи)</li>
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
<li>кэширование файлов книг на клиенте и на сервере</li>
<li>открытие книг с локального диска</li>
<li>плавный скроллинг текста</li>
@@ -25,10 +25,10 @@
<div v-show="mode == 'omnireader'">
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
&nbsp;
<span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
(скопировать)
</span>
<q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
<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>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
@@ -60,9 +60,9 @@ class CommonHelpPage extends Vue {
const result = await copyTextToClipboard(text);
const msg = (result ? mes : 'Копирование не удалось');
if (result)
this.$notify.success({message: msg});
this.$root.notify.success(msg);
else
this.$notify.error({message: msg});
this.$root.notify.error(msg);
}
}
//-----------------------------------------------------------------------------
@@ -70,20 +70,16 @@ class CommonHelpPage extends Vue {
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
}
.clickable {
color: blue;
text-decoration: underline;
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
color: blue;
}
</style>

View File

@@ -1,30 +1,51 @@
<template>
<div class="page">
<div class="box">
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
<p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
<div class="address">
<img class="logo" src="./assets/yandex.png">
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
<div class="para">{{ yandexAddress }}</div>
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
<div class="para">{{ yandexAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/paypal.png">
<div class="para">{{ paypalAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/bitcoin.png">
<el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
<div class="para">{{ bitcoinAddress }}</div>
<div class="para">{{ bitcoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/litecoin.png">
<el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
<div class="para">{{ litecoinAddress }}</div>
<div class="para">{{ litecoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/monero.png">
<el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
<div class="para">{{ moneroAddress }}</div>
<div class="para">{{ moneroAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
</div>
</div>
@@ -40,6 +61,7 @@ export default @Component({
})
class DonateHelpPage extends Vue {
yandexAddress = '410018702323056';
paypalAddress = 'bookpauk@gmail.com';
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
@@ -54,9 +76,9 @@ class DonateHelpPage extends Vue {
async copyAddress(address, prefix) {
const result = await copyTextToClipboard(address);
if (result)
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
else
this.$notify.error({message: 'Копирование не удалось'});
this.$root.notify.error('Копирование не удалось');
}
}
//-----------------------------------------------------------------------------
@@ -64,12 +86,10 @@ class DonateHelpPage extends Vue {
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
display: flex;
}
.p {
@@ -79,15 +99,10 @@ class DonateHelpPage extends Vue {
}
.box {
flex: 1;
max-width: 550px;
overflow-wrap: break-word;
}
h5 {
margin: 0;
}
.address {
padding-top: 10px;
margin-top: 20px;
@@ -97,13 +112,16 @@ h5 {
margin: 10px 10px 10px 40px;
}
.button {
margin-left: 10px;
}
.logo {
width: 130px;
position: relative;
top: 10px;
}
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
color: blue;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -4,23 +4,20 @@
Справка
</template>
<el-tabs type="border-card" v-model="selectedTab">
<el-tab-pane class="tab" label="Общее">
<CommonHelpPage></CommonHelpPage>
</el-tab-pane>
<el-tab-pane label="Клавиатура">
<HotkeysHelpPage></HotkeysHelpPage>
</el-tab-pane>
<el-tab-pane label="Мышь/тачскрин">
<MouseHelpPage></MouseHelpPage>
</el-tab-pane>
<el-tab-pane label="История версий" name="releases">
<VersionHistoryPage></VersionHistoryPage>
</el-tab-pane>
<el-tab-pane label="Помочь проекту" name="donate">
<DonateHelpPage></DonateHelpPage>
</el-tab-pane>
</el-tabs>
<div class="col column" style="min-width: 600px">
<q-btn-toggle
v-model="selectedTab"
toggle-color="primary"
no-caps unelevated
:options="buttons"
/>
<div class="separator"></div>
<keep-alive>
<component ref="page" class="col" :is="activePage"
></component>
</keep-alive>
</div>
</Window>
</template>
@@ -33,32 +30,54 @@ import Window from '../../share/Window.vue';
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
const pages = {
'CommonHelpPage': CommonHelpPage,
'HotkeysHelpPage': HotkeysHelpPage,
'MouseHelpPage': MouseHelpPage,
'VersionHistoryPage': VersionHistoryPage,
'DonateHelpPage': DonateHelpPage,
};
const tabs = [
['CommonHelpPage', 'Общее'],
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'],
];
export default @Component({
components: {
Window,
CommonHelpPage,
HotkeysHelpPage,
MouseHelpPage,
DonateHelpPage,
VersionHistoryPage,
},
components: Object.assign({ Window }, pages),
})
class HelpPage extends Vue {
selectedTab = null;
selectedTab = 'CommonHelpPage';
close() {
this.$emit('help-toggle');
this.$emit('do-action', {action: 'help'});
}
get activePage() {
if (pages[this.selectedTab])
return pages[this.selectedTab];
return null;
}
get buttons() {
let result = [];
for (const tab of tabs)
result.push({label: tab[1], value: tab[0]});
return result;
}
activateDonateHelpPage() {
this.selectedTab = 'donate';
this.selectedTab = 'DonateHelpPage';
}
activateVersionHistoryHelpPage() {
this.selectedTab = 'releases';
this.selectedTab = 'VersionHistoryPage';
}
keyHook(event) {
@@ -72,16 +91,8 @@ class HelpPage extends Vue {
</script>
<style scoped>
.el-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.el-tab-pane {
flex: 1;
display: flex;
overflow: hidden;
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style>

View File

@@ -1,28 +1,13 @@
<template>
<div class="page">
<h4>Управление с помощью горячих клавиш:</h4>
<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,25 +16,32 @@
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>
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="page">
<h4>Управление с помощью мыши/тачскрина:</h4>
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
<ul>
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
<div class="click-map-page">
@@ -49,17 +49,12 @@ class MouseHelpPage extends Vue {
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
}
.click-map-page {
position: relative;
width: 400px;

View File

@@ -1,13 +1,14 @@
<template>
<div id="versionHistoryPage" class="page">
<span class="text-h6 text-bold">История версий:</span>
<br><br>
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
<p>
{{ item }}
</p>
</span>
<br>
<h4>История версий:</h4>
<br>
<div v-for="item in versionContent" :id="item.key" :key="item.key">
@@ -58,15 +59,11 @@ class VersionHistoryPage extends Vue {
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
position: relative;
}
p {

View File

@@ -47,7 +47,7 @@
default: false
}
},
data () {
data() {
return {
svgStyle: {
fill: this.cornerColor,
@@ -60,8 +60,13 @@
},
flipped: false,
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 ' +
'123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 ' +
'C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 ' +
'176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 ' +
'216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 ' +
'C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
}
},
methods: {
@@ -99,7 +104,7 @@
}
</script>
<style>
<style scoped>
#github-corner .octo-arm {
transform-origin: 130px 106px
}
@@ -122,7 +127,8 @@
top: 0;
border: 0;
}
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
transition: fill 1s ease;
}
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
transition: fill 1s ease;
}
</style>

View File

@@ -1,31 +1,36 @@
<template>
<div ref="main" class="main">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F"></GithubCorner>
<div class="part top">
<span class="greeting bold-font">{{ title }}</span>
<div class="space"></div>
<div ref="main" class="column no-wrap" style="min-height: 500px">
<div 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">
<span class="greeting"><b>{{ title }}</b></span>
<div class="q-my-sm"></div>
<span class="greeting">Добро пожаловать!</span>
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
</div>
<div class="part center">
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
</el-input>
<div class="space"></div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
<template v-slot:append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
</template>
</q-input>
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
<el-button size="mini" @click="loadFileClick">
<div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
Загрузить файл с диска
</el-button>
<div class="space"></div>
<el-button size="mini" @click="loadBufferClick">
</q-btn>
<div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
Из буфера обмена
</el-button>
</q-btn>
<div class="space"></div>
<div class="space"></div>
<div class="q-my-md"></div>
<div v-if="mode == 'omnireader'">
<div ref="yaShare2" class="ya-share2"
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
@@ -34,12 +39,12 @@
data-url="https://omnireader.ru">
</div>
</div>
<div class="space"></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>
<div class="part bottom">
<div class="col column justify-end items-center no-wrap overflow-hidden">
<span class="bottom-span clickable" @click="openHelp">Справка</span>
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
@@ -112,7 +117,7 @@ class LoaderPage extends Vue {
submitUrl() {
if (this.bookUrl) {
this.$emit('load-book', {url: this.bookUrl});
this.$emit('load-book', {url: this.bookUrl, force: true});
this.bookUrl = '';
}
}
@@ -143,12 +148,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() {
@@ -168,80 +173,37 @@ class LoaderPage extends Vue {
const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.code == '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;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.main {
flex: 1;
display: flex;
flex-direction: column;
min-height: 480px;
}
.part {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.greeting {
font-size: 120%;
line-height: 160%;
}
.bold-font {
font-weight: bold;
}
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.top {
min-height: 120px;
}
.center {
justify-content: flex-start;
padding: 0 10px 0 10px;
min-height: 250px;
}
.bottom {
justify-content: flex-end;
}
.bottom-span {
font-size: 70%;
margin-bottom: 10px;
}
.el-input {
max-width: 700px;
}
.space {
height: 20px;
}
</style>

View File

@@ -3,14 +3,12 @@
<template slot="header">
<span style="position: relative; top: -3px">
Вставьте текст и нажмите
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
или F2
</span>
</template>
<div>
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
</div>
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
<hr/>
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
</Window>
@@ -70,7 +68,7 @@ class PasteTextPage extends Vue {
}
loadBuffer() {
this.$emit('load-buffer', {buffer: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
this.close();
}

View File

@@ -1,8 +1,24 @@
<template>
<div v-show="visible" class="main">
<div class="center">
<el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
<p class="text">{{ text }}</p>
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
<div class="column justify-start items-center" style="height: 250px">
<q-circular-progress
show-value
instant-feedback
font-size="13px"
:value="percentage"
size="100px"
:thickness="0.11"
color="green-7"
track-color="grey-4"
class="q-ma-md"
>
<span class="text-yellow">{{ percentage }}%</span>
</q-circular-progress>
<div>
<span class="text-yellow">{{ text }}</span>
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
</div>
</div>
</div>
</template>
@@ -11,11 +27,13 @@
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import * as utils from '../../../share/utils';
const ruMessage = {
'start': ' ',
'finish': ' ',
'error': ' ',
'queue': 'очередь',
'download': 'скачивание',
'decompress': 'распаковка',
'convert': 'конвертирование',
@@ -32,68 +50,51 @@ class ProgressPage extends Vue {
step = 1;
progress = 0;
visible = false;
iconStyle = '';
show() {
this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
this.text = '';
this.totalSteps = 1;
this.step = 1;
this.progress = 0;
this.iconAngle = 0;
this.ani = false;
this.visible = true;
}
hide() {
this.visible = false;
this.text = '';
this.iconAngle = 0;
}
setState(state) {
if (state.state)
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
if (state.state) {
if (state.state == 'queue') {
this.text = (state.place ? 'Номер в очереди: ' + state.place : '');
} else {
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
}
}
this.step = (state.step ? state.step : this.step);
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
this.progress = state.progress || 0;
if (!this.ani) {
(async() => {
this.ani = true;
this.iconAngle += 30;
this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
await utils.sleep(150);
this.ani = false;
})();
}
}
get percentage() {
let circle = document.querySelector('path[class="el-progress-circle__path"]');
if (circle)
circle.style.transition = '';
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
background-color: rgba(0, 0, 0, 0.8);
position: absolute;
}
.center {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
color: white;
height: 300px;
}
.text {
color: yellow;
}
</style>
<style>
.el-progress__text {
color: lightgreen !important;
}
</style>

View File

@@ -1,97 +1,148 @@
<template>
<el-container>
<el-header v-show="toolBarActive" height='50px'>
<div ref="header" class="header">
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
</el-tooltip>
<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%">{{ rstore.readerActions['loader'] }}</q-tooltip>
</button>
<div>
<el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
</el-tooltip>
<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%">{{ 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%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
</button>
<div class="space"></div>
<el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
</el-button>
</el-tooltip>
<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%">{{ 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%">{{ 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%">{{ 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%">{{ 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%">{{ rstore.readerActions['copyText'] }}</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" :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>
<el-tooltip v-show="showToolButton['offlineMode']" content="Автономный режим (без интернета)" :open-delay="1000" effect="light">
<el-button ref="offlineMode" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"><i class="el-icon-connection"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
</el-tooltip>
<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="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%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
</button>
</div>
<el-tooltip content="Настроить" :open-delay="1000" effect="light">
<el-button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')"><i class="el-icon-setting"></i></el-button>
</el-tooltip>
<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>
</el-header>
</div>
<el-main>
<div class="main col row relative-position">
<keep-alive>
<component ref="page" :is="activePage"
<component ref="page" class="col" :is="activePage"
@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>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
<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>
<el-dialog
title="Что нового:"
:visible.sync="whatsNewVisible"
width="80%">
<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" class="dialog-footer">
<el-button @click="whatsNewDisable">Больше не показывать</el-button>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
</span>
</el-dialog>
</Dialog>
</el-main>
</el-container>
<Dialog ref="dialog2" v-model="donationVisible">
<template slot="header">
Здравствуйте, уважаемые читатели!
</template>
<div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div>
<span slot="footer">
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog>
</div>
</div>
</template>
<script>
@@ -113,8 +164,10 @@ 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 Dialog from '../share/Dialog.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';
@@ -133,6 +186,7 @@ export default @Component({
HelpPage,
ClickMapPage,
ServerStorage,
Dialog,
},
watch: {
bookPos: function(newValue) {
@@ -174,6 +228,7 @@ export default @Component({
},
})
class Reader extends Vue {
rstore = {};
loaderActive = false;
progressActive = false;
fullScreenActive = false;
@@ -200,8 +255,10 @@ class Reader extends Vue {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
created() {
this.rstore = rstore;
this.loading = true;
this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
@@ -244,8 +301,8 @@ class Reader extends Vue {
(async() => {
await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent);
if (this.$root.rootRoute == '/reader') {
if (this.$root.rootRoute() == '/reader') {
if (this.routeParamUrl) {
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
} else {
@@ -258,9 +315,10 @@ class Reader extends Vue {
this.checkActivateDonateHelpPage();
this.loading = false;
await this.showWhatsNew();
this.updateRoute();
await this.showWhatsNew();
await this.showDonation();
})();
}
@@ -272,16 +330,27 @@ class Reader extends Vue {
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();
}
updateHeaderMinWidth() {
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
if (this.$refs.header)
this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
if (this.$refs.buttons)
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
(async() => {
await utils.sleep(1000);
if (this.$refs.header)
this.$refs.header.style.overflowX = 'auto';
})();
}
checkSetStorageAccessKey() {
@@ -337,6 +406,41 @@ class Reader extends Vue {
}
}
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();
@@ -455,6 +559,10 @@ class Reader extends Vue {
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) {
@@ -473,22 +581,9 @@ class Reader extends Vue {
fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) {
const element = document.documentElement;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitrequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.mozRequestFullscreen) {
element.mozRequestFullScreen();
}
this.$q.fullscreen.request();
} else {
if (document.cancelFullScreen) {
document.cancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
}
this.$q.fullscreen.exit();
}
}
@@ -511,7 +606,8 @@ class Reader extends Vue {
setPositionToggle() {
this.setPositionActive = !this.setPositionActive;
if (this.setPositionActive && this.activePage == 'TextPage' && this.mostRecentBook()) {
const page = this.$refs.page;
if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
this.closeAllTextPages();
this.setPositionActive = true;
@@ -591,6 +687,10 @@ class Reader extends Vue {
}
}
recentBooksClose() {
this.recentBooksActive = false;
}
recentBooksToggle() {
this.recentBooksActive = !this.recentBooksActive;
if (this.recentBooksActive) {
@@ -653,81 +753,53 @@ 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].$el.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 'recentBooks':
case 'refresh':
case 'offlineMode':
case 'recentBooks':
case 'settings':
if (this[`${button}Active`])
if (this.progressActive) {
classResult = classDisabled;
} else if (this[`${action}Active`]) {
classResult = classActive;
}
break;
}
switch (button) {
case 'undoAction':
if (this.actionCur <= 0)
classResult = classDisabled;
@@ -739,7 +811,7 @@ class Reader extends Vue {
}
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
switch (button) {
switch (action) {
case 'undoAction':
case 'redoAction':
case 'setPosition':
@@ -824,6 +896,8 @@ class Reader extends Vue {
return;
}
this.closeAllTextPages();
let url = encodeURI(decodeURI(opts.url));
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
@@ -930,7 +1004,7 @@ class Reader extends Vue {
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$alert(e.message, 'Ошибка', {type: 'error'});
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
@@ -954,7 +1028,7 @@ class Reader extends Vue {
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$alert(e.message, 'Ошибка', {type: 'error'});
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
@@ -986,8 +1060,119 @@ 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 'settings':
this.settingsToggle();
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 'refresh':
this.refreshBook();
break;
case 'offlineMode':
this.offlineModeToggle();
break;
case 'recentBooks':
this.recentBooksToggle();
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) {
if (this.$root.rootRoute == '/reader') {
let result = false;
if (this.$root.rootRoute() == '/reader') {
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
return result;
let handled = false;
if (!handled && this.helpActive)
handled = this.$refs.helpPage.keyHook(event);
@@ -1011,92 +1196,40 @@ class Reader extends Vue {
handled = this.$refs.page.keyHook(event);
if (!handled && event.type == 'keydown') {
if (event.code == 'Escape')
this.loaderToggle();
const action = this.$root.readerActionByKeyEvent(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 (action == 'loader') {
result = this.doAction({action, event});
}
if (!result && this.activePage == 'TextPage') {
result = this.doAction({action, event});
}
}
}
return result;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.el-container {
padding: 0;
margin: 0;
height: 100%;
}
.el-header {
.header {
padding-left: 5px;
padding-right: 5px;
background-color: #1B695F;
color: #000;
overflow-x: auto;
overflow-y: hidden;
overflow: hidden;
height: 50px;
}
.header {
display: flex;
justify-content: space-between;
}
.el-main {
position: relative;
display: flex;
padding: 0;
margin: 0;
.main {
background-color: #EBE2C9;
color: #000;
}
.tool-button {
margin: 0 2px 0 2px;
margin: 0px 2px 0 2px;
padding: 0;
color: #3E843E;
background-color: #E6EDF4;
@@ -1104,15 +1237,14 @@ class Reader extends Vue {
height: 38px;
width: 38px;
border: 0;
border-radius: 6px;
box-shadow: 3px 3px 5px black;
}
.tool-button + .tool-button {
margin: 0 2px 0 2px;
outline: 0;
}
.tool-button:hover {
background-color: white;
cursor: pointer;
}
.tool-button-active {
@@ -1127,20 +1259,19 @@ class Reader extends Vue {
.tool-button-active:hover {
color: white;
background-color: #81C581;
cursor: pointer;
}
.tool-button-disabled {
color: lightgray;
background-color: gray;
cursor: default;
}
.tool-button-disabled:hover {
color: lightgray;
background-color: gray;
}
i {
font-size: 200%;
cursor: default;
}
.space {
@@ -1157,4 +1288,10 @@ i {
text-decoration: underline;
cursor: pointer;
}
.copy-icon {
cursor: pointer;
font-size: 120%;
color: blue;
}
</style>

View File

@@ -1,97 +1,84 @@
<template>
<Window width="600px" ref="window" @close="close">
<template slot="header">
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
<span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
<span v-show="!loading">{{ header }}</span>
<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>
<el-table
<a ref="download" style='display: none;' target="_blank"></a>
<q-table
class="recent-books-table col"
:data="tableData"
style="width: 570px"
size="mini"
height="1px"
stripe
border
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
:header-cell-style = "headerCellStyle"
:row-key = "rowKey"
>
:columns="columns"
row-key="key"
:pagination.sync="pagination"
separator="cell"
hide-bottom
virtual-scroll
dense
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
placeholder="Найти"
v-model="search"
@click.stop
/>
<el-table-column
type="index"
width="35px"
>
</el-table-column>
<el-table-column
prop="touchDateTime"
min-width="85px"
sortable
>
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<span style="font-size: 90%">Время<br>просм.</span>
</template>
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<div class="desc" @click="loadBook(scope.row.url)">
{{ scope.row.touchDate }}<br>
{{ scope.row.touchTime }}
</div>
</template>
</el-table-column>
<span v-html="props.cols[2].label"></span>
</q-th>
</q-tr>
</template>
<el-table-column
>
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<!--el-input ref="input"
:value="search" @input="search = $event"
size="mini"
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
placeholder="Найти"/-->
<div class="el-input el-input--mini">
<input class="el-input__inner"
ref="input"
placeholder="Найти"
style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
:value="search" @input="search = $event.target.value"
/>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="num" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 25px">
{{ props.row.num }}
</div>
</template>
</q-td>
<el-table-column
min-width="280px"
>
<template slot-scope="scope">
<div class="desc" @click="loadBook(scope.row.url)">
<span style="color: green">{{ scope.row.desc.author }}</span><br>
<span>{{ scope.row.desc.title }}</span>
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
<div class="break-word" style="width: 68px">
{{ props.row.touchDate }}<br>
{{ props.row.touchTime }}
</div>
</template>
</el-table-column>
</q-td>
<el-table-column
min-width="90px"
>
<template slot-scope="scope">
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
<a :href="scope.row.path" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
</template>
</el-table-column>
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
<div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">{{ props.row.desc.author }}</div>
<div>{{ props.row.desc.title }}</div>
</div>
</q-td>
<el-table-column
width="60px"
>
<template slot-scope="scope">
<el-button
size="mini"
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
</el-button>
</template>
</el-table-column>
<q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%">
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
</div>
</q-td>
</el-table-column>
<q-td key="close" :props="props" class="td-mp" auto-width>
<div style="width: 38px">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(props.row.key)">
<q-icon class="la la-times" size="14px" style="top: -6px"/>
</q-btn>
</div>
</q-td>
<q-td key="last" :props="props" class="no-mp">
</q-td>
</q-tr>
</template>
</q-table>
</el-table>
</Window>
</template>
@@ -121,52 +108,90 @@ class RecentBooksPage extends Vue {
loading = false;
search = null;
tableData = [];
columns = [];
pagination = {};
created() {
this.pagination = {rowsPerPage: 0};
this.columns = [
{
name: 'num',
label: '#',
align: 'center',
sortable: true,
field: 'num',
},
{
name: 'date',
label: 'Время<br>просм.',
align: 'left',
field: 'touchDateTime',
sortable: true,
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
},
{
name: 'desc',
label: 'Название',
align: 'left',
field: 'descString',
sortable: true,
},
{
name: 'links',
label: '',
align: 'left',
},
{
name: 'close',
label: '',
align: 'left',
},
{
name: 'last',
label: '',
align: 'left',
},
];
}
init() {
this.$refs.window.init();
this.$nextTick(() => {
//this.$refs.input.focus();
//this.$refs.input.focus();//плохо на планшетах
});
(async() => {//отбражение подгрузки списка, иначе тормозит
(async() => {//подгрузка списка
if (this.initing)
return;
this.initing = true;
await this.updateTableData(3);
await utils.sleep(200);
if (bookManager.loaded) {
const t = Date.now();
if (!bookManager.loaded) {
await this.updateTableData(10);
if (bookManager.getSortedRecent().length > 10)
await utils.sleep(10*(Date.now() - t));
} else {
//для отзывчивости
await utils.sleep(100);
let i = 0;
let j = 5;
while (i < 500 && !bookManager.loaded) {
if (i % j == 0) {
bookManager.sortedRecentCached = null;
await this.updateTableData(100);
await this.updateTableData(20);
j *= 2;
}
await utils.sleep(100);
i++;
}
} else {
//для отзывчивости
await utils.sleep(100);
}
await this.updateTableData();
this.initing = false;
})();
}
rowKey(row) {
return row.key;
}
async updateTableData(limit) {
while (this.updating) await utils.sleep(100);
this.updating = true;
@@ -175,11 +200,13 @@ class RecentBooksPage extends Vue {
this.loading = !!limit;
const sorted = bookManager.getSortedRecent();
let num = 0;
for (let i = 0; i < sorted.length; i++) {
const book = sorted[i];
if (book.deleted)
continue;
num++;
if (limit && result.length >= limit)
break;
@@ -221,19 +248,19 @@ class RecentBooksPage extends Vue {
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
result.push({
num,
touchDateTime: book.touchTime,
touchDate: t[0],
touchTime: t[1],
desc: {
title: `${title}${perc}${textLen}`,
author,
title: `${title}${perc}${textLen}`,
},
descString: `${author}${title}${perc}${textLen}`,
url: book.url,
path: book.path,
key: book.key,
});
if (result.length >= 100)
break;
}
const search = this.search;
@@ -245,44 +272,39 @@ class RecentBooksPage extends Vue {
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
/*for (let i = 0; i < result.length; i++) {
if (!_.isEqual(this.tableData[i], result[i])) {
this.$set(this.tableData, i, result[i]);
await utils.sleep(10);
}
}
if (this.tableData.length > result.length)
this.tableData.splice(result.length);*/
this.tableData = result;
this.updating = false;
}
headerCellStyle(cell) {
let result = {margin: 0, padding: 0};
if (cell.columnIndex > 0) {
result['border-bottom'] = 0;
wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
const deci = num % 100;
if (deci > 10 && deci < 20) {
return '';
} else {
return endings[num % 10];
}
if (cell.rowIndex > 0) {
result.height = '0px';
result['border-right'] = 0;
}
return result;
}
get header() {
const len = (this.tableData ? this.tableData.length : 0);
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
}
async downloadBook(fb2path) {
try {
await readerApi.checkUrl(fb2path);
await readerApi.checkCachedBook(fb2path);
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.$alert(errMes, 'Ошибка', {type: 'error'});
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
}
}
@@ -296,7 +318,7 @@ class RecentBooksPage extends Vue {
async handleDel(key) {
await bookManager.delRecentBook({key});
this.updateTableData();
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
if (!bookManager.mostRecentBook())
this.close();
@@ -315,11 +337,11 @@ class RecentBooksPage extends Vue {
}
close() {
this.$emit('recent-books-toggle');
this.$emit('recent-books-close');
}
keyHook(event) {
if (event.type == 'keydown' && event.code == 'Escape') {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
this.close();
}
return true;
@@ -329,7 +351,51 @@ class RecentBooksPage extends Vue {
</script>
<style scoped>
.desc {
.recent-books-table {
width: 600px;
overflow-y: auto;
overflow-x: hidden;
}
.clickable {
cursor: pointer;
}
</style>
.td-mp {
margin: 0 !important;
padding: 4px 4px 4px 4px !important;
border-bottom: 1px solid #ddd;
}
.no-mp {
margin: 0 !important;
padding: 0 !important;
border: 0;
border-left: 1px solid #ddd !important;
}
.break-word {
line-height: 180%;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
}
</style>
<style>
.recent-books-table .q-table__middle {
height: 100%;
overflow-x: hidden;
}
.recent-books-table thead tr:first-child th {
position: sticky;
z-index: 1;
top: 0;
background-color: #c1f4cd;
}
.recent-books-table tr:nth-child(even) {
background-color: #f8f8f8;
}
</style>

View File

@@ -8,15 +8,19 @@
<span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input">
<input ref="input" class="el-input__inner"
<!--input ref="input"
placeholder="что ищем"
:value="needle" @input="needle = $event.target.value"/>
:value="needle" @input="needle = $event.target.value"/-->
<q-input ref="input" class="col" outlined dense
placeholder="что ищем"
v-model="needle" @keydown="inputKeyDown"
/>
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
</div>
<el-button-group v-show="!initStep" class="button-group">
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
</el-button-group>
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
</q-btn-group>
</div>
</Window>
</template>
@@ -39,7 +43,10 @@ export default @Component({
},
foundText: function(newValue) {
this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
//недостатки сторонних ui
const el = this.$refs.input.$el.querySelector('label div div div input');
if (el)
el.style.paddingRight = newValue.length*12 + 'px';
},
},
})
@@ -157,15 +164,16 @@ class SearchPage extends Vue {
close() {
this.stopInit = true;
this.$emit('search-toggle');
this.$emit('do-action', {action: 'search'});
}
inputKeyDown(event) {
if (event.key == 'Enter') {
this.showNext();
}
}
keyHook(event) {
//недостатки сторонних ui
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
this.showNext();
}
if (event.type == 'keydown' && (event.code == 'Escape')) {
this.close();
}
@@ -194,17 +202,14 @@ class SearchPage extends Vue {
}
.button-group {
width: 150px;
width: 100px;
margin: 0;
padding: 0;
height: 37px;
}
.el-button {
.button {
padding: 9px 17px 9px 17px;
width: 55px;
}
i {
font-size: 20px;
width: 50px;
}
</style>

View File

@@ -177,17 +177,17 @@ class ServerStorage extends Vue {
success(message) {
if (this.showServerStorageMessages)
this.$notify.success({message});
this.$root.notify.success(message);
}
warning(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$notify.warning({message});
this.$root.notify.warning(message);
}
error(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$notify.error({message});
this.$root.notify.error(message);
}
async loadSettings(force = false, doNotifySuccess = true) {

View File

@@ -4,8 +4,15 @@
Установить позицию
</template>
<div class="slider">
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
<div id="set-position-slider" class="slider q-px-md">
<q-slider
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
v-model="sliderValue"
:max="sliderMax"
label
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
</div>
</Window>
</template>
@@ -46,20 +53,16 @@ class SetPositionPage extends Vue {
this.initialized = true;
}
formatTooltip(val) {
if (this.sliderMax)
return (val/this.sliderMax*100).toFixed(2) + '%';
else
return 0;
}
close() {
this.$emit('set-position-toggle');
}
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.code == 'Escape' || action == 'setPosition') {
this.close();
}
}
return true;
}
@@ -73,9 +76,13 @@ class SetPositionPage extends Vue {
background-color: #efefef;
border-radius: 15px;
}
</style>
.el-slider {
margin-right: 20px;
margin-left: 20px;
<style>
#set-position-slider .q-slider__thumb path {
fill: white !important;
stroke: blue !important;
stroke-width: 2 !important;
}
</style>

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,17 @@
const defPalette = [
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
];
export default defPalette;

View File

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

View File

@@ -0,0 +1,33 @@
<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="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

@@ -0,0 +1,107 @@
<!---------------------------------------------->
<div class="part-header">Подсказки, уведомления</div>
<div class="item row no-wrap">
<div class="label-6">Подсказка</div>
<q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать или нет подсказку при каждой загрузке книги
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Подсказка</div>
<q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Мерцать сообщением в строке статуса и на кнопке<br>
обновления при загрузке книги из кэша
</q-tooltip>
</q-checkbox>
</div>
<div class="item row no-wrap">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомления и ошибки от<br>
синхронизатора данных с сервером
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showWhatsNewDialog">
Показывать уведомление "Что нового"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомления "Что нового"<br>
при каждом выходе новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showDonationDialog2020">
Показывать "Оплатим хостинг вместе"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомление "Оплатим хостинг вместе"
</q-tooltip>
</q-checkbox>
</div>
<!---------------------------------------------->
<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">
Добавлять параметр "__p"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Добавление параметра "__p" в строке браузера<br>
позволяет передавать ссылку на книгу в читалке<br>
без потери текущей позиции. Однако в этом случае<br>
при листании забивается история браузера, т.к. на<br>
каждое изменение позиции происходит смена URL.
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Копирование</div>
<q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Загружать весь текст в окно<br>
копирования текста со страницы
</q-tooltip>
</q-checkbox>
</div>

View File

@@ -0,0 +1,28 @@
<!---------------------------------------------->
<div class="part-header">Анимация</div>
<div class="item row">
<div class="label-5">Тип</div>
<q-select class="col-left" v-model="pageChangeAnimation" :options="pageChangeAnimationOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
</div>
<div class="item row">
<div class="label-5">Скорость</div>
<NumInput class="col-left" v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disable="pageChangeAnimation == ''"/>
</div>
<!---------------------------------------------->
<div class="part-header">Другое</div>
<div class="item row">
<div class="label-5">Страница</div>
<q-checkbox v-model="keepLastToFirst" size="xs" label="Переносить последнюю строку">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Переносить последнюю строку страницы<br>
в начало следующей при листании
</q-tooltip>
</q-checkbox>
</div>

View File

@@ -0,0 +1,101 @@
<div class="part-header">Управление синхронизацией данных</div>
<div class="item row">
<div class="label-1"></div>
<q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
</div>
<div v-show="serverSyncEnabled">
<!---------------------------------------------->
<div class="part-header">Профили устройств</div>
<div class="item row">
<div class="label-1"></div>
<div class="text col">
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
</div>
</div>
<div class="item row">
<div class="label-1">Устройство</div>
<div class="col">
<q-select v-model="currentProfile" :options="currentProfileOptions"
style="width: 275px"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
</div>
</div>
<div class="item row">
<div class="label-1"></div>
<q-btn class="button" dense no-caps @click="addProfile">Добавить</q-btn>
<q-btn class="button" dense no-caps @click="delProfile">Удалить</q-btn>
<q-btn class="button" dense no-caps @click="delAllProfiles">Удалить все</q-btn>
</div>
<!---------------------------------------------->
<div class="part-header">Ключ доступа</div>
<div class="item row">
<div class="label-1"></div>
<div class="text col">
Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
</div>
</div>
<div class="item row">
<div class="label-1"></div>
<q-btn class="button" style="width: 250px" dense no-caps @click="showServerStorageKey">
<span v-show="serverStorageKeyVisible">Скрыть</span>
<span v-show="!serverStorageKeyVisible">Показать</span>
&nbsp;ключ доступа
</q-btn>
</div>
<div class="item row">
<div class="label-1"></div>
<div v-if="!serverStorageKeyVisible" class="col">
<hr/>
<b>{{ partialStorageKey }}</b> (часть вашего ключа)
<hr/>
</div>
<div v-else class="col" style="line-height: 100%">
<hr/>
<div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
<b>{{ serverStorageKey }}</b>
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
<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'">
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
<br><div class="text-center" style="margin-top: 5px">
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<hr/>
</div>
</div>
<div class="item row">
<div class="label-1"></div>
<q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
</div>
<div class="item row">
<div class="label-1"></div>
<q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
</div>
<div class="item row">
<div class="label-1"></div>
<div class="text col">
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
например, после переустановки ОС или чистки/смены браузера.<br>
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
и шифруются ключом доступа перед отправкой на сервер.
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div class="item row">
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
</div>

View File

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

View File

@@ -0,0 +1,58 @@
<!---------------------------------------------->
<div class="hidden part-header">Цвет</div>
<div class="item row">
<div class="label-2">Текст</div>
<div class="col row">
<q-input class="col-left no-mp"
outlined dense
v-model="textColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="textColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
</div>
</div>
<div class="q-mt-md"/>
<div class="item row">
<div class="label-2">Фон</div>
<div class="col row">
<q-input class="col-left no-mp"
outlined dense
v-model="bgColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
:disable="wallpaper != ''"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<div class="q-px-sm"/>
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
</div>
</div>

View File

@@ -0,0 +1,56 @@
<!---------------------------------------------->
<div class="hidden part-header">Шрифт</div>
<div class="item row">
<div class="label-2">Локальный/веб</div>
<div class="col row">
<q-select class="col-left" v-model="fontName" :options="fontsOptions" :disable="webFontName != ''"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
<div class="q-px-sm"/>
<q-select class="col" v-model="webFontName" :options="webFontsOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Веб шрифты дают большое разнообразие,<br>
однако есть шанс, что шрифт будет загружаться<br>
очень медленно или вовсе не загрузится
</q-tooltip>
</q-select>
</div>
</div>
<div class="item row">
<div class="label-2">Размер</div>
<div class="col row">
<NumInput class="col-left" v-model="fontSize" :min="5" :max="200"/>
<div class="col q-pt-xs text-right">
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
</div>
</div>
</div>
<div class="item row">
<div class="label-2">Сдвиг</div>
<div class="col row">
<NumInput class="col-left" v-model="vertShift" :min="-100" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сдвиг шрифта по вертикали в процентах от размера.<br>
Отрицательное значение сдвигает вверх, положительное -<br>
вниз. Значение зависит от метрики шрифта.
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Стиль</div>
<div class="col row">
<q-checkbox v-model="fontBold" size="xs" label="Жирный" />
<q-checkbox class="q-ml-sm" v-model="fontItalic" size="xs" label="Курсив" />
</div>
</div>

View File

@@ -0,0 +1,36 @@
<!---------------------------------------------->
<div class="hidden part-header">Строка статуса</div>
<div class="item row">
<div class="label-2">Статус</div>
<div class="col row">
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
</div>
</div>
<div class="item row">
<div class="label-2">Высота</div>
<div class="col row">
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
</div>
</div>
<div class="item row">
<div class="label-2">Прозрачность</div>
<div class="col row">
<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

@@ -0,0 +1,144 @@
<!---------------------------------------------->
<div class="hidden part-header">Текст</div>
<div class="item row">
<div class="label-2">Интервал</div>
<div class="col row">
<NumInput class="col-left" v-model="lineInterval" :min="0" :max="200"/>
</div>
</div>
<div class="item row">
<div class="label-2">Параграф</div>
<div class="col row">
<NumInput class="col-left" v-model="p" :min="0" :max="2000"/>
</div>
</div>
<div class="item row">
<div class="label-2">Отступ</div>
<div class="col row">
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Слева/справа
</q-tooltip>
</NumInput>
<div class="q-px-sm"/>
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сверху/снизу
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Сдвиг</div>
<div class="col row">
<NumInput class="col-left" v-model="textVertShift" :min="-100" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
Отрицательное значение сдвигает вверх, положительное -<br>
вниз.
</q-tooltip>
</NumInput>
</div>
</div>
<div class="item row">
<div class="label-2">Скроллинг</div>
<div class="col row">
<NumInput class="col-left" v-model="scrollingDelay" :min="1" :max="10000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Замедление скроллинга в миллисекундах.<br>
Определяет время, за которое текст<br>
прокручивается на одну строку.
</q-tooltip>
</NumInput>
<div class="q-px-sm"/>
<q-select class="col" v-model="scrollingType" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Вид скроллинга: линейный,<br>
ускорение-замедление и пр.
</q-tooltip>
</q-select>
</div>
</div>
<div class="item row">
<div class="label-2">Выравнивание</div>
<div class="col row">
<q-checkbox v-model="textAlignJustify" size="xs" label="По ширине" />
<q-checkbox class="q-ml-sm" v-model="wordWrap" size="xs" label="Перенос по слогам" />
</div>
</div>
<div class="item row">
<div class="label-2"></div>
<div class="col-left column justify-center text-right">
Компактность
</div>
<div class="q-px-sm"/>
<NumInput class="col" v-model="compactTextPerc" :min="0" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Степень компактности текста в процентах.<br>
Чем больше компактность, тем хуже выравнивание<br>
по правому краю.
</q-tooltip>
</NumInput>
</div>
<div class="item row">
<div class="label-2">Обработка</div>
<div class="col row">
<q-checkbox v-model="cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
</div>
</div>
<div class="item row">
<div class="label-2"></div>
<div class="col-left column justify-center text-right">
Добавлять пустые
</div>
<div class="q-px-sm"/>
<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="showImages" size="xs" label="Показывать" />
<q-checkbox class="q-ml-sm" v-model="showInlineImagesInCenter" @input="needReload" :disable="!showImages" size="xs" label="Инлайн в центр">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Выносить все изображения в центр экрана
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
</div>
</div>
<div class="item row">
<div class="label-2"></div>
<div class="col-left column justify-center text-right">
Высота не более
</div>
<div class="q-px-sm"/>
<NumInput class="col" v-model="imageHeightLines" :min="1" :max="100" :disable="!showImages">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Определяет высоту изображения количеством строк.<br>
В случае превышения высоты, изображение будет<br>
уменьшено с сохранением пропорций так, чтобы<br>
помещаться в указанное количество строк.
</q-tooltip>
</NumInput>
</div>

View File

@@ -21,13 +21,15 @@
@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="width: 0px; height: 0px"></canvas>
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
</div>
</template>
@@ -37,8 +39,8 @@ import Vue from 'vue';
import Component from 'vue-class-component';
import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import {sleep} from '../../../share/utils';
import {sleep} from '../../../share/utils';
import bookManager from '../share/bookManager';
import DrawHelper from './DrawHelper';
import rstore from '../../../store/modules/reader';
@@ -130,7 +132,11 @@ class TextPage extends Vue {
await this.doPageAnimation();
}, 10);
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
this.$root.$on('resize', async() => {
this.$nextTick(this.onResize);
await sleep(500);
this.$nextTick(this.onResize);
});
}
mounted() {
@@ -143,6 +149,8 @@ class TextPage extends Vue {
}
calcDrawProps() {
const wideLetter = 'Щ';
//preloaded fonts
this.fontList = [`12px ${this.fontName}`];
@@ -199,6 +207,22 @@ class TextPage extends Vue {
this.drawHelper.lineHeight = this.lineHeight;
this.drawHelper.context = this.context;
//альтернатива context.measureText
if (!this.context.measureText(wideLetter).width) {
const ctx = this.$refs.measureWidth;
this.drawHelper.measureText = function(text, style) {
ctx.innerText = text;
ctx.style.font = this.fontByStyle(style);
return ctx.clientWidth;
};
this.drawHelper.measureTextFont = function(text, font) {
ctx.innerText = text;
ctx.style.font = font;
return ctx.clientWidth;
}
}
//statusBar
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
@@ -211,8 +235,10 @@ class TextPage extends Vue {
this.parsed.wordWrap = this.wordWrap;
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
let t = '';
while (this.drawHelper.measureText(t, {}) < this.w) t += 'Щ';
let t = wideLetter;
if (!this.drawHelper.measureText(t, {}))
throw new Error('Ошибка measureText');
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
this.parsed.maxWordLength = t.length - 1;
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
this.parsed.lineHeight = this.lineHeight;
@@ -221,6 +247,9 @@ class TextPage extends Vue {
this.parsed.imageHeightLines = this.imageHeightLines;
this.parsed.imageFitWidth = this.imageFitWidth;
this.parsed.compactTextPerc = this.compactTextPerc;
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
}
//scrolling page
@@ -247,25 +276,18 @@ class TextPage extends Vue {
async checkLoadedFonts() {
let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
if (loaded.some(r => !r)) {
loaded = await Promise.all(this.fontList.map(font => document.fonts.load(font)));
if (loaded.some(r => !r.length))
throw new Error('some font not loaded');
await Promise.all(this.fontList.map(font => document.fonts.load(font)));
}
}
async loadFonts() {
this.fontsLoading = true;
let inst = null;
let close = null;
(async() => {
await sleep(500);
if (this.fontsLoading)
inst = this.$notify({
title: '',
dangerouslyUseHTMLString: true,
message: 'Загрузка шрифта &nbsp;<i class="el-icon-loading"></i>',
duration: 0
});
close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
})();
if (!this.fontsLoaded)
@@ -277,29 +299,15 @@ class TextPage extends Vue {
this.fontsLoaded[this.fontCssUrl] = 1;
}
const waitingTime = 10*1000;
const delay = 100;
let i = 0;
//ждем шрифты
while (i < waitingTime/delay) {
i++;
try {
await this.checkLoadedFonts();
i = waitingTime;
} catch (e) {
await sleep(delay);
}
}
if (i !== waitingTime) {
this.$notify.error({
title: 'Ошибка загрузки',
message: 'Некоторые шрифты не удалось загрузить'
});
try {
await this.checkLoadedFonts();
} catch (e) {
this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
}
this.fontsLoading = false;
if (inst)
inst.close();
if (close)
close();
}
getSettings() {
@@ -330,11 +338,15 @@ class TextPage extends Vue {
// ширина шрифта некоторое время выдается неверно, поэтому
if (!omitLoadFonts) {
const parsed = this.parsed;
await sleep(100);
let i = 0;
const t = this.parsed.testText;
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
await sleep(100);
if (this.parsed === parsed) {
parsed.force = true;
this.parsed.testWidth = this.drawHelper.measureText(t, {});
this.draw();
parsed.force = false;
}
}
}
@@ -368,47 +380,51 @@ class TextPage extends Vue {
if (this.lastBook) {
(async() => {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10);
try {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
return;
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
return;
}
this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title);
this.parsed = this.book.parsed;
this.page1 = null;
this.page2 = null;
this.statusBar = null;
await this.stopTextScrolling();
await this.calcPropsAndLoadFonts();
this.refreshTime();
if (this.lazyParseEnabled)
this.lazyParsePara();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title);
this.parsed = this.book.parsed;
this.page1 = null;
this.page2 = null;
this.statusBar = null;
await this.stopTextScrolling();
this.calcPropsAndLoadFonts();
this.refreshTime();
if (this.lazyParseEnabled)
this.lazyParsePara();
})();
}
}
@@ -432,13 +448,13 @@ class TextPage extends Vue {
}
async onResize() {
/*this.page1 = null;
this.page2 = null;
this.statusBar = null;*/
this.calcDrawProps();
this.setBackground();
this.draw();
try {
this.calcDrawProps();
this.setBackground();
this.draw();
} catch (e) {
//
}
}
get settings() {
@@ -488,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;
}
@@ -529,7 +545,7 @@ class TextPage extends Vue {
}
this.resolveTransition1Finish = null;
this.doingScrolling = false;
this.$emit('stop-scrolling');
this.doStopScrolling();
this.draw();
}
@@ -868,22 +884,26 @@ 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 newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
this.commit('reader/setSettings', newSettings);
await sleep(50);
@@ -924,69 +944,6 @@ class TextPage extends Vue {
}
}
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;
@@ -1064,7 +1021,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;
@@ -1091,7 +1048,7 @@ class TextPage extends Vue {
} else if (event.button == 1) {
this.doScrollingToggle();
} else if (event.button == 2) {
this.doToolBarToggle();
this.doToolBarToggle(event);
}
}
@@ -1116,7 +1073,7 @@ class TextPage extends Vue {
if (url && url.indexOf('file://') != 0) {
window.open(url, '_blank');
} else {
this.$alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска', '', {type: 'warning'});
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
}
}

View File

@@ -605,6 +605,7 @@ export default class BookParser {
if (!this.force &&
para.parsed &&
para.parsed.testWidth === this.testWidth &&
para.parsed.w === this.w &&
para.parsed.p === this.p &&
para.parsed.wordWrap === this.wordWrap &&
@@ -620,6 +621,7 @@ export default class BookParser {
return para.parsed;
const parsed = {
testWidth: this.testWidth,
w: this.w,
p: this.p,
wordWrap: this.wordWrap,

View File

@@ -464,7 +464,7 @@ class BookManager {
addEventListener(listener) {
if (this.eventListeners.indexOf(listener) < 0)
this.eventListeners.push(listener);
this.eventListeners.push(listener);
}
removeEventListener(listener) {

View File

@@ -1,4 +1,87 @@
export const versionHistory = [
{
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)',
content:
`
<ul>
<li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content:
`
<ul>
<li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content:
`
<ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Settings в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -1,7 +1,7 @@
<template>
<el-container>
<div>
Раздел Sources в разработке
</el-container>
</div>
</template>
<script>

View File

@@ -0,0 +1,64 @@
<template>
<q-dialog v-model="active">
<div class="column bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<slot name="header"></slot>
</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="col q-mx-md">
<slot></slot>
</div>
<div class="row justify-end q-pa-md">
<slot name="footer"></slot>
</div>
</div>
</q-dialog>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
const DialogProps = Vue.extend({
props: {
value: Boolean,
}
})
export default @Component({
})
class Dialog extends DialogProps {
get active() {
return this.value;
}
set active(value) {
this.$emit('input', value);
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.header {
height: 50px;
}
.caption {
font-size: 110%;
overflow: hidden;
}
.close-icon {
width: 50px;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="hidden"></div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Notify extends Vue {
notify(opts) {
let {
caption = null,
captionColor = 'black',
color = 'positive',
icon = '',
iconColor = 'white',
message = '',
messageColor = 'black',
} = opts;
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
return this.$q.notify({
position: 'top-right',
color,
textColor: iconColor,
icon,
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
html: true,
message:
`<div style="max-width: 350px;">
${caption}
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
</div>`
});
}
success(message, caption) {
this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
}
warning(message, caption) {
this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
}
error(message, caption) {
this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
}
info(message, caption) {
this.notify({color: 'info', icon: 'la la-bell', message, caption});
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -0,0 +1,185 @@
<template>
<q-input outlined dense
v-model="filteredValue"
input-style="text-align: center"
class="no-mp"
:class="(error ? 'error' : '')"
:disable="disable"
>
<slot></slot>
<template v-slot:prepend>
<q-icon :class="(validate(value - step) ? '' : 'disable')"
name="la la-minus-circle"
class="button"
v-ripple="validate(value - step)"
@click="minus"
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
@mouseup.prevent.stop="onMouseUp"
@mouseout.prevent.stop="onMouseUp"
@touchstart.stop="onTouchStart($event, 'minus')"
@touchend.stop="onTouchEnd"
@touchcancel.prevent.stop="onTouchEnd"
/>
</template>
<template v-slot:append>
<q-icon :class="(validate(value + step) ? '' : 'disable')"
name="la la-plus-circle"
class="button"
v-ripple="validate(value + step)"
@click="plus"
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
@mouseup.prevent.stop="onMouseUp"
@mouseout.prevent.stop="onMouseUp"
@touchstart.stop="onTouchStart($event, 'plus')"
@touchend.stop="onTouchEnd"
@touchcancel.prevent.stop="onTouchEnd"
/>
</template>
</q-input>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import * as utils from '../../share/utils';
const NumInputProps = Vue.extend({
props: {
value: Number,
min: { type: Number, default: -Number.MAX_VALUE },
max: { type: Number, default: Number.MAX_VALUE },
step: { type: Number, default: 1 },
digits: { type: Number, default: 0 },
disable: Boolean
}
});
export default @Component({
watch: {
filteredValue: function(newValue) {
if (this.validate(newValue)) {
this.error = false;
this.$emit('input', this.string2number(newValue));
} else {
this.error = true;
}
},
value: function(newValue) {
this.filteredValue = newValue;
},
}
})
class NumInput extends NumInputProps {
filteredValue = 0;
error = false;
created() {
this.filteredValue = this.value;
}
string2number(value) {
return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
}
validate(value) {
let n = this.string2number(value);
if (isNaN(n))
return false;
if (n < this.min)
return false;
if (n > this.max)
return false;
return true;
}
plus() {
const newValue = this.value + this.step;
if (this.validate(newValue))
this.filteredValue = newValue;
}
minus() {
const newValue = this.value - this.step;
if (this.validate(newValue))
this.filteredValue = newValue;
}
onMouseDown(event, way) {
this.startClickRepeat = true;
this.clickRepeat = false;
if (event.button == 0) {
(async() => {
await utils.sleep(300);
if (this.startClickRepeat) {
this.clickRepeat = true;
while (this.clickRepeat) {
if (way == 'plus') {
this.plus();
} else {
this.minus();
}
await utils.sleep(50);
}
}
})();
}
}
onMouseUp() {
if (this.inTouch)
return;
this.startClickRepeat = false;
this.clickRepeat = false;
}
onTouchStart(event, way) {
if (!this.$isMobileDevice)
return;
if (event.touches.length == 1) {
this.inTouch = true;
this.onMouseDown({button: 0}, way);
}
}
onTouchEnd() {
if (!this.$isMobileDevice)
return;
this.inTouch = false;
this.onMouseUp();
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.no-mp {
margin: 0;
padding: 0;
}
.button {
font-size: 130%;
border-radius: 20px;
color: #bbb;
cursor: pointer;
}
.button:hover {
color: #616161;
background-color: #efebe9;
}
.error {
background-color: #ffabab;
border-radius: 3px;
}
.disable, .disable:hover {
cursor: not-allowed;
color: #bbb;
background-color: white;
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
<slot></slot>
<!--------------------------------------------------->
<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>
<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>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
</div>
</div>
<!--------------------------------------------------->
<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>
<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>
<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">OK</q-btn>
</div>
</div>
<!--------------------------------------------------->
<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>
<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>
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
<div class="error"><span v-show="error != ''">{{ error }}</span></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">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="las la-exclamation-circle" 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>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import * as utils from '../../share/utils';
export default @Component({
watch: {
inputValue: function(newValue) {
this.validate(newValue);
},
}
})
class StdDialog extends Vue {
caption = '';
message = '';
active = false;
type = '';
inputValue = '';
error = '';
iconColor = '';
hotKeyCode = '';
created() {
if (this.$root.addKeyHook) {
this.$root.addKeyHook(this.keyHook);
}
}
init(message, caption, opts) {
this.caption = caption;
this.message = message;
this.ok = false;
this.type = '';
this.inputValidator = null;
this.inputValue = '';
this.error = '';
this.iconColor = 'text-warning';
if (opts && opts.color) {
this.iconColor = `text-${opts.color}`;
}
this.hotKeyCode = '';
if (opts && opts.hotKeyCode) {
this.hotKeyCode = opts.hotKeyCode;
}
}
onHide() {
if (this.hideTrigger) {
this.hideTrigger();
this.hideTrigger = null;
}
}
onShow() {
if (this.type == 'prompt') {
this.enableValidator = true;
if (this.inputValue)
this.validate(this.inputValue);
this.$refs.input.focus();
}
}
validate(value) {
if (!this.enableValidator)
return false;
if (this.inputValidator) {
const result = this.inputValidator(value);
if (result !== true) {
this.error = result;
return false;
}
}
this.error = '';
return true;
}
okClick() {
if (this.type == 'prompt' && !this.validate(this.inputValue)) {
this.$refs.dialog.shake();
return;
}
if (this.type == 'hotKey' && this.hotKeyCode == '') {
this.$refs.dialog.shake();
return;
}
this.ok = true;
this.$refs.dialog.hide();
}
alert(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'alert';
this.active = true;
});
}
confirm(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'confirm';
this.active = true;
});
}
prompt(message, caption, opts) {
return new Promise((resolve) => {
this.enableValidator = false;
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve({value: this.inputValue});
} else {
resolve(false);
}
};
this.type = 'prompt';
if (opts) {
this.inputValidator = opts.inputValidator || null;
this.inputValue = opts.inputValue || '';
}
this.active = true;
});
}
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) {
let handled = false;
if (this.type == 'hotKey') {
if (event.type == 'keydown') {
this.hotKeyCode = utils.keyEventToCode(event);
handled = true;
}
} else {
if (event.code == 'Enter') {
this.okClick();
handled = true;
}
if (event.code == 'Escape') {
this.$nextTick(() => {
this.$refs.dialog.hide();
});
handled = true;
}
}
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.header {
height: 50px;
}
.caption {
font-size: 110%;
overflow: hidden;
}
.close-icon {
width: 50px;
}
.buttons {
height: 60px;
}
.error {
height: 20px;
font-size: 80%;
color: red;
}
</style>

View File

@@ -1,12 +1,13 @@
<template>
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
<div ref="windowBox" class="windowBox" @click.stop>
<div class="window">
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
<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="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
<span class="header-text"><slot name="header"></slot></span>
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
<span class="header-text col"><slot name="header"></slot></span>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
</div>
<slot></slot>
</div>
</div>
@@ -116,23 +117,20 @@ class Window extends Vue {
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
background-color: transparent !important;
z-index: 50;
}
.windowBox {
position: absolute;
display: flex;
.xyfit {
height: 100%;
width: 100%;
}
.window {
.flexfit {
flex: 1;
display: flex;
flex-direction: column;
}
.window {
margin: 10px;
background-color: #ffffff;
border: 3px double black;
@@ -141,23 +139,21 @@ class Window extends Vue {
}
.header {
display: flex;
justify-content: flex-end;
background-color: #59B04F;
background: linear-gradient(to bottom right, green, #59B04F);
align-items: center;
height: 30px;
}
.header-text {
flex: 1;
margin-left: 10px;
margin-right: 10px;
color: yellow;
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
overflow: hidden;
white-space: nowrap;
}
.close-button {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
cursor: pointer;
@@ -166,4 +162,5 @@ class Window extends Vue {
.close-button:hover {
background-color: #69C05F;
}
</style>

View File

@@ -1,68 +0,0 @@
import Vue from 'vue';
/*
import ElementUI from 'element-ui';
import './theme/index.css';
import locale from 'element-ui/lib/locale/lang/ru-RU';
Vue.use(ElementUI, { locale });
*/
//------------------------------------------------------
import './theme/index.css';
import ElMenu from 'element-ui/lib/menu';
import ElMenuItem from 'element-ui/lib/menu-item';
import ElButton from 'element-ui/lib/button';
import ElButtonGroup from 'element-ui/lib/button-group';
import ElCheckbox from 'element-ui/lib/checkbox';
import ElTabs from 'element-ui/lib/tabs';
import ElTabPane from 'element-ui/lib/tab-pane';
import ElTooltip from 'element-ui/lib/tooltip';
import ElCol from 'element-ui/lib/col';
import ElContainer from 'element-ui/lib/container';
import ElAside from 'element-ui/lib/aside';
import ElHeader from 'element-ui/lib/header';
import ElMain from 'element-ui/lib/main';
import ElInput from 'element-ui/lib/input';
import ElInputNumber from 'element-ui/lib/input-number';
import ElSelect from 'element-ui/lib/select';
import ElOption from 'element-ui/lib/option';
import ElTable from 'element-ui/lib/table';
import ElTableColumn from 'element-ui/lib/table-column';
import ElProgress from 'element-ui/lib/progress';
import ElSlider from 'element-ui/lib/slider';
import ElForm from 'element-ui/lib/form';
import ElFormItem from 'element-ui/lib/form-item';
import ElColorPicker from 'element-ui/lib/color-picker';
import ElDialog from 'element-ui/lib/dialog';
import Notification from 'element-ui/lib/notification';
import Loading from 'element-ui/lib/loading';
import MessageBox from 'element-ui/lib/message-box';
const components = {
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
ElCol, ElContainer, ElAside, ElMain, ElHeader,
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
ElProgress, ElSlider, ElForm, ElFormItem,
ElColorPicker, ElDialog,
};
for (let name in components) {
Vue.component(name, components[name]);
}
//Vue.use(Loading.directive);
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
//Vue.prototype.$message = Message;
import lang from 'element-ui/lib/locale/lang/ru-RU';
import locale from 'element-ui/lib/locale';
locale.use(lang);

View File

@@ -1,11 +1,12 @@
<!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>

View File

@@ -2,7 +2,7 @@ import Vue from 'vue';
import router from './router';
import store from './store';
import './element';
import './quasar';
import App from './components/App.vue';
//Vue.config.productionTip = false;

89
client/quasar.js Normal file
View File

@@ -0,0 +1,89 @@
import Vue from 'vue';
import 'quasar/dist/quasar.css';
import Quasar from 'quasar/src/vue-plugin.js'
//config
const config = {};
//components
//import {QLayout} from 'quasar/src/components/layout';
//import {QPageContainer, QPage} from 'quasar/src/components/page';
//import {QDrawer} from 'quasar/src/components/drawer';
import {QCircularProgress} from 'quasar/src/components/circular-progress';
import {QInput} from 'quasar/src/components/input';
import {QBtn} from 'quasar/src/components/btn';
import {QBtnGroup} from 'quasar/src/components/btn-group';
import {QBtnToggle} from 'quasar/src/components/btn-toggle';
import {QIcon} from 'quasar/src/components/icon';
import {QSlider} from 'quasar/src/components/slider';
import {QTabs, QTab} from 'quasar/src/components/tabs';
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
import {QSeparator} from 'quasar/src/components/separator';
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
import {QTooltip} from 'quasar/src/components/tooltip';
import {QSpinner} from 'quasar/src/components/spinner';
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
import {QCheckbox} from 'quasar/src/components/checkbox';
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';
const components = {
//QLayout,
//QPageContainer, QPage,
//QDrawer,
QCircularProgress,
QInput,
QBtn,
QBtnGroup,
QBtnToggle,
QIcon,
QSlider,
QTabs, QTab,
//QTabPanels, QTabPanel,
QSeparator,
QList, QItem, QItemSection, QItemLabel,
QTooltip,
QSpinner,
QTable, QTh, QTr, QTd,
QCheckbox,
QSelect,
QColor,
QPopupProxy,
QDialog,
QChip,
};
//directives
import Ripple from 'quasar/src/directives/Ripple';
import ClosePopup from 'quasar/src/directives/ClosePopup';
const directives = {Ripple, ClosePopup};
//plugins
import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
import Notify from 'quasar/src/plugins/Notify';
const plugins = {
AppFullscreen,
Notify,
};
//use
Vue.use(Quasar, { config, components, directives, plugins });
//icons
//import '@quasar/extras/material-icons/material-icons.css';
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
import '@quasar/extras/line-awesome/line-awesome.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import lineAwesome from 'quasar/icon-set/line-awesome.js'
Quasar.iconSet.set(lineAwesome);

View File

@@ -193,4 +193,47 @@ export function parseQuery(str) {
query[first] = [query[first], second];
}
return query;
}
}
export function escapeXml(str) {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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,73 @@
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки',
'settings': 'Настроить',
'undoAction': 'Действие назад',
'redoAction': 'Действие вперед',
'fullScreen': 'На весь экран',
'scrolling': 'Плавный скроллинг',
'stopScrolling': '',
'setPosition': 'Установить позицию',
'search': 'Найти в тексте',
'copyText': 'Скопировать текст со страницы',
'refresh': 'Принудительно обновить книгу',
'offlineMode': 'Автономный режим (без интернета)',
'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: 'refresh', show: true},
{name: 'offlineMode', show: false},
{name: 'recentBooks', show: true},
];
//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: 'refresh', codes: ['R']},
{name: 'offlineMode', codes: ['O']},
{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 +194,7 @@ const webFonts = [
];
//----------------------------------------------------------------------------------------------------------
const settingDefaults = {
textColor: '#000000',
backgroundColor: '#EBE2C9',
@@ -160,11 +219,12 @@ 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
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
pageChangeAnimationSpeed: 80, //0-100%
allowUrlParamBookPos: false,
@@ -182,10 +242,12 @@ const settingDefaults = {
imageFitWidth: true,
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
enableSitesFilter: true,
fontShifts: {},
showToolButton: {},
userHotKeys: {},
};
for (const font of fonts)
@@ -194,6 +256,8 @@ 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;
// initial state
const state = {
@@ -204,6 +268,7 @@ const state = {
profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '',
donationRemindDate: '',
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settingsRev: {},
@@ -238,6 +303,9 @@ const mutations = {
setWhatsNewContentHash(state, value) {
state.whatsNewContentHash = value;
},
setDonationRemindDate(state, value) {
state.donationRemindDate = value;
},
setCurrentProfile(state, value) {
state.currentProfile = value;
},
@@ -250,7 +318,9 @@ const mutations = {
};
export default {
readerActions,
toolButtons,
hotKeys,
fonts,
webFonts,
settingDefaults,

Binary file not shown.

File diff suppressed because one or more lines are too long

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.omnireader/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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ server {
server_name omnireader.ru;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;
@@ -18,11 +19,18 @@ server {
proxy_pass http://127.0.0.1:44081;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
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,59 @@
server {
listen 80;
server_name 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:44081;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
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 old.omnireader.ru;
client_max_body_size 50m;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
root /home/oldreader;
index index.html;
# Обработка php файлов с помощью fpm
location ~ \.php$ {
try_files $uri =404;
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
}

View File

@@ -1,3 +1,11 @@
#!/bin/sh
#!/bin/bash
sudo -H -u www-data sh -c "cd /var/www; /home/liberama/liberama"
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;"

3901
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.8.1",
"version": "0.9.3",
"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,26 +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": {
"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",
@@ -71,19 +70,21 @@
"lodash": "^4.17.15",
"minimist": "^1.2.0",
"multer": "^1.4.2",
"node-stream-zip": "^1.8.2",
"pako": "^1.0.10",
"pako": "^1.0.11",
"path-browserify": "^1.0.0",
"quasar": "^1.11.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",
"zip-stream": "^2.1.2"
"vue": "github:bookpauk/vue",
"vue-router": "^3.1.6",
"vuex": "^3.1.2",
"vuex-persistedstate": "^2.7.1",
"webdav": "^2.10.2",
"ws": "^7.2.1",
"zip-stream": "^2.1.3"
}
}

View File

@@ -22,6 +22,7 @@ module.exports = {
maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false,
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
db: [
{
@@ -45,5 +46,14 @@ module.exports = {
},
],
remoteWebDavStorage: false,
/*
remoteWebDavStorage: {
url: '127.0.0.1:1900',
username: '',
password: '',
},
*/
};

View File

@@ -10,6 +10,7 @@ const propsToSave = [
'useExternalBookConverter',
'servers',
'remoteWebDavStorage',
];
let instance = null;

View File

@@ -3,8 +3,11 @@ const _ = require('lodash');
class MiscController extends BaseController {
async getConfig(req, res) {
if (Array.isArray(req.body.params))
return _.pick(this.config, req.body.params);
if (Array.isArray(req.body.params)) {
const paramsSet = new Set(req.body.params);
return _.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x)));
}
//bad request
res.status(400).send({error: 'params is not an array'});
return false;

View File

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

View File

@@ -0,0 +1,167 @@
const WebSocket = require ('ws');
const _ = require('lodash');
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
const WorkerState = require('../core/WorkerState');//singleton
const log = new (require('../core/AppLogger'))().log;//singleton
const utils = require('../core/utils');
const cleanPeriod = 1*60*1000;//1 минута
const closeSocketOnIdle = 5*60*1000;//5 минут
class WebSocketController {
constructor(wss, config) {
this.config = config;
this.isDevelopment = (config.branch == 'development');
this.readerStorage = new ReaderStorage();
this.readerWorker = new ReaderWorker(config);
this.workerState = new WorkerState();
this.wss = wss;
wss.on('connection', (ws) => {
ws.on('message', (message) => {
this.onMessage(ws, message);
});
});
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
periodicClean() {
try {
const now = Date.now();
this.wss.clients.forEach((ws) => {
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
ws.terminate();
}
});
} finally {
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
async onMessage(ws, message) {
let req = {};
try {
if (this.isDevelopment) {
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
}
ws.lastActivity = Date.now();
req = JSON.parse(message);
switch (req.action) {
case 'test':
await this.test(req, ws); break;
case 'get-config':
await this.getConfig(req, ws); break;
case 'worker-get-state':
await this.workerGetState(req, ws); break;
case 'worker-get-state-finish':
await this.workerGetStateFinish(req, ws); break;
case 'reader-restore-cached-file':
await this.readerRestoreCachedFile(req, ws); break;
case 'reader-storage':
await this.readerStorageDo(req, ws); break;
default:
throw new Error(`Action not found: ${req.action}`);
}
} catch (e) {
this.send({error: e.message}, req, ws);
}
}
send(res, req, ws) {
if (ws.readyState == WebSocket.OPEN) {
ws.lastActivity = Date.now();
let r = res;
if (req.requestId)
r = Object.assign({requestId: req.requestId}, r);
const message = JSON.stringify(r);
ws.send(message);
if (this.isDevelopment) {
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
}
}
}
//Actions ------------------------------------------------------------------
async test(req, ws) {
this.send({message: 'Liberama project is awesome'}, req, ws);
}
async getConfig(req, ws) {
if (Array.isArray(req.params)) {
const paramsSet = new Set(req.params);
this.send(_.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))), req, ws);
} else {
throw new Error('params is not an array');
}
}
async workerGetState(req, ws) {
if (!req.workerId)
throw new Error(`key 'workerId' is wrong`);
const state = this.workerState.getState(req.workerId);
this.send((state ? state : {}), req, ws);
}
async workerGetStateFinish(req, ws) {
if (!req.workerId)
throw new Error(`key 'workerId' is wrong`);
const refreshPause = 200;
let i = 0;
let state = {};
while (1) {// eslint-disable-line no-constant-condition
const prevProgress = state.progress || -1;
const prevState = state.state || '';
const lastModified = state.lastModified || 0;
state = this.workerState.getState(req.workerId);
this.send((state && lastModified != state.lastModified ? state : {}), req, ws);
if (!state) break;
if (state.state != 'finish' && state.state != 'error')
await utils.sleep(refreshPause);
else
break;
i++;
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
}
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
}
}
async readerRestoreCachedFile(req, ws) {
if (!req.path)
throw new Error(`key 'path' is empty`);
const workerId = this.readerWorker.restoreCachedFile(req.path);
const state = this.workerState.getState(workerId);
this.send((state ? state : {}), req, ws);
}
async readerStorageDo(req, ws) {
if (!req.body)
throw new Error(`key 'body' is empty`);
if (!req.body.action)
throw new Error(`key 'action' is empty`);
if (!req.body.items || Array.isArray(req.body.data))
throw new Error(`key 'items' is empty`);
this.send(await this.readerStorage.doAction(req.body), req, ws);
}
}
module.exports = WebSocketController;

View File

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

View File

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

View File

@@ -25,7 +25,8 @@ class AppLogger {
loggerParams = [
{log: 'ConsoleLog'},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO]},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.fatal.log`, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
];
}

View File

@@ -3,14 +3,18 @@ const zlib = require('zlib');
const path = require('path');
const unbzip2Stream = require('unbzip2-stream');
const tar = require('tar-fs');
const ZipStreamer = require('./ZipStreamer');
const iconv = require('iconv-lite');
const utils = require('./utils');
const ZipStreamer = require('./Zip/ZipStreamer');
const appLogger = new (require('./AppLogger'))();//singleton
const FileDetector = require('./FileDetector');
const textUtils = require('./Reader/BookConverter/textUtils');
const utils = require('./utils');
class FileDecompressor {
constructor() {
constructor(limitFileSize = 0) {
this.detector = new FileDetector();
this.limitFileSize = limitFileSize;
}
async decompressNested(filename, outputDir) {
@@ -112,7 +116,25 @@ class FileDecompressor {
async unZip(filename, outputDir) {
const zip = new ZipStreamer();
return await zip.unpack(filename, outputDir);
try {
return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize,
limitFileCount: 1000
});
} catch (e) {
fs.emptyDir(outputDir);
return await zip.unpack(filename, outputDir, {
limitFileSize: this.limitFileSize,
limitFileCount: 1000,
decodeEntryNameCallback: (nameRaw) => {
const enc = textUtils.getEncodingLite(nameRaw);
if (enc.indexOf('ISO-8859') < 0) {
return iconv.decode(nameRaw, enc);
}
return nameRaw;
}
});
}
}
unBz2(filename, outputDir) {
@@ -124,9 +146,16 @@ class FileDecompressor {
}
unTar(filename, outputDir) {
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { (async() => {
const files = [];
if (this.limitFileSize) {
if ((await fs.stat(filename)).size > this.limitFileSize) {
reject('Файл слишком большой');
return;
}
}
const tarExtract = tar.extract(outputDir, {
map: (header) => {
files.push({path: header.name, size: header.size});
@@ -148,7 +177,7 @@ class FileDecompressor {
});
inputStream.pipe(tarExtract);
});
})().catch(reject); });
}
decompressByStream(stream, filename, outputDir) {
@@ -173,6 +202,16 @@ class FileDecompressor {
});
stream.on('error', reject);
if (this.limitFileSize) {
let readSize = 0;
stream.on('data', (buffer) => {
readSize += buffer.length;
if (readSize > this.limitFileSize)
stream.destroy(new Error('Файл слишком большой'));
});
}
inputStream.on('error', reject);
outputStream.on('error', reject);
@@ -189,9 +228,9 @@ class FileDecompressor {
});
}
async gzipFile(inputFile, outputFile) {
async gzipFile(inputFile, outputFile, level = 1) {
return new Promise((resolve, reject) => {
const gzip = zlib.createGzip({level: 1});
const gzip = zlib.createGzip({level});
const input = fs.createReadStream(inputFile);
const output = fs.createWriteStream(outputFile);
@@ -202,13 +241,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);
await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
// переупакуем через некоторое время на максималках, если упаковали плохо
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);
await fs.move(filenameGZ, outFilename, {overwrite: true});
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",
@@ -721,6 +687,43 @@
"rules": [
{ "type": "equal", "start": 64, "end": 68, "bytes": "4d4f4249" }
]
}
},
{
"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

@@ -1,12 +1,11 @@
const got = require('got');
const maxDownloadSize = 50*1024*1024;
class FileDownloader {
constructor() {
constructor(limitDownloadSize = 0) {
this.limitDownloadSize = limitDownloadSize;
}
async load(url, callback) {
async load(url, callback, abort) {
let errMes = '';
const options = {
encoding: null,
@@ -23,10 +22,14 @@ class FileDownloader {
}
let prevProg = 0;
const request = got(url, options).on('downloadProgress', progress => {
if (progress.transferred > maxDownloadSize) {
errMes = 'file too big';
request.cancel();
const request = got(url, options);
request.on('downloadProgress', progress => {
if (this.limitDownloadSize) {
if (progress.transferred > this.limitDownloadSize) {
errMes = 'Файл слишком большой';
request.cancel();
}
}
let prog = 0;
@@ -38,8 +41,12 @@ class FileDownloader {
if (prog != prevProg && callback)
callback(prog);
prevProg = prog;
});
if (abort && abort()) {
errMes = 'abort';
request.cancel();
}
});
try {
return (await request).body;

View File

@@ -3,7 +3,7 @@ const fs = require('fs-extra');
const path = require('path');
const log = new (require('../AppLogger'))().log;//singleton
const ZipStreamer = require('../ZipStreamer');
const ZipStreamer = require('../Zip/ZipStreamer');
const utils = require('../utils');

119
server/core/LimitedQueue.js Normal file
View File

@@ -0,0 +1,119 @@
class LimitedQueue {
constructor(enqueueAfter = 10, size = 100, timeout = 60*60*1000) {//timeout в ms
this.size = size;
this.timeout = timeout;
this.abortCount = 0;
this.enqueueAfter = enqueueAfter;
this.freed = enqueueAfter;
this.listeners = [];
}
_addListener(listener) {
this.listeners.push(listener);
}
//отсылаем сообщение первому ожидающему и удаляем его из списка
_emitFree() {
if (this.listeners.length > 0) {
let listener = this.listeners.shift();
listener.onFree();
for (let i = 0; i < this.listeners.length; i++) {
this.listeners[i].onPlaceChange(i + 1);
}
}
}
get(onPlaceChange) {
return new Promise((resolve, reject) => {
if (this.destroyed)
reject('destroyed');
const take = () => {
if (this.freed <= 0)
throw new Error('Ошибка получения ресурсов в очереди ожидания');
this.freed--;
this.resetTimeout();
let aCount = this.abortCount;
return {
ret: () => {
if (aCount == this.abortCount) {
this.freed++;
this._emitFree();
aCount = -1;
this.resetTimeout();
}
},
abort: () => {
return (aCount != this.abortCount);
},
resetTimeout: this.resetTimeout.bind(this)
};
};
if (this.freed > 0) {
resolve(take());
} else {
if (this.listeners.length < this.size) {
this._addListener({
onFree: () => {
resolve(take());
},
onError: (err) => {
reject(err);
},
onPlaceChange: (i) => {
if (onPlaceChange)
onPlaceChange(i);
}
});
if (onPlaceChange)
onPlaceChange(this.listeners.length);
} else {
reject('Превышен размер очереди ожидания');
}
}
});
}
resetTimeout() {
if (this.timer)
clearTimeout(this.timer);
this.timer = setTimeout(() => { this.clean(); }, this.timeout);
}
clean() {
this.timer = null;
if (this.freed < this.enqueueAfter) {
this.abortCount++;
//чистка listeners
for (const listener of this.listeners) {
listener.onError('Время ожидания в очереди истекло');
}
this.listeners = [];
this.freed = this.enqueueAfter;
}
}
destroy() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
for (const listener of this.listeners) {
listener.onError('destroy');
}
this.listeners = [];
this.abortCount++;
this.destroyed = true;
}
}
module.exports = LimitedQueue;

View File

@@ -226,12 +226,12 @@ class Logger {
// catch ctrl+c event and exit normally
process.on('SIGINT', () => {
this.log(LM_WARN, 'Ctrl-C pressed, exiting...');
this.log(LM_FATAL, 'Ctrl-C pressed, exiting...');
process.exit(2);
});
process.on('SIGTERM', () => {
this.log(LM_WARN, 'Kill signal, exiting...');
this.log(LM_FATAL, 'Kill signal, exiting...');
process.exit(2);
});

View File

@@ -0,0 +1 @@
test

View File

@@ -1,12 +1,12 @@
const fs = require('fs-extra');
const iconv = require('iconv-lite');
const chardet = require('chardet');
const he = require('he');
const LimitedQueue = require('../../LimitedQueue');
const textUtils = require('./textUtils');
const utils = require('../../utils');
let execConverterCounter = 0;
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
class ConvertBase {
constructor(config) {
@@ -32,13 +32,26 @@ class ConvertBase {
throw new Error('Внешний конвертер pdftohtml не найден');
}
async execConverter(path, args, onData) {
execConverterCounter++;
async execConverter(path, args, onData, abort) {
onData = (onData ? onData : () => {});
let q = null;
try {
if (execConverterCounter > 10)
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
q = await queue.get(() => {onData();});
} catch (e) {
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
}
const result = await utils.spawnProcess(path, {args, onData});
try {
const result = await utils.spawnProcess(path, {
killAfter: 600,
args,
onData: (data) => {
q.resetTimeout();
onData(data);
},
abort
});
if (result.code != 0) {
let error = result.code;
if (this.config.branch == 'development')
@@ -48,29 +61,21 @@ class ConvertBase {
} catch(e) {
if (e.status == 'killed') {
throw new Error('Слишком долгое ожидание конвертера');
} else if (e.status == 'abort') {
throw new Error('abort');
} else if (e.status == 'error') {
throw new Error(e.error);
} else {
throw new Error(e);
}
} finally {
execConverterCounter--;
q.ret();
}
}
decode(data) {
let selected = textUtils.getEncoding(data);
if (selected == 'ISO-8859-5') {
const charsetAll = chardet.detectAll(data.slice(0, 20000));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {
selected = charset.name;
break;
}
}
}
if (selected.toLowerCase() != 'utf-8')
return iconv.decode(data, selected);
else

View File

@@ -16,7 +16,7 @@ class ConvertDoc extends ConvertDocX {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const docFile = `${outFile}.doc`;
@@ -24,9 +24,9 @@ class ConvertDoc extends ConvertDocX {
const fb2File = `${outFile}.fb2`;
await fs.copy(inputFiles.sourceFile, docFile);
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile]);
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile], null, abort);
return await super.convert(docxFile, fb2File, callback);
return await super.convert(docxFile, fb2File, callback, abort);
}
}

View File

@@ -20,12 +20,12 @@ class ConvertDocX extends ConvertBase {
return false;
}
async convert(docxFile, fb2File, callback) {
async convert(docxFile, fb2File, callback, abort) {
let perc = 0;
await this.execConverter(this.calibrePath, [docxFile, fb2File], () => {
perc = (perc < 100 ? perc + 5 : 50);
await this.execConverter(this.calibrePath, [docxFile, fb2File, '-vv'], () => {
perc = (perc < 100 ? perc + 1 : 50);
callback(perc);
});
}, abort);
return await fs.readFile(fb2File);
}
@@ -35,7 +35,7 @@ class ConvertDocX extends ConvertBase {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const docxFile = `${outFile}.docx`;
@@ -43,7 +43,7 @@ class ConvertDocX extends ConvertBase {
await fs.copy(inputFiles.sourceFile, docxFile);
return await this.convert(docxFile, fb2File, callback);
return await this.convert(docxFile, fb2File, callback, abort);
}
}

View File

@@ -28,7 +28,7 @@ class ConvertEpub extends ConvertBase {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const epubFile = `${outFile}.epub`;
@@ -37,10 +37,10 @@ class ConvertEpub extends ConvertBase {
await fs.copy(inputFiles.sourceFile, epubFile);
let perc = 0;
await this.execConverter(this.calibrePath, [epubFile, fb2File], () => {
perc = (perc < 100 ? perc + 5 : 50);
await this.execConverter(this.calibrePath, [epubFile, fb2File, '-vv'], () => {
perc = (perc < 100 ? perc + 1 : 50);
callback(perc);
});
}, abort);
return await fs.readFile(fb2File);
}

View File

@@ -6,6 +6,7 @@ class ConvertHtml extends ConvertBase {
check(data, opts) {
const {dataType} = opts;
//html?
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
return {isText: false};
@@ -14,6 +15,11 @@ class ConvertHtml extends ConvertBase {
return {isText: true};
}
//из буфера обмена?
if (data.toString().indexOf('<buffer>') == 0) {
return {isText: false};
}
return false;
}

View File

@@ -16,7 +16,7 @@ class ConvertMobi extends ConvertBase {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const mobiFile = `${outFile}.mobi`;
@@ -25,10 +25,10 @@ class ConvertMobi extends ConvertBase {
await fs.copy(inputFiles.sourceFile, mobiFile);
let perc = 0;
await this.execConverter(this.calibrePath, [mobiFile, fb2File], () => {
perc = (perc < 100 ? perc + 5 : 50);
await this.execConverter(this.calibrePath, [mobiFile, fb2File, '-vv'], () => {
perc = (perc < 100 ? perc + 1 : 50);
callback(perc);
});
}, abort);
return await fs.readFile(fb2File);
}

View File

@@ -18,7 +18,7 @@ class ConvertPdf extends ConvertHtml {
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const {inputFiles, callback, abort} = opts;
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
@@ -27,7 +27,7 @@ class ConvertPdf extends ConvertHtml {
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
perc = (perc < 80 ? perc + 10 : 40);
callback(perc);
});
}, abort);
callback(80);
const data = await fs.readFile(outFile);

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