Compare commits

...

343 Commits

Author SHA1 Message Date
Book Pauk
ea2f178730 Merge branch 'release/0.11.4-2' 2022-04-14 19:54:14 +07:00
Book Pauk
4b5c8d9efe Добавил подсказку 2022-04-14 19:53:47 +07:00
Book Pauk
28ebf13c3a Merge tag '0.11.4-1' into develop
0.11.4-1
2022-04-14 19:19:21 +07:00
Book Pauk
5d52e63dd9 Merge branch 'release/0.11.4-1' 2022-04-14 19:19:14 +07:00
Book Pauk
1a0e024050 Поправил баг 2022-04-14 19:18:49 +07:00
Book Pauk
e627a0d970 Merge tag '0.11.4' into develop
0.11.4
2022-04-14 19:05:36 +07:00
Book Pauk
48668d94ad Merge branch 'release/0.11.4' 2022-04-14 19:05:31 +07:00
Book Pauk
e08c431dd9 Версия 0.11.4 2022-04-14 19:05:07 +07:00
Book Pauk
5ee58ad6f0 Поправка багов 2022-04-14 19:00:04 +07:00
Book Pauk
ac0a4f0586 Добавлена кнопка 'Управление кликом' 2022-04-14 18:50:11 +07:00
Book Pauk
b6f4c153e5 Добавлена кнопка 'Загрузить из буфера обмена' 2022-04-14 18:34:41 +07:00
Book Pauk
4fdaf5f555 Добавлена кнопка 'Загрузить файл с диска' 2022-04-14 17:48:51 +07:00
Book Pauk
b4ee9d6c00 Скрыта опция "Помочь проекту".
Добавлена кнопка "Вызвать справку".
2022-04-14 17:27:29 +07:00
Book Pauk
7c73c74730 Добавлена подсказка при невалидном URL книги 2022-04-14 17:13:38 +07:00
Book Pauk
c20aa089fa npm 2022-03-29 17:45:57 +07:00
Book Pauk
b0e15c22ea Merge tag '0.11.3' into develop
0.11.3
2022-03-29 17:41:03 +07:00
Book Pauk
d58a2c065a Merge branch 'release/0.11.3' 2022-03-29 17:40:57 +07:00
Book Pauk
53135e7ee8 Поправка даты 2022-03-29 17:40:29 +07:00
Book Pauk
5c48ca9e6c Рефакторинг versionHistory, небольшие поправки 2022-03-29 17:37:24 +07:00
Book Pauk
c4a280f3d8 Скрыл устаревший чекбокс 2022-03-29 16:52:03 +07:00
Book Pauk
ba2943c722 Поправлен баг 2022-03-29 16:49:04 +07:00
Book Pauk
26f6ffc83a Убрал PayPal из списка 2022-03-29 16:25:26 +07:00
Book Pauk
bcf075a72c Доработки WebSocketConnection 2022-03-29 16:23:34 +07:00
Book Pauk
02d458d192 Миграция "jembadb" => "^2.3.0" 2022-03-29 15:49:48 +07:00
Book Pauk
a349d8af68 Обновил пакет JembaDb 2022-02-08 20:55:31 +07:00
Book Pauk
0dbaf32aac Merge tag '0.11.2' into develop
0.11.2
2022-01-11 23:25:23 +07:00
Book Pauk
e8c41ef3a8 Merge branch 'release/0.11.2' 2022-01-11 23:24:58 +07:00
Book Pauk
e43a44e986 0.11.2 2022-01-11 23:24:37 +07:00
Book Pauk
f14b8ed277 Добавлена реакция на сигнал SIGUSR2 2022-01-11 23:23:54 +07:00
Book Pauk
bbfe8a64cb Мелкая поправка 2022-01-11 23:11:04 +07:00
Book Pauk
bcf3c2dab0 Улучшение обработки ошибок 2022-01-11 22:23:35 +07:00
Book Pauk
d5404fd260 Убрал устаревший код 2022-01-11 21:30:43 +07:00
Book Pauk
54bc662e43 Поправил конфиг для nginx 2021-12-24 17:59:26 +07:00
Book Pauk
42546ca97e Обновление jembadb до версии 1.3.0 2021-12-21 20:21:32 +07:00
Book Pauk
5c13cf0eb9 Добавил -C GZip для pkg 2021-12-20 17:27:04 +07:00
Book Pauk
2a9d44ae9a Поправка конфига для eslint 2021-12-20 17:26:19 +07:00
Book Pauk
38414ae7b6 Переход на пакет jembadb 2021-12-17 20:05:57 +07:00
Book Pauk
3ecb3e80ac Удалил комментарии 2021-12-12 01:56:24 +07:00
Book Pauk
4968828488 Merge tag '0.11.1-2' into develop
0.11.1-2
2021-12-03 15:25:17 +07:00
Book Pauk
4db3cd24df Merge branch 'release/0.11.1-2' 2021-12-03 15:25:11 +07:00
Book Pauk
45c6d3da77 Поправил таймаут, улучшение скорости синхронизации 2021-12-03 15:16:39 +07:00
Book Pauk
4aab1da3c6 Merge tag '0.11.1-1' into develop
0.11.1-1
2021-12-03 15:03:46 +07:00
Book Pauk
bf5dfa1c15 Merge branch 'release/0.11.1-1' 2021-12-03 15:03:37 +07:00
Book Pauk
7549bdd2b4 Обновил pkg 2021-12-03 15:02:56 +07:00
Book Pauk
1bb2525ab2 Merge tag '0.11.1' into develop
0.11.1
2021-12-03 14:35:04 +07:00
Book Pauk
22a556f612 Merge branch 'release/0.11.1' 2021-12-03 14:34:56 +07:00
Book Pauk
056611e87c Версия 0.11.1 2021-12-03 14:34:36 +07:00
Book Pauk
6debe24880 Удален более ненужный файл 2021-12-03 14:30:57 +07:00
Book Pauk
56559bddab Мелкий рефакторинг 2021-12-03 14:28:17 +07:00
Book Pauk
9ec74eccb4 Добавлен папаметр forceAutoRepair 2021-12-03 14:21:50 +07:00
Book Pauk
3d2f45c20d Мелие поправки 2021-12-03 14:21:36 +07:00
Book Pauk
fb2eedd5ba Добавлен конвертер SQLITE -> JambaDb 2021-12-03 14:07:32 +07:00
Book Pauk
e278b4a00e Мелкие поправки 2021-12-02 18:39:28 +07:00
Book Pauk
0beaa611f6 Переход на JembaDb 2021-12-02 18:36:49 +07:00
Book Pauk
14ca2daa39 Небольшой рефакторинг 2021-12-01 22:09:48 +07:00
Book Pauk
714eb3ae83 Поправки по результату тестирования 2021-12-01 21:26:26 +07:00
Book Pauk
6286d663c9 Поправлен баг 2021-12-01 19:27:16 +07:00
Book Pauk
b5db2079d2 Jemba-миграции 2021-12-01 17:50:48 +07:00
Book Pauk
b3b30b9bd9 Поправил триггер для autorepair 2021-11-24 15:15:22 +07:00
Book Pauk
0b6a726503 Новый движок БД 2021-11-24 14:15:09 +07:00
Book Pauk
609334c5a6 Пометил модули устаревшими 2021-11-24 14:14:24 +07:00
Book Pauk
4852c7aec3 Добавлен модуль AsyncExit для выполненния cleanup-процедур перед выходом из приложения 2021-11-24 14:13:13 +07:00
Book Pauk
b1e3d33694 Merge tag '0.11.0-1' into develop
0.11.0-1
2021-11-22 21:12:42 +07:00
Book Pauk
2bfc557071 Merge branch 'release/0.11.0-1' 2021-11-22 21:12:35 +07:00
Book Pauk
e1216109bc Поправлен баг с maxBodyLength клиента WebDav 2021-11-22 21:12:02 +07:00
Book Pauk
990b8f390c Merge tag '0.11.0' into develop
0.11.0
2021-11-18 18:43:53 +07:00
Book Pauk
e6f6cd4ff3 Merge branch 'release/0.11.0' 2021-11-18 18:43:45 +07:00
Book Pauk
7deb745651 Поправил ссылку на инструкцию certbot 2021-11-18 18:28:25 +07:00
Book Pauk
70f3ca8067 Добавил beta-конфиг nginx для http 2021-11-18 18:25:04 +07:00
Book Pauk
bb8497a997 Поправил ошибку в доке 2021-11-18 18:22:08 +07:00
Book Pauk
2127e2ec0a Версия 0.11.0 2021-11-18 18:17:22 +07:00
Book Pauk
9be4011d54 Небольшая поправка формирования заголовка 2021-11-18 17:56:54 +07:00
Book Pauk
c534edfeb5 Поправки 2021-11-16 15:41:51 +07:00
Book Pauk
adc8cd7243 Переход на Vue 3 2021-11-16 15:05:00 +07:00
Book Pauk
522d2d3b9c Актуализация пакетов 2021-11-16 14:41:53 +07:00
Book Pauk
046933a05e Поправка багов 2021-11-16 14:32:54 +07:00
Book Pauk
9143288de2 Поправка копирования assets 2021-11-16 14:03:21 +07:00
Book Pauk
6053ca6c0e Настройка правильных редиректов роутера 2021-11-07 15:38:17 +07:00
Book Pauk
084197530e Форматирование кода 2021-11-07 15:38:05 +07:00
Book Pauk
9f366ca811 Поправлен баг resize 2021-11-07 14:49:33 +07:00
Book Pauk
7c07e6f004 Поправка бага 2021-11-01 19:08:17 +07:00
Book Pauk
3d4d7e0342 Переход на Vue 3 2021-11-01 18:23:58 +07:00
Book Pauk
1a8f241aad Переход на Vue 3, небольшая реструктуризация файлов 2021-11-01 17:56:45 +07:00
Book Pauk
33e938b76a Поправлен баг 2021-10-31 21:51:03 +07:00
Book Pauk
e2db546066 Переход на Vue 3 2021-10-31 21:28:31 +07:00
Book Pauk
def9ee52e2 Поправка разметки 2021-10-31 13:19:11 +07:00
Book Pauk
1afe10be03 Переход на Vue 3 2021-10-31 13:14:12 +07:00
Book Pauk
fa44641fa2 Актуализация пакетов 2021-10-31 13:00:41 +07:00
Book Pauk
9a1ef85c93 Переход на Vue 3 2021-10-29 19:11:10 +07:00
Book Pauk
b848cf5aa7 Переход на Vue 3 2021-10-29 18:24:23 +07:00
Book Pauk
8057e18ebc Переход на Vue 3 2021-10-29 16:33:38 +07:00
Book Pauk
76e09ef34e Переход на Vue 3, в процессе 2021-10-29 15:27:04 +07:00
Book Pauk
00cb2dc274 Переход на Vue 3, в процессе 2021-10-29 12:56:28 +07:00
Book Pauk
ed46e91432 Переход на Vue 3, в процессе 2021-10-29 12:21:53 +07:00
Book Pauk
88d75fb0d8 Переход на Vue 3, в процессе 2021-10-28 16:55:44 +07:00
Book Pauk
a1d7a73459 Переход на Vue 3, в процессе 2021-10-28 15:17:19 +07:00
Book Pauk
687f89729b Переход на Vue 3, в процессе 2021-10-28 14:53:22 +07:00
Book Pauk
6bf678e01f Функция для преобразования Vue-класса во Vue-компонент 2021-10-28 13:52:25 +07:00
Book Pauk
a18aec2f96 Переход на Vue 3 - начало, пока ничего не работает 2021-10-27 23:09:20 +07:00
Book Pauk
1c0cf303a0 Поправка настроек eslint 2021-10-27 16:11:20 +07:00
Book Pauk
5c7ae73982 Поправки по требованиям eslint 2021-10-27 15:07:25 +07:00
Book Pauk
4e9c69a1cf Настройка eslint 2021-10-27 15:05:37 +07:00
Book Pauk
78375be8bf Поправки по требованиям eslint 2021-10-27 15:05:18 +07:00
Book Pauk
b684725094 Настройка eslint 2021-10-27 04:11:07 +07:00
Book Pauk
ff52602c3a Мелкие поправки 2021-10-27 04:10:19 +07:00
Book Pauk
ce704c5e26 Актуализация пакетов 2021-10-27 01:28:16 +07:00
Book Pauk
4503e4ed17 Актуализация пакетов 2021-10-27 01:03:47 +07:00
Book Pauk
01c384c43a Актуализация пакетов 2021-10-27 00:52:57 +07:00
Book Pauk
dda2de58a8 Актуализация пакетов, в процессе 2021-10-27 00:06:42 +07:00
Book Pauk
0365acbf7a Актуализация пакетов 2021-10-26 01:05:09 +07:00
Book Pauk
bbf1ab7180 Актуализация пакетов 2021-10-26 00:35:45 +07:00
Book Pauk
83bf1f1d3a Актуализация пакетов 2021-10-26 00:18:18 +07:00
Book Pauk
fdf04fed0e Актуализация пакетов 2021-10-25 23:55:26 +07:00
Book Pauk
acce32bfa7 Актуализация пакетов 2021-10-25 16:15:15 +07:00
Book Pauk
614c45ac7d Актуализировал readme 2021-10-25 15:31:07 +07:00
Book Pauk
c4c0199a1b Merge tag '0.10.3' into develop
0.10.3
2021-10-25 01:54:07 +07:00
Book Pauk
a53ebb9355 Merge branch 'release/0.10.3' 2021-10-25 01:54:00 +07:00
Book Pauk
06e12930c7 Актуализирован конвертер для samlib.ru 2021-10-25 01:01:26 +07:00
Book Pauk
0f7655773a Версия 0.10.3 2021-10-20 18:29:52 +07:00
Book Pauk
26660461d4 Исправлен баг парсера с пустыми параграфами (содержащими только разметку) 2021-10-20 18:05:38 +07:00
Book Pauk
b41ee91db5 Актуализировал инструкцию 2021-10-20 15:49:07 +07:00
Book Pauk
746dd8d37a Актуализация инструкции 2021-10-20 15:42:09 +07:00
Book Pauk
fb4a57027d Merge tag '0.10.2' into develop
0.10.2
2021-10-19 23:33:48 +07:00
Book Pauk
c97660bed0 Merge branch 'release/0.10.2' 2021-10-19 23:33:38 +07:00
Book Pauk
fd8c8812a3 Версия 0.10.2 2021-10-19 23:26:45 +07:00
Book Pauk
0101392858 Актуализированы версии пакетов 2021-10-19 19:55:46 +07:00
Book Pauk
cc3f82d693 Обновление пакетов pkg и sqlite 2021-10-19 02:48:08 +07:00
Book Pauk
d21997c918 Подготовка конфигов и инструкций к разворачиванию на Ubuntu 20.04 2021-10-18 20:25:45 +07:00
Book Pauk
74fec12f5c Merge tag '0.10.1-1' into develop
0.10.1-1
2021-10-10 19:08:28 +07:00
Book Pauk
59525f8fa7 Merge branch 'release/0.10.1-1' 2021-10-10 19:08:20 +07:00
Book Pauk
3c6d3befb2 Поправил историю версий 2021-10-10 19:07:48 +07:00
Book Pauk
dfa72c80bc Merge tag '0.10.1' into develop
0.10.1
2021-10-10 18:35:24 +07:00
Book Pauk
c6e534b9db Merge branch 'hotfix/0.10.1' 2021-10-10 18:35:11 +07:00
Book Pauk
032ab6a85d Хотфикс для исправления проблемы с пустой БД storage при инициализации 2021-10-10 18:34:18 +07:00
Book Pauk
830c066ebf Обновил иконку 2021-10-10 18:25:32 +07:00
Book Pauk
c432388515 Поправил иконку 2021-02-11 22:51:41 +07:00
Book Pauk
476deba93a Заменил иконку 2021-02-11 22:07:58 +07:00
Book Pauk
ffb4f2386d Merge tag '0.10.0-2' into develop
0.10.0-2
2021-02-10 20:19:51 +07:00
Book Pauk
21716163cb Merge branch 'release/0.10.0-2' 2021-02-10 20:19:47 +07:00
Book Pauk
ca924148a5 Поправки багов 2021-02-10 20:18:41 +07:00
Book Pauk
37aa9b84ae Merge tag '0.10.0-1' into develop
0.10.0-1
2021-02-10 15:41:24 +07:00
Book Pauk
c7bd7d4d7d Merge branch 'release/0.10.0-1' 2021-02-10 15:41:19 +07:00
Book Pauk
d81a50e696 Поправки багов 2021-02-10 15:40:44 +07:00
Book Pauk
dda9943dbe Merge tag '0.10.0' into develop
0.10.0
2021-02-10 03:23:57 +07:00
Book Pauk
2b4b9f24a1 Merge branch 'release/0.10.0' 2021-02-10 03:23:50 +07:00
Book Pauk
2af77f22d6 Мелкая поправка 2021-02-10 03:22:20 +07:00
Book Pauk
f142e5812d Добавлена опция "Не включать строку статуса в обои" 2021-02-10 03:18:47 +07:00
Book Pauk
ed901fc181 Добавлена возможность загружать пользовательские обои, пока без синхронизации 2021-02-10 02:55:47 +07:00
Book Pauk
87a068899a Поправки wallpaper 2021-02-09 22:29:20 +07:00
Book Pauk
115f683128 Улучшение отображения селектора обоев 2021-02-09 21:55:19 +07:00
Book Pauk
111568fc2e Поправлен баг 2021-02-09 21:16:17 +07:00
Book Pauk
825136b5ff 0.10.0 2021-02-09 21:05:26 +07:00
Book Pauk
eae34b1121 История 2021-02-09 21:04:56 +07:00
Book Pauk
b9d7a6a3bb Убрал дебаг 2021-02-09 21:00:51 +07:00
Book Pauk
1e5375f8f9 Рефакторинг 2021-02-09 21:00:18 +07:00
Book Pauk
f597c603bf Добавил цвета для статусбара и разделителя 2021-02-09 18:43:43 +07:00
Book Pauk
b93dd0a59e Поправка 2021-02-09 18:08:13 +07:00
Book Pauk
a5740e4349 Доработки 2021-02-09 18:07:02 +07:00
Book Pauk
dacbd05911 Работа над двухстраничным режимом 2021-02-09 17:47:10 +07:00
Book Pauk
65c66e0feb Работа над двухстраничным режимом 2021-02-09 15:46:57 +07:00
Book Pauk
52f9131f99 Доработки двухстраничного режима 2021-02-04 20:34:25 +07:00
Book Pauk
cfc946ad12 Работа над двухстраничным режимом 2021-02-04 20:08:06 +07:00
Book Pauk
a207a0554c Работа на двухстраничным режимом 2021-02-04 15:55:12 +07:00
Book Pauk
675e898163 Работа над двухстраничным режимом 2021-02-04 15:18:32 +07:00
Book Pauk
d2167d8605 Работа над двухстраничным режимом 2021-02-02 18:09:21 +07:00
Book Pauk
de849d3447 Рефакторинг 2021-02-02 18:08:55 +07:00
Book Pauk
6c20b0b83e Улучшения SqliteConnectionPool 2021-02-01 18:05:32 +07:00
Book Pauk
a09b70a991 Рефакторинг WebSocketConnection, небольшие улучшения 2021-02-01 17:57:24 +07:00
Book Pauk
2427a3e08b Поправка версии node 2020-12-30 03:41:59 +07:00
Book Pauk
1104f9b850 Небольшая поправка 2020-12-24 21:39:39 +07:00
Book Pauk
dc48700e9e Небольшая поправка 2020-12-24 21:35:31 +07:00
Book Pauk
f0b0c39328 Поправки по результату тестирования, незначительные улучшения 2020-12-24 20:51:02 +07:00
Book Pauk
aad74cf682 Поправки по результату тестирования, оптимизации 2020-12-24 18:32:57 +07:00
Book Pauk
d449478204 Небольшое форматирование 2020-12-24 18:21:18 +07:00
Book Pauk
d4f6536caa Поправки по результату тестирования 2020-12-24 16:33:44 +07:00
Book Pauk
1eac00f71c Поправка багов 2020-12-24 00:44:38 +07:00
Book Pauk
ca1170a9f0 Поправки по результату тестирования 2020-12-24 00:25:54 +07:00
Book Pauk
79dda03bac Рефакторинг, плюс небольшое улучшение механизма загрузки шрифта 2020-12-23 22:38:52 +07:00
Book Pauk
6c8e0b8573 Поправил баг 2020-12-23 22:23:37 +07:00
Book Pauk
17c14722fd Рефакторинг 2020-12-23 21:17:39 +07:00
Book Pauk
48612ee118 Поправлен баг 2020-12-22 02:24:46 +07:00
Book Pauk
205c676999 Переименование YandexMoney -> ЮMoney 2020-12-21 19:50:27 +07:00
Book Pauk
54e0dd0478 В список недавних добавлена полоска прочитанного 2020-12-21 18:08:35 +07:00
Book Pauk
2de8d7515e Добалвлен крестик в строку поиска 2020-12-21 17:48:49 +07:00
Book Pauk
a251d16432 Merge tag '0.9.12-1' into develop
0.9.12-1
2020-12-19 21:23:43 +07:00
Book Pauk
599caba912 Merge branch 'release/0.9.12-1' 2020-12-19 21:23:38 +07:00
Book Pauk
3477c43465 Поправка по результату чтения логов 2020-12-19 21:19:58 +07:00
Book Pauk
200dac7946 Небольшая поправка 2020-12-19 21:17:31 +07:00
Book Pauk
e60829946d Merge tag '0.9.12' into develop
0.9.12
2020-12-19 03:23:32 +07:00
Book Pauk
ef12a84285 Merge branch 'release/0.9.12' 2020-12-19 03:23:27 +07:00
Book Pauk
6a18ae3f27 Версия 0.9.12 2020-12-19 03:22:47 +07:00
Book Pauk
a250e95950 Поправил баг 2020-12-19 03:07:59 +07:00
Book Pauk
b174ae452b Оптимизации проверок типа файла 2020-12-19 03:05:58 +07:00
Book Pauk
0b63bce357 Исправления багов 2020-12-19 02:47:06 +07:00
Book Pauk
de0d10e792 Мелкая поправка 2020-12-19 02:46:46 +07:00
Book Pauk
b358b340b4 Улучшение формирования оглавления 2020-12-19 00:20:11 +07:00
Book Pauk
455aba7f4f Мелкая поправка текста 2020-12-19 00:17:36 +07:00
Book Pauk
fde0437157 Добавлено извлечение схемы документа в ConvertPdfImages, мелкий рефакторинг 2020-12-18 23:56:55 +07:00
Book Pauk
480c95bd63 Добавлена возможность конвертирования pdf как набор изображений.
Добавлены соответствующие настройки в читалку.
2020-12-18 23:30:13 +07:00
Book Pauk
972f957685 Работа над вкладкой "Конвертирование" 2020-12-18 22:44:20 +07:00
Book Pauk
40ff04e5dc Работа над вкладкой "Конвертирование" 2020-12-18 21:48:08 +07:00
Book Pauk
b3c028bd7a Убрал устаревшее 2020-12-18 21:23:02 +07:00
Book Pauk
51ec6a54fa Переименования, небольшое улучшение html-title 2020-12-17 23:39:45 +07:00
Book Pauk
7a29b16ee8 Коментарии к 0.9.12 2020-12-17 23:37:00 +07:00
Book Pauk
7af6fd8248 Новая вкладка 2020-12-17 23:36:30 +07:00
Book Pauk
e1c93169b5 Добавлена вкладка "Конвертирование" 2020-12-17 23:35:56 +07:00
Book Pauk
f4716d5a1e Поправлен баг 2020-12-17 23:12:36 +07:00
Book Pauk
f5c06ce420 Добавлен парсинг оглавления из djvu, добавлено отображение атрибута alt изображений в ContentsPage 2020-12-17 20:57:29 +07:00
Book Pauk
9492f85d80 Merge tag '0.9.11-4' into develop
0.9.11-4
2020-12-16 21:17:05 +07:00
Book Pauk
b1303a3ba2 Merge branch 'release/0.9.11-4' 2020-12-16 21:16:58 +07:00
Book Pauk
5c9cfe5e6f Оптимизация 2020-12-16 21:15:45 +07:00
Book Pauk
b89b5322b8 Merge tag '0.9.11-3' into develop
0.9.11-3
2020-12-16 21:07:39 +07:00
Book Pauk
945feba6b2 Merge branch 'release/0.9.11-3' 2020-12-16 21:07:32 +07:00
Book Pauk
c8af4b907b Добавлено отображение текущей позиции в оглавлении 2020-12-16 21:06:27 +07:00
Book Pauk
298e8928cf Поправлен мелкий баг 2020-12-16 17:10:44 +07:00
Book Pauk
8cb67d2976 Поправлен баг 2020-12-16 16:41:51 +07:00
Book Pauk
32b8382641 Поправлен баг 2020-12-16 16:09:35 +07:00
Book Pauk
007e97463b Небольшая поправка 2020-12-16 15:56:10 +07:00
Book Pauk
e4f190698d Merge tag '0.9.11-2' into develop
0.9.11-2
2020-12-16 01:43:05 +07:00
Book Pauk
b3be07b17e Merge branch 'release/0.9.11-2' 2020-12-16 01:43:00 +07:00
Book Pauk
72f8977071 Добавлено отображение номера изображения в статусбар 2020-12-16 01:41:37 +07:00
Book Pauk
3dbf00344e Мелкая поправка 2020-12-16 01:05:06 +07:00
Book Pauk
ffdf0b12cd В список изображений добавлено отображение самой картинки 2020-12-16 01:00:54 +07:00
Book Pauk
a51150c729 Рефакторинг 2020-12-15 23:01:58 +07:00
Book Pauk
37e14b397c Рефакторинг 2020-12-15 21:56:14 +07:00
Book Pauk
e48af7ee7d Дополнительно отображаем тип файла в списке изображений 2020-12-15 20:17:21 +07:00
Book Pauk
3eb3dd371a В ContentsPage добавлена вкладка "Изображения" 2020-12-15 15:40:12 +07:00
Book Pauk
8ef6551560 Улучшено распознавание xml-формата 2020-12-15 15:04:30 +07:00
Book Pauk
b1f5f3dd28 Merge tag '0.9.11-1' into develop
0.9.11-1
2020-12-14 02:23:42 +07:00
Book Pauk
6074c4b7bd Merge branch 'release/0.9.11-1' 2020-12-14 02:23:36 +07:00
Book Pauk
9906dd43c7 Работа над конвертером pdf 2020-12-14 02:22:38 +07:00
Book Pauk
17699f66f8 Небольшое улучшение парсинга оглавления 2020-12-14 02:07:20 +07:00
Book Pauk
80a29e654d Поправки механизма оповещения о выходе новой версии 2020-12-14 00:21:48 +07:00
Book Pauk
4184fda247 Мелкая поправка 2020-12-13 22:53:47 +07:00
Book Pauk
7460ff7055 Добавлена проверка выхода новой версии читалки и уведомление об этом,
заодно попутный рефакторинг
2020-12-13 22:50:24 +07:00
Book Pauk
3137b86cee Работа над конвертером Pdf 2020-12-13 21:54:03 +07:00
Book Pauk
b2ca84bb7e Поправил readme 2020-12-13 19:07:15 +07:00
Book Pauk
7d692dd730 Рефакторинг 2020-12-13 18:55:56 +07:00
Book Pauk
8850a89aa7 Поправлен баг 2020-12-13 18:51:13 +07:00
Book Pauk
57b01dd204 Рефакторинг, добавлена поддержка jpeg, png 2020-12-13 17:03:47 +07:00
Book Pauk
8aa1da36b6 Небольшие поправки 2020-12-13 16:21:21 +07:00
Book Pauk
2dbe29d632 Merge tag '0.9.11' into develop
0.9.11
2020-12-09 22:31:37 +07:00
Book Pauk
7fa891b4fc Merge branch 'release/0.9.11' 2020-12-09 22:31:33 +07:00
Book Pauk
6cb7412cf3 Версия 0.9.11 2020-12-09 22:30:58 +07:00
Book Pauk
157322834b Небольшая поправка 2020-12-09 22:30:19 +07:00
Book Pauk
1a13a0fee1 Работа над конвертером pdf 2020-12-09 22:19:14 +07:00
Book Pauk
37256255bf Добавлена поддержка тегов 'sup' и 'sub' 2020-12-09 20:35:52 +07:00
Book Pauk
75e01c899e Работа над конвертером pdf 2020-12-09 20:08:17 +07:00
Book Pauk
ef0d6eab89 Работа над конвертером Pdf 2020-12-09 19:05:09 +07:00
Book Pauk
5d54b1b0f4 Работа над конвертером pdf 2020-12-09 03:52:24 +07:00
Book Pauk
522f953b4f Работа над конвертером pdf 2020-12-09 03:06:15 +07:00
Book Pauk
15f02c7115 Работа над конвертером pdf 2020-12-09 01:29:58 +07:00
Book Pauk
174c877eee Рефакторинг, плюс небольшие доработки 2020-12-09 01:29:09 +07:00
Book Pauk
fd9ec736d7 Рефакторинг 2020-12-08 19:36:53 +07:00
Book Pauk
2c94025ba3 Поправлен баг 2020-12-08 19:31:00 +07:00
Book Pauk
bfadf35c40 Закончена работа над xmlParser, оттестировано 2020-12-08 18:48:55 +07:00
Book Pauk
f3b69caa12 Работа над модулем xmlParser 2020-12-08 16:17:36 +07:00
Book Pauk
18a83a5b0b Поправки настроек сжатия 2020-12-08 14:26:49 +07:00
Book Pauk
bd9669b782 Поправка цели dev 2020-12-08 14:26:25 +07:00
Book Pauk
e05713aa7f Работа над конвертером pdf 2020-12-08 14:15:17 +07:00
Book Pauk
bc3e1f0a6f Мелкий рефакторинг 2020-12-07 22:13:14 +07:00
Book Pauk
063d01b5ca Перевод pdf-конвертера на использование pdfalto 2020-12-07 22:05:01 +07:00
Book Pauk
81c38d7749 Мелкий рефакторинг 2020-12-07 20:13:32 +07:00
Book Pauk
a29842b084 Поправка readme 2020-12-07 20:12:37 +07:00
Book Pauk
bb5adcdaf6 Рефакторинг 2020-12-07 01:30:10 +07:00
Book Pauk
537e17a219 Merge tag '0.9.10-5' into develop
0.9.10-5
2020-12-05 13:42:45 +07:00
Book Pauk
03ce50153e Merge branch 'release/0.9.10-5' 2020-12-05 13:42:39 +07:00
Book Pauk
15d01ad7fc Коррекция таймаутов очереди ожидания 2020-12-05 13:41:42 +07:00
Book Pauk
e2b29e2c2f Merge tag '0.9.10-4' into develop
0.9.10-4
2020-12-05 13:25:10 +07:00
Book Pauk
ce7ae84e0f Merge branch 'release/0.9.10-4' 2020-12-05 13:25:06 +07:00
Book Pauk
01eb545f15 Улучшение работы с очередью, поправка багов 2020-12-05 13:24:04 +07:00
Book Pauk
706738c7f1 Merge tag '0.9.10-3' into develop
0.9.10-3
2020-12-05 01:40:37 +07:00
Book Pauk
6afa78cde9 Merge branch 'release/0.9.10-3' 2020-12-05 01:40:30 +07:00
Book Pauk
71f5710bba Увеличен лимит количества файлов для распаковки 2020-12-05 01:12:29 +07:00
Book Pauk
0d87043f91 Поправлен неверный вызов reject 2020-12-05 01:11:31 +07:00
Book Pauk
e25375fb7a Поправка багов 2020-12-05 00:31:53 +07:00
Book Pauk
41822999c8 Небольшие поправки 2020-12-05 00:06:54 +07:00
Book Pauk
07444bc7c2 Добавлена подсказка в сообщение об ошибке 2020-12-04 23:25:34 +07:00
Book Pauk
ec48e5b0b7 Мелкая поправка 2020-12-04 20:14:53 +07:00
Book Pauk
e8e2e9297f Merge tag '0.9.10-2' into develop
0.9.10-2
2020-12-04 20:00:40 +07:00
Book Pauk
4f871dd5ca Merge branch 'release/0.9.10-2' 2020-12-04 20:00:35 +07:00
Book Pauk
f5f07a591a Небольшие доработки конвертирования 2020-12-04 20:00:05 +07:00
Book Pauk
4c11e6918f Merge tag '0.9.10-1' into develop
0.9.10-1
2020-12-04 18:38:02 +07:00
Book Pauk
403b9c0508 Merge branch 'release/0.9.10-1' 2020-12-04 18:37:58 +07:00
Book Pauk
ee8ba75371 Переделывание конвертера Djvu, теперь работает быстрее и без промежуточного конвертирования в pdf 2020-12-04 18:24:08 +07:00
Book Pauk
a2773fb180 Поправил readme 2020-12-04 18:23:57 +07:00
Book Pauk
ca36d588fc Merge tag '0.9.10' into develop
0.9.10
2020-12-03 22:05:47 +07:00
Book Pauk
1e65707b7f Merge branch 'release/0.9.10' 2020-12-03 22:05:42 +07:00
Book Pauk
eddf34ce55 Мелкая поправка 2020-12-03 22:04:47 +07:00
Book Pauk
0fb43aa33c Версия 0.9.10 2020-12-03 21:41:23 +07:00
Book Pauk
b273b02da4 Убрал "k" в статусбаре 2020-12-03 21:37:58 +07:00
Book Pauk
0b997f9673 Поправка приветственного сообщения 2020-12-03 21:37:27 +07:00
Book Pauk
bdb2ae57a8 Добавлена частичная поддержка формата Djvu 2020-12-03 21:30:50 +07:00
Book Pauk
b5e563679a Мелкая поправка 2020-12-03 20:00:22 +07:00
Book Pauk
992c104262 Удален ненужный файл 2020-12-03 19:57:27 +07:00
Book Pauk
555154031e Добавлен запрос persistent storage, увеличен размер кэша книг 2020-12-03 19:53:08 +07:00
Book Pauk
acb083e429 Поправлен readme 2020-12-03 19:30:27 +07:00
Book Pauk
4a527d192d Улучшено управление внешними конвертерами 2020-12-03 19:04:34 +07:00
Book Pauk
39c3bf17dd Добавлен пакет pidusage 2020-12-03 18:43:57 +07:00
Book Pauk
afc8c84f41 Мелкие поправки таймаутов 2020-12-03 12:39:16 +07:00
Book Pauk
a085e04c4d Поправки параметров запуска Rar 2020-12-03 10:24:47 +07:00
Book Pauk
2f82b0db34 Добавлена поддержка rar-архивов с помощью внешнего архиватора 2020-12-02 23:56:17 +07:00
Book Pauk
0124c2b17d Поправка цели dev 2020-12-02 23:55:23 +07:00
Book Pauk
d2cfbbc9f3 Поправлен триггер сокрытия диалога 2020-12-02 22:24:11 +07:00
Book Pauk
c59f48822c Префикс "file://" заменен на "disk://", т.к. порождается конфликт с браузерным протоколом file:// 2020-12-02 22:01:36 +07:00
Book Pauk
b2d6584c4a Merge tag '0.9.9-2' into develop
0.9.9-2
2020-11-24 04:53:10 +07:00
Book Pauk
8f7cafb240 Merge branch 'release/0.9.9-2' 2020-11-24 04:52:55 +07:00
Book Pauk
08fd0f15ff Улучшено распознавание параграфов чистого текста 2020-11-24 04:49:18 +07:00
Book Pauk
dbb1bfe587 Поправки распознавания кодировки fb2-файла 2020-11-24 02:09:17 +07:00
Book Pauk
fe4b7a5a85 Улучшено распознавание xml-формата 2020-11-23 23:49:20 +07:00
Book Pauk
d8df5d76e5 Поправка ридми 2020-11-23 21:57:52 +07:00
Book Pauk
b65dcc5ade Добавлен параметр -nodrm 2020-11-23 21:49:19 +07:00
Book Pauk
a5c387a19e Поправка строки запуска сервера 2020-11-23 21:35:45 +07:00
Book Pauk
07c38d9a9f Поправка для журналирования ошибок конвертера 2020-11-23 21:19:04 +07:00
Book Pauk
20ac8a444b Merge tag '0.9.9-1' into develop
0.9.9-1
2020-11-21 13:36:15 +07:00
Book Pauk
7b601c9c7f Merge branch 'release/0.9.9-1' 2020-11-21 13:36:11 +07:00
Book Pauk
8d2f74daa4 Небольшие доделки в связи с оптимизацией загрузки списка недавних 2020-11-21 13:34:58 +07:00
Book Pauk
01e82dca5f Merge tag '0.9.9' into develop
0.9.9
2020-11-21 04:06:54 +07:00
Book Pauk
094bb407ed Merge branch 'release/0.9.9' 2020-11-21 04:06:49 +07:00
Book Pauk
338baa55ec Версия 0.9.9 2020-11-21 04:06:16 +07:00
Book Pauk
d06d20a33e Оптимизация загрузки и хранения списка недавних книг 2020-11-21 04:03:54 +07:00
Book Pauk
d46ba6b92b Мелкий рефакторинг 2020-11-21 01:13:30 +07:00
Book Pauk
ec2639039d Поправка бага 2020-11-20 21:57:54 +07:00
Book Pauk
3a211ded2e Мелкий рефакторинг, добавлено удобочитаемое имя при сохраненнии fb2 2020-11-20 21:43:37 +07:00
Book Pauk
c2131e3654 Доработки парсинга fb2 - распознаем тег sequence 2020-11-20 21:26:16 +07:00
Book Pauk
594fb59395 Убрал лишнее 2020-11-20 20:45:47 +07:00
Book Pauk
f44378ec84 Рефакторинг 2020-11-20 18:25:24 +07:00
Book Pauk
0f6b366f62 Расширил лог 2020-11-20 16:17:54 +07:00
Book Pauk
8d0a5997ee Небольшая поправка 2020-11-20 16:06:23 +07:00
Book Pauk
347cb3417e Поправки названий и справки 2020-11-19 15:20:40 +07:00
Book Pauk
337fcebd10 Merge tag '0.9.8-5' into develop
0.9.8-5
2020-11-15 14:57:27 +07:00
Book Pauk
e057b130e9 Merge branch 'release/0.9.8-5' 2020-11-15 14:57:23 +07:00
Book Pauk
19a0765a1a Рефакторинг, плюс небольшие доделки 2020-11-15 14:55:02 +07:00
Book Pauk
b81cd3240b Merge tag '0.9.8-4' into develop
0.9.8-4
2020-11-15 00:09:10 +07:00
Book Pauk
1e6105b076 Merge branch 'release/0.9.8-4' 2020-11-15 00:09:03 +07:00
Book Pauk
d8d89b3463 Добавлен показ аннотации в начале книги 2020-11-15 00:07:45 +07:00
Book Pauk
459564cb2d Рефакторинг 2020-11-14 23:43:22 +07:00
Book Pauk
084df35184 Мелкий рефакторинг 2020-11-14 23:32:39 +07:00
Book Pauk
81912babeb Merge tag '0.9.8-3' into develop
0.9.8-3
2020-11-14 23:27:14 +07:00
Book Pauk
3943fc7d95 Merge branch 'release/0.9.8-3' 2020-11-14 23:27:09 +07:00
Book Pauk
3999dc930c Рефакторинг, плюс небольшие изменения внешнего вида 2020-11-14 23:24:20 +07:00
Book Pauk
d4dea16456 Убрал неиспользуемые компоненты 2020-11-14 23:23:51 +07:00
Book Pauk
ed38cb33a5 Merge tag '0.9.8-2' into develop
0.9.8-2
2020-11-14 12:01:44 +07:00
146 changed files with 21735 additions and 14656 deletions

6
.babelrc Normal file
View File

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

View File

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

View File

@@ -2,7 +2,7 @@
Браузерная онлайн-читалка книг и децентрализованная библиотека. Браузерная онлайн-читалка книг и децентрализованная библиотека.
Читалка ![](https://omnireader.ru/favicon.ico)[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS: Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
![](docs/assets/face.jpg) ![](docs/assets/face.jpg)
![](docs/assets/reader.jpg) ![](docs/assets/reader.jpg)
@@ -11,7 +11,7 @@
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md) Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
## Сборка проекта ## Сборка проекта
Необходима версия node.js не ниже 10. Необходима версия node.js не ниже 14.
``` ```
$ git clone https://github.com/bookpauk/liberama $ git clone https://github.com/bookpauk/liberama

View File

@@ -24,8 +24,8 @@ async function main() {
await fs.ensureDir(tempDownloadDir); await fs.ensureDir(tempDownloadDir);
//sqlite3 //sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-linux-x64.tar.gz'; const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-linux-x64/node_sqlite3.node`; const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) { if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку // Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку

View File

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

View File

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

View File

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

View File

@@ -24,8 +24,8 @@ async function main() {
await fs.ensureDir(tempDownloadDir); await fs.ensureDir(tempDownloadDir);
//sqlite3 //sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-win32-x64.tar.gz'; const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-win32-x64/node_sqlite3.node`; const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) { if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку // Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку

View File

@@ -13,8 +13,7 @@ class Misc {
]}; ]};
try { try {
await wsc.open(); const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error) if (config.error)
throw new Error(config.error); throw new Error(config.error);
return config; return config;

View File

@@ -19,8 +19,7 @@ class Reader {
let response = {}; let response = {};
try { try {
await wsc.open(); const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
let prevResponse = false; let prevResponse = false;
while (1) {// eslint-disable-line no-constant-condition while (1) {// eslint-disable-line no-constant-condition
@@ -66,7 +65,7 @@ class Reader {
await utils.sleep(refreshPause); await utils.sleep(refreshPause);
i++; i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания'); throw new Error('Слишком долгое время ожидания');
} }
//проверка воркера //проверка воркера
@@ -124,8 +123,7 @@ class Reader {
let response = null let response = null
try { try {
await wsc.open(); response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
//если с WebSocket проблема, работаем по http //если с WebSocket проблема, работаем по http
@@ -181,9 +179,8 @@ class Reader {
maxUploadFileSize = 10*1024*1024; maxUploadFileSize = 10*1024*1024;
if (file.size > maxUploadFileSize) if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`); throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData(); let formData = new FormData();
formData.append('file', file); formData.append('file', file, file.name);
const options = { const options = {
headers: { headers: {
@@ -211,8 +208,7 @@ class Reader {
async storage(request) { async storage(request) {
let response = null; let response = null;
try { try {
await wsc.open(); response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
//если с WebSocket проблема, работаем по http //если с WebSocket проблема, работаем по http
@@ -223,7 +219,7 @@ class Reader {
const state = response.state; const state = response.state;
if (!state) if (!state)
throw new Error('Неверный ответ api'); throw new Error('Неверный ответ api');
if (response.state == 'error') { if (state == 'error') {
throw new Error(response.error); throw new Error(response.error);
} }

View File

@@ -1,185 +1,3 @@
import * as utils from '../share/utils'; import WebSocketConnection from '../../server/core/WebSocketConnection';
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;
this.connecting = false;
}
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) => { (async() => {
//Ожидаем окончания процесса подключения, если open уже был вызван
let i = 0;
while (this.connecting && i < 200) {//10 сек
await utils.sleep(50);
i++;
}
if (i >= 200)
this.connecting = false;
//проверим подключение, и если нет, то подключимся заново
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
this.connecting = true;
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
url = url || `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(url);
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
this.ws.onopen = (e) => {
this.connecting = false;
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 (this.connecting) {
this.connecting = false;
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(); export default new WebSocketConnection();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,22 +1,28 @@
<template> <template>
<div class="fit row"> <div class="fit row">
<Notify ref="notify"/> <Notify ref="notify" />
<StdDialog ref="stdDialog"/> <StdDialog ref="stdDialog" />
<keep-alive>
<router-view class="col"></router-view> <router-view v-slot="{ Component }">
</keep-alive> <keep-alive v-if="showPage">
<component :is="Component" class="col" />
</keep-alive>
</router-view>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from './vueComponent.js';
import Component from 'vue-class-component';
import Notify from './share/Notify.vue'; import Notify from './share/Notify.vue';
import StdDialog from './share/StdDialog.vue'; import StdDialog from './share/StdDialog.vue';
import sanitizeHtml from 'sanitize-html';
import miscApi from '../api/misc';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Notify, Notify,
StdDialog, StdDialog,
@@ -28,8 +34,11 @@ export default @Component({
} }
}, },
}) };
class App extends Vue { class App {
_options = componentOptions;
showPage = false;
itemRuText = { itemRuText = {
'/cardindex': 'Картотека', '/cardindex': 'Картотека',
'/reader': 'Читалка', '/reader': 'Читалка',
@@ -42,7 +51,6 @@ class App extends Vue {
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
this.state = this.$store.state; this.state = this.$store.state;
this.uistate = this.$store.state.uistate; this.uistate = this.$store.state.uistate;
this.config = this.$store.state.config; this.config = this.$store.state.config;
@@ -50,7 +58,7 @@ class App extends Vue {
//root route //root route
let cachedRoute = ''; let cachedRoute = '';
let cachedPath = ''; let cachedPath = '';
this.$root.rootRoute = () => { this.$root.getRootRoute = () => {
if (this.$route.path != cachedPath) { if (this.$route.path != cachedPath) {
cachedPath = this.$route.path; cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i); const m = cachedPath.match(/^(\/[^/]*).*$/i);
@@ -69,46 +77,50 @@ class App extends Vue {
} }
}); });
// set-app-title this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
this.$root.$on('set-app-title', this.setAppTitle);
//global keyHooks // setAppTitle
this.keyHooks = []; this.$root.setAppTitle = this.setAppTitle;
this.keyHook = (event) => {
for (const hook of this.keyHooks) //sanitize
this.$root.sanitize = sanitizeHtml;
//global event hooks
this.eventHooks = {};
this.$root.eventHook = (hookName, event) => {
if (!this.eventHooks[hookName])
return;
for (const hook of this.eventHooks[hookName])
hook(event); hook(event);
} }
this.$root.addKeyHook = (hook) => { this.$root.addEventHook = (hookName, hook) => {
if (this.keyHooks.indexOf(hook) < 0) if (!this.eventHooks[hookName])
this.keyHooks.push(hook); this.eventHooks[hookName] = [];
if (this.eventHooks[hookName].indexOf(hook) < 0)
this.eventHooks[hookName].push(hook);
} }
this.$root.removeKeyHook = (hook) => { this.$root.removeEventHook = (hookName, hook) => {
const i = this.keyHooks.indexOf(hook); if (!this.eventHooks[hookName])
return;
const i = this.eventHooks[hookName].indexOf(hook);
if (i >= 0) if (i >= 0)
this.keyHooks.splice(i, 1); this.eventHooks[hookName].splice(i, 1);
} }
document.addEventListener('keyup', (event) => { document.addEventListener('keyup', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
document.addEventListener('keypress', (event) => { document.addEventListener('keypress', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
window.addEventListener('resize', () => {
this.$root.$emit('resize');
});
}
routerReady() { window.addEventListener('resize', (event) => {
return new Promise ((resolve) => { this.$root.eventHook('resize', event);
this.$router.onReady(() => {
resolve();
});
}); });
} }
@@ -116,26 +128,36 @@ class App extends Vue {
this.$root.notify = this.$refs.notify; this.$root.notify = this.$refs.notify;
this.$root.stdDialog = this.$refs.stdDialog; 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.$root.notify.error(mes, 'Ошибка API');
}
});
this.setAppTitle(); this.setAppTitle();
(async() => { (async() => {
await this.routerReady(); //загрузим конфиг сревера
try {
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
this.showPage = true;
} catch(e) {
//проверим, не получен ли конфиг ранее
if (!this.mode) {
this.$root.notify.error(e.message, 'Ошибка API');
} else {
//вероятно, работаем в оффлайне
this.showPage = true;
}
console.error(e);
}
//запросим persistent storage
if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist();
}
await this.$router.isReady();
this.redirectIfNeeded(); this.redirectIfNeeded();
})(); })();
} }
toggleCollapse() { toggleCollapse() {
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse); this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
this.$root.$emit('resize'); this.$root.eventHook('resize');
} }
get isCollapse() { get isCollapse() {
@@ -170,7 +192,7 @@ class App extends Vue {
} }
get rootRoute() { get rootRoute() {
return this.$root.rootRoute(); return this.$root.getRootRoute();
} }
setAppTitle(title) { setAppTitle(title) {
@@ -180,7 +202,7 @@ class App extends Vue {
} else if (this.mode == 'omnireader') { } else if (this.mode == 'omnireader') {
document.title = `Omni Reader - всегда с вами`; document.title = `Omni Reader - всегда с вами`;
} else if (this.config && this.mode !== null) { } else if (this.config && this.mode !== null) {
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`; document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
} }
} else { } else {
document.title = title; document.title = title;
@@ -225,6 +247,8 @@ class App extends Vue {
} }
} }
} }
export default vueComponent(App);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -256,6 +280,14 @@ body, html, #app {
animation: rotating 2s linear infinite; animation: rotating 2s linear infinite;
} }
@keyframes rotating {
from {
transform: rotate(0deg);
} to {
transform: rotate(360deg);
}
}
.notify-button-icon { .notify-button-icon {
font-size: 16px !important; font-size: 16px !important;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,91 +1,144 @@
<template> <template>
<Window width="600px" ref="window" @close="close"> <Window ref="window" width="600px" @close="close">
<template slot="header"> <template #header>
Оглавление/закладки Оглавление/закладки
</template> </template>
<div class="bg-grey-3 row"> <div class="bg-grey-3 row">
<q-tabs <q-tabs
v-model="selectedTab" v-model="selectedTab"
active-color="black" active-color="black"
active-bg-color="white" active-bg-color="white"
indicator-color="white" indicator-color="white"
dense dense
no-caps no-caps
inline-label inline-label
class="no-mp bg-grey-4 text-grey-7" class="no-mp bg-grey-4 text-grey-7"
> >
<q-tab name="contents" icon="la la-list" label="Оглавление" /> <q-tab name="contents" icon="la la-list" label="Оглавление" />
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" /> <q-tab name="images" icon="la la-image" label="Изображения" />
</q-tabs> <q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
</div> </q-tabs>
</div>
<div class="q-mb-sm"/> <div class="q-mb-sm" />
<div class="tab-panel" v-show="selectedTab == 'contents'"> <div v-show="selectedTab == 'contents'" class="tab-panel">
<div> <div>
<div class="row" v-for="item in contents" :key="item.key"> <div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<q-expansion-item v-if="item.list.length" <div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
class="item separator-bottom" <div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
expand-icon-toggle <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" />
switch-toggle-side
expand-icon="la la-arrow-circle-down"
>
<template slot="header">
<div class="row no-wrap clickable" style="width: 465px" @click="setBookPos(item.offset)">
<div :style="item.style"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
<div class="column justify-center">{{ item.perc }}%</div>
</div> </div>
</template> <div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px" />
<q-item class="subitem separator-top column justify-center" v-for="subitem in item.list" :key="subitem.key"> </div>
<div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(subitem.offset)"> <div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="subitem.style"></div> <div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="subitem.label"></div> <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">{{ subitem.perc }}%</div> <div class="column justify-center">
{{ item.perc }}%
</div>
</div> </div>
</q-item>
</q-expansion-item>
<q-item v-else class="item separator-bottom">
<div class="row no-wrap clickable" style="padding-left: 55px; width: 520px" @click="setBookPos(item.offset)">
<div :style="item.style"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
<div class="column justify-center">{{ item.perc }}%</div>
</div> </div>
</q-item>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
<div class="col row clickable" @click="setBookPos(subitem.offset)">
<div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<div class="column justify-center">
{{ subitem.perc }}%
</div>
</div>
</div>
</div>
</div>
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
Оглавление отсутствует
</div>
</div> </div>
</div> </div>
</div>
<div class="tab-panel" v-show="selectedTab == 'bookmarks'"> <div v-show="selectedTab == 'images'" class="tab-panel">
<div class="column justify-center items-center" style="height: 100px"> <div>
Раздел находится в разработке <div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
<i class="loading-img-icon la la-images"></i>
</div>
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
</div>
<div class="no-expand-button column justify-center items-center">
<div class="image-num">
{{ item.num }}
</div>
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">
JPG
</div>
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">
PNG
</div>
<div v-show="!item.local" class="image-type it-net-color row justify-center">
INET
</div>
</div>
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div>
</div>
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
Изображения отсутствуют
</div>
</div>
</div> </div>
</div>
<div v-show="selectedTab == 'bookmarks'" class="tab-panel">
<div class="column justify-center items-center" style="height: 100px">
Раздел находится в разработке
</div>
</div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
//import _ from 'lodash'; //import _ from 'lodash';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
//import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
watch: { watch: {
bookPos() {
this.updateBookPosSelection();
}
}, },
}) };
class ContentsPage extends Vue { class ContentsPage {
_options = componentOptions;
_props = {
bookPos: Number,
isVisible: Boolean,
};
selectedTab = 'contents'; selectedTab = 'contents';
contents = []; contents = [];
images = [];
imageSrc = [];
imageLoaded = [];
created() { created() {
} }
@@ -93,74 +146,202 @@ class ContentsPage extends Vue {
async init(currentBook, parsed) { async init(currentBook, parsed) {
this.$refs.window.init(); this.$refs.window.init();
if (this.parsed != parsed) { //закладки
this.contents = [];
await this.$nextTick(); //проверим, надо ли обновлять списки
this.parsed = parsed; if (this.parsed == parsed) {
this.updateBookPosSelection();
return;
}
//далее формирование оглавления
this.parsed = parsed;
this.contents = [];
await this.$nextTick();
const pc = parsed.contents;
const ims = parsed.images;
const newpc = [];
if (pc.length) {//если есть оглавление
//преобразуем все, кроме первого, разделы body в title-subtitle
let curSubtitles = [];
let prevBodyIndex = -1;
for (let i = 0; i < pc.length; i++) {
const cont = pc[i];
if (prevBodyIndex != cont.bodyIndex)
curSubtitles = [];
prevBodyIndex = cont.bodyIndex;
if (cont.bodyIndex > 1) {
if (cont.inset < 1) {
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
} else {
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
}
} else {
newpc.push(cont);
}
}
} else {//попробуем вытащить из images
for (let i = 0; i < ims.length; i++) {
const image = ims[i];
if (image.alt) {
newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
}
}
} }
const prepareLabel = (title, bolder = false) => { const prepareLabel = (title, bolder = false) => {
let titleParts = title.split('<p>'); let titleParts = title.split('<p>');
const textParts = titleParts.filter(v => v).map(v => `<div>${v.replace(/(<([^>]+)>)/ig, '')}</div>`); const textParts = titleParts.filter(v => v).map(v => `<div>${utils.removeHtmlTags(v)}</div>`);
if (bolder && textParts.length > 1) if (bolder && textParts.length > 1)
textParts[0] = `<b>${textParts[0]}</b>`; textParts[0] = `<b>${textParts[0]}</b>`;
return textParts.join(''); return textParts.join('');
} }
const insetStyle = inset => `width: ${inset*20}px`; const getIndentStyle = inset => `width: ${inset*20}px`;
const pc = parsed.contents;
const newpc = [];
//преобразуем не первые разделы body в title-subtitle const getLabelStyle = (inset) => {
let curSubtitles = []; const fontSizes = ['110%', '100%', '90%', '85%'];
let prevBodyIndex = -1; inset = (inset > 3 ? 3 : inset);
for (let i = 0; i < pc.length; i++) { return `font-size: ${fontSizes[inset]}`;
const cont = pc[i]; };
if (prevBodyIndex != cont.bodyIndex)
curSubtitles = [];
prevBodyIndex = cont.bodyIndex;
if (cont.bodyIndex > 1) {
if (cont.inset < 1) {
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
} else {
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
}
} else {
newpc.push(cont);
}
}
//формируем newContents //формируем newContents
let i = 0; let i = 0;
const newContents = []; const newContents = [];
newpc.forEach((cont) => { newpc.forEach((cont) => {
const label = prepareLabel(cont.title, true); const label = prepareLabel(cont.title, true);
const style = insetStyle(cont.inset); const indentStyle = getIndentStyle(cont.inset);
const labelStyle = getLabelStyle(cont.inset);
let j = 0; let j = 0;
const list = []; const list = [];
cont.subtitles.forEach((sub) => { cont.subtitles.forEach((sub) => {
const l = prepareLabel(sub.title); const l = prepareLabel(sub.title);
const s = insetStyle(sub.inset + 1); const s = getIndentStyle(sub.inset + 1);
const ls = getLabelStyle(cont.inset + 1);
const p = parsed.para[sub.paraIndex]; const p = parsed.para[sub.paraIndex];
list.push({perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, style: s}); list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s, labelStyle: ls};
j++; j++;
}); });
const p = parsed.para[cont.paraIndex]; const p = parsed.para[cont.paraIndex];
newContents.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, style, list}); newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
i++; i++;
}); });
this.contents = newContents; this.contents = newContents;
//формируем newImages
const newImages = [];
for (i = 0; i < ims.length; i++) {
const image = ims[i];
const bin = parsed.binary[image.id];
const type = (bin ? bin.type : '');
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
const indentStyle = getIndentStyle(1);
const labelStyle = getLabelStyle(1);
const p = parsed.para[image.paraIndex];
newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
}
this.images = newImages;
if (this.selectedTab == 'contents' && !this.contents.length && this.images.length)
this.selectedTab = 'images';
//выделим на bookPos
this.updateBookPosSelection();
//асинхронная загрузка изображений
this.imageSrc = [];
this.imageLoaded = [];
await utils.sleep(50);
(async() => {
for (i = 0; i < ims.length; i++) {
const {id, local} = ims[i];
const bin = this.parsed.binary[id];
if (local)
this.imageSrc[id] = (bin ? `data:${bin.type};base64,${bin.data}` : '');
else
this.imageSrc[id] = id;
this.imageLoaded[id] = true;
await utils.sleep(5);
}
})();
}
async updateBookPosSelection() {
if (!this.isVisible)
return;
await utils.sleep(50);
const bp = this.bookPos;
for (let i = 0; i < this.contents.length; i++) {
const item = this.contents[i];
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
for (let j = 0; j < item.list.length; j++) {
const subitem = item.list[j];
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
if (bp >= subitem.offset && bp < nextSubOffset) {
subitem.isBookPos = true;
this.contents[i] = Object.assign(item, {list: item.list});
} else if (subitem.isBookPos) {
subitem.isBookPos = false;
this.contents[i] = Object.assign(item, {list: item.list});
}
}
if (bp >= item.offset && bp < nextOffset) {
this.contents[i] = Object.assign(item, {isBookPos: true});
} else if (item.isBookPos) {
this.contents[i] = Object.assign(item, {isBookPos: false});
}
}
for (let i = 0; i < this.images.length; i++) {
const img = this.images[i];
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
if (bp >= img.offset && bp < nextOffset) {
this.images[i] = Object.assign(img, {isBookPos: true});
} else if (img.isBookPos) {
this.images[i] = Object.assign(img, {isBookPos: false});
}
}
}
async expandClick(key) {
const item = this.contents[key];
const expanded = !item.expanded;
if (!expanded) {
const subitems = this.$refs[`subitem${key}`];
subitems.style.height = '0';
await utils.sleep(200);
}
this.contents[key] = Object.assign({}, item, {expanded});
if (expanded) {
await this.$nextTick();
const subitems = this.$refs[`subitem${key}`];
subitems.style.height = subitems.scrollHeight + 'px';
}
} }
async setBookPos(newValue) { async setBookPos(newValue) {
this.$emit('book-pos-changed', {bookPos: newValue}); this.$emit('book-pos-changed', {bookPos: newValue});
await this.$nextTick();
this.close(); this.close();
} }
@@ -175,6 +356,8 @@ class ContentsPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(ContentsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -188,20 +371,82 @@ class ContentsPage extends Vue {
.clickable { .clickable {
cursor: pointer; cursor: pointer;
padding: 10px 0 10px 0;
} }
.item:hover { .item, .subitem, .item-book-pos, .subitem-book-pos {
border-bottom: 1px solid #e0e0e0;
}
.item:hover, .subitem:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.subitem:hover { .item-book-pos {
background-color: #e0e0e0; background-color: #b0f0b0;
} }
.separator-top { .subitem-book-pos {
border-top: 1px solid #e0e0e0; background-color: #d0f5d0;
} }
.separator-bottom {
border-top: 1px solid #e0e0e0; .item-book-pos:hover {
background-color: #b0e0b0;
}
.subitem-book-pos:hover {
background-color: #d0f0d0;
}
.expand-button, .no-expand-button {
width: 40px;
}
.subitems-transition {
height: 0;
transition: height 0.2s linear;
overflow: hidden;
}
.icon {
transition: transform 0.2s;
}
.expanded-icon {
transform: rotate(90deg);
}
.image-num {
font-size: 120%;
padding-bottom: 3px;
}
.image-type {
border: 1px solid black;
border-radius: 6px;
font-size: 80%;
padding: 2px 0 2px 0;
width: 34px;
}
.it-jpg-color {
background: linear-gradient(to right, #fabc3d, #ffec6d);
}
.it-png-color {
background: linear-gradient(to right, #4bc4e5, #6bf4ff);
}
.it-net-color {
background: linear-gradient(to right, #00c400, #00f400);
}
.image-thumb-box {
width: 120px;
overflow: hidden;
}
.image-thumb {
height: 50px;
}
.loading-img-icon {
font-size: 250%;
} }
</style> </style>

View File

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

View File

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

View File

@@ -1,49 +1,68 @@
<template> <template>
<div class="page"> <div class="page">
<div class="box"> <div class="box">
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p> <p class="p">
Вы можете пожертвовать на развитие проекта любую сумму:
</p>
<div class="address"> <div class="address">
<img class="logo" src="./assets/yandex.png"> <img class="logo" src="./assets/yoomoney.png">
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br> <q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
<div class="para">{{ yandexAddress }} Пожертвовать
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')"> </q-btn><br>
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip> <div class="para">
{{ yooAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
</div> </div>
</div> </div>
<div class="address"> <!--div class="address">
<img class="logo" src="./assets/paypal.png"> <img class="logo" src="./assets/paypal.png">
<div class="para">{{ paypalAddress }} <div class="para">
{{ paypalAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')"> <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-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
</div> </div>
</div> </div-->
<div class="address"> <div class="address">
<img class="logo" src="./assets/bitcoin.png"> <img class="logo" src="./assets/bitcoin.png">
<div class="para">{{ bitcoinAddress }} <div class="para">
{{ bitcoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')"> <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-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
</div> </div>
</div> </div>
<div class="address"> <div class="address">
<img class="logo" src="./assets/litecoin.png"> <img class="logo" src="./assets/litecoin.png">
<div class="para">{{ litecoinAddress }} <div class="para">
{{ litecoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')"> <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-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
</div> </div>
</div> </div>
<div class="address"> <div class="address">
<img class="logo" src="./assets/monero.png"> <img class="logo" src="./assets/monero.png">
<div class="para">{{ moneroAddress }} <div class="para">
{{ moneroAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')"> <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-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
</div> </div>
</div> </div>
@@ -53,14 +72,12 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import {copyTextToClipboard} from '../../../../share/utils'; import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({ class DonateHelpPage {
}) yooAddress = '410018702323056';
class DonateHelpPage extends Vue {
yandexAddress = '410018702323056';
paypalAddress = 'bookpauk@gmail.com'; paypalAddress = 'bookpauk@gmail.com';
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85'; bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ'; litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
@@ -69,8 +86,8 @@ class DonateHelpPage extends Vue {
created() { created() {
} }
donateYandexMoney() { donateYooMoney() {
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank'); window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
} }
async copyAddress(address, prefix) { async copyAddress(address, prefix) {
@@ -81,6 +98,8 @@ class DonateHelpPage extends Vue {
this.$root.notify.error('Копирование не удалось'); this.$root.notify.error('Копирование не удалось');
} }
} }
export default vueComponent(DonateHelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window @close="close"> <Window @close="close">
<template slot="header"> <template #header>
Справка Справка
</template> </template>
@@ -14,8 +14,7 @@
<div class="separator"></div> <div class="separator"></div>
<keep-alive> <keep-alive>
<component ref="page" class="col" :is="activePage" <component :is="activePage" ref="page" class="col"></component>
></component>
</keep-alive> </keep-alive>
</div> </div>
</Window> </Window>
@@ -23,22 +22,21 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue'; import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue'; import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue'; import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue'; import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue'; //import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
const pages = { const pages = {
'CommonHelpPage': CommonHelpPage, 'CommonHelpPage': CommonHelpPage,
'HotkeysHelpPage': HotkeysHelpPage, 'HotkeysHelpPage': HotkeysHelpPage,
'MouseHelpPage': MouseHelpPage, 'MouseHelpPage': MouseHelpPage,
'VersionHistoryPage': VersionHistoryPage, 'VersionHistoryPage': VersionHistoryPage,
'DonateHelpPage': DonateHelpPage, //'DonateHelpPage': DonateHelpPage,
}; };
const tabs = [ const tabs = [
@@ -46,13 +44,15 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'], ['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'], ['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'], ['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'], //['DonateHelpPage', 'Помочь проекту'],
]; ];
export default @Component({ const componentOptions = {
components: Object.assign({ Window }, pages), components: Object.assign({ Window }, pages),
}) };
class HelpPage extends Vue { class HelpPage {
_options = componentOptions;
selectedTab = 'CommonHelpPage'; selectedTab = 'CommonHelpPage';
close() { close() {
@@ -73,7 +73,7 @@ class HelpPage extends Vue {
} }
activateDonateHelpPage() { activateDonateHelpPage() {
this.selectedTab = 'DonateHelpPage'; //this.selectedTab = 'DonateHelpPage';
} }
activateVersionHistoryHelpPage() { activateVersionHistoryHelpPage() {
@@ -87,6 +87,8 @@ class HelpPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(HelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,75 +1,129 @@
<template> <template>
<div class="column no-wrap"> <div class="column no-wrap">
<div ref="header" class="header" v-show="toolBarActive"> <div v-show="toolBarActive" ref="header" class="header">
<div ref="buttons" class="row justify-between no-wrap"> <div ref="buttons" class="row justify-between no-wrap">
<div> <div>
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple> <button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
<q-icon name="la la-arrow-left" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loader'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
<q-icon name="la la-caret-square-up" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadFile'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
<q-icon name="la la-comment" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadBuffer'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
<q-icon name="la la-question" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['help'] }}
</q-tooltip>
</button> </button>
</div> </div>
<div> <div>
<button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple> <button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
<q-icon name="la la-angle-left" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['undoAction'] }}
</q-tooltip>
</button> </button>
<button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple> <button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
<q-icon name="la la-angle-right" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['redoAction'] }}
</q-tooltip>
</button> </button>
<div class="space"></div> <div class="space"></div>
<button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple> <button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['fullScreen'] }}
</q-tooltip>
</button> </button>
<button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple> <button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
<q-icon name="la la-film" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['scrolling'] }}
</q-tooltip>
</button> </button>
<button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple> <button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
<q-icon name="la la-angle-double-right" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['setPosition'] }}
</q-tooltip>
</button> </button>
<button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple> <button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
<q-icon name="la la-search" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['search'] }}
</q-tooltip>
</button> </button>
<button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple> <button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
<q-icon name="la la-copy" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['copyText'] }}
</q-tooltip>
</button> </button>
<button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple> <button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
<q-icon name="la la-retweet" size="32px"/> <q-icon name="la la-magic" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['convOptions'] }}
</q-tooltip>
</button> </button>
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple> <button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['refresh'] }}
</q-tooltip>
</button> </button>
<div class="space"></div> <div class="space"></div>
<button ref="contents" v-show="showToolButton['contents']" class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')" v-ripple> <button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
<q-icon name="la la-list" size="32px"/> <q-icon name="la la-list" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['contents'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['contents'] }}
</q-tooltip>
</button> </button>
<button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple> <button v-show="mode == 'liberama.top' && showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
<q-icon name="la la-sitemap" size="32px"/> <q-icon name="la la-sitemap" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['libs'] }}
</q-tooltip>
</button> </button>
<button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple> <button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
<q-icon name="la la-book-open" size="32px"/> <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> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['recentBooks'] }}
</q-tooltip>
</button> </button>
</div> </div>
<div> <div>
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple> <button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
<q-icon name="la la-unlink" size="32px"/> <q-icon name="la la-mouse" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['clickControl'] }}
</q-tooltip>
</button> </button>
<button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple> <button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
<q-icon name="la la-cog" size="32px"/> <q-icon name="la la-unlink" size="32px" />
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['offlineMode'] }}
</q-tooltip>
</button>
<button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
<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> </button>
</div> </div>
</div> </div>
@@ -77,7 +131,10 @@
<div class="main col row relative-position"> <div class="main col row relative-position">
<keep-alive> <keep-alive>
<component ref="page" class="col" :is="activePage" <component
:is="activePage"
ref="page"
class="col"
@load-book="loadBook" @load-book="loadBook"
@load-file="loadFile" @load-file="loadFile"
@book-pos-changed="bookPosChanged" @book-pos-changed="bookPosChanged"
@@ -86,30 +143,32 @@
</keep-alive> </keep-alive>
<SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage> <SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
<SearchPage v-show="searchActive" ref="searchPage" <SearchPage
v-show="searchActive"
ref="searchPage"
@do-action="doAction" @do-action="doAction"
@book-pos-changed="bookPosChanged" @book-pos-changed="bookPosChanged"
@start-text-search="startTextSearch" @start-text-search="startTextSearch"
@stop-text-search="stopTextSearch"> @stop-text-search="stopTextSearch"
</SearchPage> ></SearchPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage> <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage> <LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage> <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage> <SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage> <HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage> <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage> <ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
<ContentsPage v-show="contentsActive" ref="contentsPage" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs> <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import {Buffer} from 'safe-buffer'; import {Buffer} from 'safe-buffer';
@@ -125,17 +184,23 @@ import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
import SettingsPage from './SettingsPage/SettingsPage.vue'; import SettingsPage from './SettingsPage/SettingsPage.vue';
import HelpPage from './HelpPage/HelpPage.vue'; import HelpPage from './HelpPage/HelpPage.vue';
import ClickMapPage from './ClickMapPage/ClickMapPage.vue'; import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
import ServerStorage from './ServerStorage/ServerStorage.vue';
import ContentsPage from './ContentsPage/ContentsPage.vue'; import ContentsPage from './ContentsPage/ContentsPage.vue';
import ServerStorage from './ServerStorage/ServerStorage.vue';
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue'; import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager'; import bookManager from './share/bookManager';
import wallpaperStorage from './share/wallpaperStorage';
import dynamicCss from '../../share/dynamicCss';
import rstore from '../../store/modules/reader'; import rstore from '../../store/modules/reader';
import readerApi from '../../api/reader'; import readerApi from '../../api/reader';
import miscApi from '../../api/misc';
import {versionHistory} from './versionHistory';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
LoaderPage, LoaderPage,
TextPage, TextPage,
@@ -149,9 +214,9 @@ export default @Component({
SettingsPage, SettingsPage,
HelpPage, HelpPage,
ClickMapPage, ClickMapPage,
ServerStorage,
ContentsPage, ContentsPage,
ServerStorage,
ReaderDialogs, ReaderDialogs,
}, },
watch: { watch: {
@@ -191,25 +256,38 @@ export default @Component({
} }
})(); })();
}, },
dualPageMode(newValue) {
if (newValue)
this.stopScrolling();
},
}, },
}) };
class Reader extends Vue {
rstore = {};
loaderActive = false;
offlineModeActive = false;
progressActive = false;
fullScreenActive = false;
scrollingActive = false; class Reader {
_options = componentOptions;
rstore = {};
loaderActive = false;
loadFileActive = false;
loadBufferActive = false;
fullScreenActive = false;
setPositionActive = false; setPositionActive = false;
searchActive = false; searchActive = false;
copyTextActive = false; copyTextActive = false;
convOptionsActive = false;
refreshActive = false;
contentsActive = false;
libsActive = false; libsActive = false;
recentBooksActive = false; recentBooksActive = false;
clickControlActive = false;
offlineModeActive = false;
settingsActive = false; settingsActive = false;
helpActive = false;
clickMapActive = false; clickMapActive = false;
contentsActive = false; helpActive = false;
scrollingActive = false;
progressActive = false;
bookPos = null; bookPos = null;
allowUrlParamBookPos = false; allowUrlParamBookPos = false;
@@ -224,19 +302,28 @@ class Reader extends Vue {
whatsNewVisible = false; whatsNewVisible = false;
whatsNewContent = ''; whatsNewContent = '';
donationVisible = false; donationVisible = false;
dualPageMode = false;
created() { created() {
this.rstore = rstore; this.rstore = rstore;
this.loading = true; this.loading = true;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
this.reader = this.$store.state.reader; this.reader = this.$store.state.reader;
this.config = this.$store.state.config; this.config = this.$store.state.config;
this.$root.addKeyHook(this.keyHook); this.$root.addEventHook('key', this.keyHook);
this.lastActivePage = false; this.lastActivePage = false;
this.$watch(
() => this.$route.path,
(newValue) => {
if (newValue == '/reader') {
this.updateRoute();
}
}
);
this.debouncedSetRecentBook = _.debounce(async(newValue) => { this.debouncedSetRecentBook = _.debounce(async(newValue) => {
const recent = this.mostRecentBook(); const recent = this.mostRecentBook();
if (recent && (recent.bookPos != newValue || recent.bookPosSeen !== this.bookPosSeen)) { if (recent && (recent.bookPos != newValue || recent.bookPosSeen !== this.bookPosSeen)) {
@@ -250,11 +337,11 @@ class Reader extends Vue {
await this.$nextTick(); await this.$nextTick();
this.paramPosIgnore = false; this.paramPosIgnore = false;
} }
}, 500, {'maxWait':5000}); }, 250, {maxWait: 5000});
this.scrollingSetRecentBook = _.debounce((newValue) => { this.scrollingSetRecentBook = _.debounce((newValue) => {
this.debouncedSetRecentBook(newValue); this.debouncedSetRecentBook(newValue);
}, 15000, {'maxWait':20000}); }, 15000, {maxWait: 20000});
document.addEventListener('fullscreenchange', () => { document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null); this.fullScreenActive = (document.fullscreenElement !== null);
@@ -267,10 +354,11 @@ class Reader extends Vue {
this.updateHeaderMinWidth(); this.updateHeaderMinWidth();
(async() => { (async() => {
await wallpaperStorage.init();
await bookManager.init(this.settings); await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent); bookManager.addEventListener(this.bookManagerEvent);
if (this.$root.rootRoute() == '/reader') { if (this.$root.getRootRoute() == '/reader') {
if (this.routeParamUrl) { if (this.routeParamUrl) {
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh}); await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
} else { } else {
@@ -293,6 +381,16 @@ class Reader extends Vue {
await this.$refs.dialogs.init(); await this.$refs.dialogs.init();
})(); })();
(async() => {
this.isFirstNeedUpdateNotify = true;
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
while (true) {// eslint-disable-line no-constant-condition
await this.checkNewVersionAvailable();
await utils.sleep(3600*1000); //каждый час
}
//дальше кода нет
})();
} }
loadSettings() { loadSettings() {
@@ -301,9 +399,17 @@ class Reader extends Vue {
this.copyFullText = settings.copyFullText; this.copyFullText = settings.copyFullText;
this.showClickMapPage = settings.showClickMapPage; this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl; this.clickControl = settings.clickControl;
this.clickControlActive = this.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton; this.showToolButton = settings.showToolButton;
this.enableSitesFilter = settings.enableSitesFilter; this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara;
this.djvuQuality = settings.djvuQuality;
this.pdfAsText = settings.pdfAsText;
this.pdfQuality = settings.pdfQuality;
this.dualPageMode = settings.dualPageMode;
this.userWallpapers = settings.userWallpapers;
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys); this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
this.$root.readerActionByKeyEvent = (event) => { this.$root.readerActionByKeyEvent = (event) => {
@@ -311,6 +417,71 @@ class Reader extends Vue {
} }
this.updateHeaderMinWidth(); this.updateHeaderMinWidth();
this.loadWallpapers();//no await
}
showHelpOnErrorIfNeeded(errorMessage) {
//небольшая эвристика
let i = errorMessage.indexOf('http://');
if (i < 0)
i = errorMessage.indexOf('https://');
errorMessage = errorMessage.substring(i + 7);
const perCount = errorMessage.split('%').length - 1;
if (perCount > errorMessage.length/3.2) {
this.$refs.dialogs.showUrlHelp();
return true;
}
return false;
}
//wallpaper css
async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength();
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
this.wallpaperDataLength = wallpaperDataLength;
let newCss = '';
for (const wp of this.userWallpapers) {
const data = await wallpaperStorage.getData(wp.cssClass);
if (!data) {
//здесь будем восстанавливать данные с сервера
}
if (data) {
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
}
}
dynamicCss.replace('wallpapers', newCss);
}
}
async checkNewVersionAvailable() {
if (!this.checkingNewVersion && this.showNeedUpdateNotify) {
this.checkingNewVersion = true;
try {
await utils.sleep(15*1000); //подождем 15 секунд, чтобы прогрузился ServiceWorker при выходе новой версии
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
let againMes = '';
if (this.isFirstNeedUpdateNotify) {
againMes = ' еще один раз';
}
if (this.version != this.clientVersion)
this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
} catch(e) {
console.error(e);
} finally {
this.checkingNewVersion = false;
}
this.isFirstNeedUpdateNotify = false;
}
} }
updateHeaderMinWidth() { updateHeaderMinWidth() {
@@ -394,6 +565,14 @@ class Reader extends Vue {
return this.$store.state.config.mode; return this.$store.state.config.mode;
} }
get version() {
return this.$store.state.config.version;
}
get clientVersion() {
return versionHistory[0].version;
}
get routeParamUrl() { get routeParamUrl() {
let result = ''; let result = '';
const path = this.$route.fullPath; const path = this.$route.fullPath;
@@ -449,7 +628,20 @@ class Reader extends Vue {
//сохранение в serverStorage //сохранение в serverStorage
if (value) { if (value) {
await utils.sleep(500); await utils.sleep(500);
await this.$refs.serverStorage.saveRecent(value);
let timer = setTimeout(() => {
if (!this.offlineModeActive)
this.$root.notify.error('Таймаут соединения');
}, 10000);
try {
await this.$refs.serverStorage.saveRecent(value);
} catch (e) {
if (!this.offlineModeActive)
this.$root.notify.error(e.message);
} finally {
clearTimeout(timer);
}
} }
} }
} }
@@ -480,7 +672,7 @@ class Reader extends Vue {
toolBarToggle() { toolBarToggle() {
this.commit('reader/setToolBarActive', !this.toolBarActive); this.commit('reader/setToolBarActive', !this.toolBarActive);
this.$root.$emit('resize'); this.$root.eventHook('resize');
} }
fullScreenToggle() { fullScreenToggle() {
@@ -510,6 +702,28 @@ class Reader extends Vue {
} }
} }
loadFileToggle() {
if (!this.loaderActive)
this.loaderToggle();
this.$nextTick(() => {
const page = this.$refs.page;
if (this.activePage == 'LoaderPage' && page.loadFileClick) {
page.loadFileClick();
}
});
}
loadBufferToggle() {
if (!this.loaderActive)
this.loaderToggle();
this.$nextTick(() => {
const page = this.$refs.page;
if (this.activePage == 'LoaderPage' && page.showPasteText) {
page.showPasteText();
}
});
}
setPositionToggle() { setPositionToggle() {
this.setPositionActive = !this.setPositionActive; this.setPositionActive = !this.setPositionActive;
const page = this.$refs.page; const page = this.$refs.page;
@@ -593,12 +807,6 @@ class Reader extends Vue {
} }
} }
refreshBookSplitToPara() {
if (this.mostRecentBook()) {
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
}
}
recentBooksClose() { recentBooksClose() {
this.recentBooksActive = false; this.recentBooksActive = false;
} }
@@ -643,6 +851,12 @@ class Reader extends Vue {
} }
} }
clickControlToggle() {
const newSettings = _.cloneDeep(this.settings);
newSettings.clickControl = !this.clickControl;
this.commit('reader/setSettings', newSettings);
}
offlineModeToggle() { offlineModeToggle() {
this.offlineModeActive = !this.offlineModeActive; this.offlineModeActive = !this.offlineModeActive;
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive; this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
@@ -662,6 +876,12 @@ class Reader extends Vue {
} }
} }
convOptionsToggle() {
this.settingsToggle();
if (this.settingsActive)
this.$refs.settingsPage.selectedTab = 'convert';
}
helpToggle() { helpToggle() {
this.helpActive = !this.helpActive; this.helpActive = !this.helpActive;
if (this.helpActive) { if (this.helpActive) {
@@ -689,9 +909,8 @@ class Reader extends Vue {
} }
refreshBook() { refreshBook() {
if (this.mostRecentBook()) { const mrb = this.mostRecentBook();
this.loadBook({url: this.mostRecentBook().url, force: true}); this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
}
} }
undoAction() { undoAction() {
@@ -726,16 +945,19 @@ class Reader extends Vue {
switch (action) { switch (action) {
case 'loader': case 'loader':
case 'loadFile':
case 'loadBuffer':
case 'help':
case 'fullScreen': case 'fullScreen':
case 'setPosition': case 'setPosition':
case 'scrolling':
case 'search': case 'search':
case 'copyText': case 'copyText':
case 'splitToPara': case 'convOptions':
case 'refresh': case 'refresh':
case 'contents': case 'contents':
case 'libs': case 'libs':
case 'recentBooks': case 'recentBooks':
case 'clickControl':
case 'offlineMode': case 'offlineMode':
case 'settings': case 'settings':
if (this.progressActive) { if (this.progressActive) {
@@ -744,6 +966,13 @@ class Reader extends Vue {
classResult = classActive; classResult = classActive;
} }
break; break;
case 'scrolling':
if (this.progressActive || this.dualPageMode) {
classResult = classDisabled;
} else if (this[`${action}Active`]) {
classResult = classActive;
}
break;
case 'undoAction': case 'undoAction':
if (this.actionCur <= 0) if (this.actionCur <= 0)
classResult = classDisabled; classResult = classDisabled;
@@ -765,7 +994,6 @@ class Reader extends Vue {
case 'contents': case 'contents':
classResult = classDisabled; classResult = classDisabled;
break; break;
case 'splitToPara':
case 'refresh': case 'refresh':
case 'recentBooks': case 'recentBooks':
if (!this.mostRecentBookReactive) if (!this.mostRecentBookReactive)
@@ -786,7 +1014,7 @@ class Reader extends Vue {
} }
get activePage() { get activePage() {
let result = ''; let result = undefined;
if (this.progressActive) if (this.progressActive)
result = 'ProgressPage'; result = 'ProgressPage';
@@ -801,7 +1029,7 @@ class Reader extends Vue {
} }
if (result != 'TextPage') { if (result != 'TextPage') {
this.$root.$emit('set-app-title'); this.$root.setAppTitle();
} }
// на LoaderPage всегда показываем toolBar // на LoaderPage всегда показываем toolBar
@@ -816,7 +1044,7 @@ class Reader extends Vue {
const isParsed = await bookManager.hasBookParsed(last); const isParsed = await bookManager.hasBookParsed(last);
if (!isParsed) { if (!isParsed) {
this.$root.$emit('set-app-title'); this.$root.setAppTitle();
return; return;
} }
@@ -846,8 +1074,12 @@ class Reader extends Vue {
let url = encodeURI(decodeURI(opts.url)); let url = encodeURI(decodeURI(opts.url));
//TODO: убрать конвертирование 'file://' после 06.2021
if (url.length == 71 && url.indexOf('file://') == 0)
url = url.replace(/^file/, 'disk');
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) && if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('file://') != 0)) (url.indexOf('disk://') != 0))
url = 'http://' + url; url = 'http://' + url;
// уже просматривается сейчас // уже просматривается сейчас
@@ -878,6 +1110,7 @@ class Reader extends Vue {
wasOpened = (wasOpened ? wasOpened : {}); wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos); const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen); const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
let book = null; let book = null;
@@ -922,9 +1155,13 @@ class Reader extends Vue {
if (!book) { if (!book) {
book = await readerApi.loadBook({ book = await readerApi.loadBook({
url, url,
skipCheck: (opts.skipCheck ? true : false), uploadFileName,
isText: (opts.isText ? true : false), enableSitesFilter: this.enableSitesFilter,
enableSitesFilter: this.enableSitesFilter skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false),
djvuQuality: this.djvuQuality,
pdfAsText: this.pdfAsText,
pdfQuality: this.pdfQuality,
}, },
(state) => { (state) => {
progress.setState(state); progress.setState(state);
@@ -940,7 +1177,7 @@ class Reader extends Vue {
}); });
// добавляем в историю // добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook)); await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
this.mostRecentBook(); this.mostRecentBook();
this.addAction(bookPos); this.addAction(bookPos);
this.updateRoute(true); this.updateRoute(true);
@@ -957,7 +1194,11 @@ class Reader extends Vue {
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'}); if (!this.showHelpOnErrorIfNeeded(e.message)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
} finally {
this.checkNewVersionAvailable();
} }
} }
@@ -977,7 +1218,7 @@ class Reader extends Vue {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
await this.loadBook({url}); await this.loadBook({url, uploadFileName: opts.file.name, force: true});
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.loaderActive = true; this.loaderActive = true;
@@ -1021,6 +1262,12 @@ class Reader extends Vue {
case 'loader': case 'loader':
this.loaderToggle(); this.loaderToggle();
break; break;
case 'loadFile':
this.loadFileToggle();
break;
case 'loadBuffer':
this.loadBufferToggle();
break;
case 'help': case 'help':
this.helpToggle(); this.helpToggle();
break; break;
@@ -1048,8 +1295,8 @@ class Reader extends Vue {
case 'copyText': case 'copyText':
this.copyTextToggle(); this.copyTextToggle();
break; break;
case 'splitToPara': case 'convOptions':
this.refreshBookSplitToPara(); this.convOptionsToggle();
break; break;
case 'refresh': case 'refresh':
this.refreshBook(); this.refreshBook();
@@ -1063,6 +1310,9 @@ class Reader extends Vue {
case 'recentBooks': case 'recentBooks':
this.recentBooksToggle(); this.recentBooksToggle();
break; break;
case 'clickControl':
this.clickControlToggle();
break;
case 'offlineMode': case 'offlineMode':
this.offlineModeToggle(); this.offlineModeToggle();
break; break;
@@ -1131,7 +1381,7 @@ class Reader extends Vue {
keyHook(event) { keyHook(event) {
let result = false; let result = false;
if (this.$root.rootRoute() == '/reader') { if (this.$root.getRootRoute() == '/reader') {
if (this.$root.stdDialog.active) if (this.$root.stdDialog.active)
return result; return result;
@@ -1165,18 +1415,21 @@ class Reader extends Vue {
if (!result && event.type == 'keydown') { if (!result && event.type == 'keydown') {
const action = this.$root.readerActionByKeyEvent(event); const action = this.$root.readerActionByKeyEvent(event);
if (action == 'loader') { /*if (action == 'loader') {
result = this.doAction({action, event}); result = this.doAction({action, event});
} }
if (!result && this.activePage == 'TextPage') { if (!result && this.activePage == 'TextPage') {
result = this.doAction({action, event}); result = this.doAction({action, event});
} }*/
result = this.doAction({action, event});
} }
} }
return result; return result;
} }
} }
export default vueComponent(Reader);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,20 +1,25 @@
<template> <template>
<div> <div>
<Dialog ref="dialog1" v-model="whatsNewVisible"> <Dialog ref="dialog1" v-model="whatsNewVisible">
<template slot="header"> <template #header>
Что нового: Что нового:
</template> </template>
<div style="line-height: 20px" v-html="whatsNewContent"></div> <div style="line-height: 20px; min-width: 300px">
<div v-html="whatsNewContent"></div>
</div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span> <span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn> <template #footer>
</span> <q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
Больше не показывать
</q-btn>
</template>
</Dialog> </Dialog>
<Dialog ref="dialog2" v-model="donationVisible"> <Dialog ref="dialog2" v-model="donationVisible">
<template slot="header"> <template #header>
Здравствуйте, уважаемые читатели! Здравствуйте, уважаемые читатели!
</template> </template>
@@ -35,7 +40,9 @@
Автор также обращается с просьбой о помощи в распространении Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a> <a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')"> <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-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon> </q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр. на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом. Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
@@ -47,60 +54,53 @@
<br><br> <br><br>
<div class="row justify-center"> <div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn> <!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
Помочь проекту
</q-btn-->
</div> </div>
</div> </div>
<span slot="footer"> <template #footer>
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span> <span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br> <br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn> <q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">
</span> Напомнить позже
</q-btn>
</template>
</Dialog> </Dialog>
<Dialog ref="dialog3" v-model="liberamaTopVisible"> <Dialog ref="dialog3" v-model="urlHelpVisible">
<template slot="header"> <template #header>
Здравствуйте, уважаемые читатели! Обнаружена невалидная ссылка в поле "URL книги".
<br>
</template> </template>
<div style="word-break: normal"> <div style="word-break: normal">
Создан новый ресурс:<br><br> Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
<a href="https://liberama.top" target="_blank">liberama.top</a>
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
<br><br> <br><br>
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами: Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
<ul> <q-icon class="q-mr-xs" name="la la-comment" size="24px" />
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li> Из буфера обмена
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li> </q-btn>
</ul> на странице загрузки.
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
Спасибо, что вы с нами!
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div> </div>
<span slot="footer">
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
</span>
</Dialog> </Dialog>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Dialog from '../../share/Dialog.vue'; import Dialog from '../../share/Dialog.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
export default @Component({ const componentOptions = {
components: { components: {
Dialog Dialog
}, },
@@ -109,12 +109,14 @@ export default @Component({
this.loadSettings(); this.loadSettings();
}, },
}, },
}) };
class ReaderDialogs extends Vue { class ReaderDialogs {
_options = componentOptions;
whatsNewVisible = false; whatsNewVisible = false;
whatsNewContent = ''; whatsNewContent = '';
donationVisible = false; donationVisible = false;
liberamaTopVisible = false; urlHelpVisible = false;
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -127,23 +129,21 @@ class ReaderDialogs extends Vue {
async init() { async init() {
await this.showWhatsNew(); await this.showWhatsNew();
await this.showDonation(); await this.showDonation();
await this.showLiberamaTop();
} }
loadSettings() { loadSettings() {
const settings = this.settings; const settings = this.settings;
this.showWhatsNewDialog = settings.showWhatsNewDialog; this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020; this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
} }
async showWhatsNew() { async showWhatsNew() {
const whatsNew = versionHistory[0]; const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog && if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') && whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) { this.whatsNewHeader != this.whatsNewContentHash) {
await utils.sleep(2000); await utils.sleep(2000);
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content; this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
this.whatsNewVisible = true; this.whatsNewVisible = true;
} }
} }
@@ -157,6 +157,15 @@ class ReaderDialogs extends Vue {
} }
} }
async showUrlHelp() {
this.urlHelpVisible = true;
}
loadBufferClick() {
this.$emit('load-buffer-toggle');
this.urlHelpVisible = false;
}
donationDialogDisable() { donationDialogDisable() {
this.donationVisible = false; this.donationVisible = false;
if (this.showDonationDialog2020) { if (this.showDonationDialog2020) {
@@ -171,7 +180,6 @@ class ReaderDialogs extends Vue {
openDonate() { openDonate() {
this.donationVisible = false; this.donationVisible = false;
this.liberamaTopVisible = false;
this.$emit('donate-toggle'); this.$emit('donate-toggle');
} }
@@ -190,8 +198,11 @@ class ReaderDialogs extends Vue {
whatsNewDisable() { whatsNewDisable() {
this.whatsNewVisible = false; this.whatsNewVisible = false;
const whatsNew = versionHistory[0]; this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
this.commit('reader/setWhatsNewContentHash', whatsNew.header); }
get whatsNewHeader() {
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
} }
get mode() { get mode() {
@@ -210,28 +221,14 @@ class ReaderDialogs extends Vue {
return this.$store.state.reader.donationRemindDate; return this.$store.state.reader.donationRemindDate;
} }
async showLiberamaTop() {
const today = utils.formatDate(new Date(), 'coDate');
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
await utils.sleep(3000);
this.liberamaTopVisible = true;
}
}
liberamaTopDialogDisable() {
this.liberamaTopVisible = false;
if (this.showLiberamaTopDialog2020) {
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
}
}
keyHook() { keyHook() {
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active) if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
return true; return true;
return false; return false;
} }
} }
export default vueComponent(ReaderDialogs);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,40 +1,49 @@
<template> <template>
<Window width="600px" ref="window" @close="close"> <Window ref="window" width="600px" @close="close">
<template slot="header"> <template #header>
<span v-show="!loading">{{ header }}</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> <span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7" />
Список загружается
</span>
</template> </template>
<a ref="download" style='display: none;' target="_blank"></a> <a ref="download" style="display: none;" target="_blank"></a>
<q-table <q-table
class="recent-books-table col" class="recent-books-table col"
:data="tableData" :rows="tableData"
:columns="columns"
row-key="key" row-key="key"
:pagination.sync="pagination" :columns="columns"
:pagination="pagination"
separator="cell" separator="cell"
hide-bottom hide-bottom
virtual-scroll virtual-scroll
dense dense
> >
<template v-slot:header="props"> <template #header="props">
<q-tr :props="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 key="num" class="td-mp" style="width: 25px" :props="props">
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th> <span v-html="props.cols[0].label"></span>
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4"> </q-th>
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white" <q-th key="date" class="td-mp break-word" style="width: 77px" :props="props">
<span v-html="props.cols[1].label"></span>
</q-th>
<q-th key="desc" class="td-mp" style="width: 332px" :props="props" colspan="4">
<q-input ref="input" v-model="search"
outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
placeholder="Найти" placeholder="Найти"
v-model="search"
@click.stop @click.stop
/> >
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
</template>
</q-input>
<span v-html="props.cols[2].label"></span> <span v-html="props.cols[2].label"></span>
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template #body="props">
<q-tr :props="props"> <q-tr :props="props">
<q-td key="num" :props="props" class="td-mp" auto-width> <q-td key="num" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 25px"> <div class="break-word" style="width: 25px">
@@ -42,24 +51,27 @@
</div> </div>
</q-td> </q-td>
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width> <q-td key="date" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
<div class="break-word" style="width: 68px"> <div class="break-word" style="width: 68px">
{{ props.row.touchDate }}<br> {{ props.row.touchDate }}<br>
{{ props.row.touchTime }} {{ props.row.touchTime }}
</div> </div>
</q-td> </q-td>
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width> <q-td key="desc" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
<div class="break-word" style="width: 332px; font-size: 90%"> <div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">{{ props.row.desc.author }}</div> <div style="color: green">
{{ props.row.desc.author }}
</div>
<div>{{ props.row.desc.title }}</div> <div>{{ props.row.desc.title }}</div>
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
</div> </div>
</q-td> </q-td>
<q-td key="links" :props="props" class="td-mp" auto-width> <q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%"> <div class="break-word" style="width: 75px; font-size: 90%">
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br> <a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a> <a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
</div> </div>
</q-td> </q-td>
@@ -68,8 +80,9 @@
<q-btn <q-btn
dense dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px" style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(props.row.key)"> @click="handleDel(props.row.key)"
<q-icon class="la la-times" size="14px" style="top: -6px"/> >
<q-icon class="la la-times" size="14px" />
</q-btn> </q-btn>
</div> </div>
</q-td> </q-td>
@@ -78,23 +91,22 @@
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import path from 'path'; import path from 'path-browserify';
import _ from 'lodash'; //import _ from 'lodash';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader'; import readerApi from '../../../api/reader';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
@@ -103,15 +115,18 @@ export default @Component({
this.updateTableData(); this.updateTableData();
} }
}, },
}) };
class RecentBooksPage extends Vue { class RecentBooksPage {
_options = componentOptions;
loading = false; loading = false;
search = null; search = '';
tableData = []; tableData = [];
columns = []; columns = [];
pagination = {}; pagination = {};
created() { created() {
this.firstInit = true;
this.pagination = {rowsPerPage: 0}; this.pagination = {rowsPerPage: 0};
this.columns = [ this.columns = [
@@ -166,28 +181,13 @@ class RecentBooksPage extends Vue {
return; return;
this.initing = true; this.initing = true;
if (this.firstInit) {//для отзывчивости
if (!bookManager.loaded) { await this.updateTableData(20);
await this.updateTableData(10); this.firstInit = false;
//для отзывчивости
await utils.sleep(100);
let i = 0;
let j = 5;
while (i < 500 && !bookManager.loaded) {
if (i % j == 0) {
bookManager.sortedRecentCached = null;
await this.updateTableData(20);
j *= 2;
}
await utils.sleep(100);
i++;
}
} else {
//для отзывчивости
await utils.sleep(100);
} }
await utils.sleep(50);
await this.updateTableData(); await this.updateTableData();
this.initing = false; this.initing = false;
})(); })();
} }
@@ -214,38 +214,21 @@ class RecentBooksPage extends Vue {
d.setTime(book.touchTime); d.setTime(book.touchTime);
const t = utils.formatDate(d).split(' '); const t = utils.formatDate(d).split(' ');
let readPart = 0;
let perc = ''; let perc = '';
let textLen = ''; let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0)); const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) { if (book.textLength) {
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`; readPart = p/book.textLength;
perc = ` [${(readPart*100).toFixed(2)}%]`;
textLen = ` ${Math.round(book.textLength/1000)}k`; textLen = ` ${Math.round(book.textLength/1000)}k`;
} }
const fb2 = (book.fb2 ? book.fb2 : {}); const bt = utils.getBookTitle(book.fb2);
let title = fb2.bookTitle; let title = bt.bookTitle;
if (title) title = (title ? `"${title}"`: '');
title = `"${title}"`; const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
else
title = '';
let author = '';
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
author = authorNames.join(', ');
} else {//TODO: убрать в будущем
author = _.compact([
fb2.lastName,
fb2.firstName,
fb2.middleName
]).join(' ');
}
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
result.push({ result.push({
num, num,
@@ -256,9 +239,11 @@ class RecentBooksPage extends Vue {
author, author,
title: `${title}${perc}${textLen}`, title: `${title}${perc}${textLen}`,
}, },
descString: `${author}${title}${perc}${textLen}`, readPart,
descString: `${author}${title}${perc}${textLen}`,//для сортировки
url: book.url, url: book.url,
path: book.path, path: book.path,
fullTitle: bt.fullTitle,
key: book.key, key: book.key,
}); });
} }
@@ -276,6 +261,11 @@ class RecentBooksPage extends Vue {
this.updating = false; this.updating = false;
} }
resetSearch() {
this.search = '';
this.$refs.input.focus();
}
wordEnding(num) { wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', '']; const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
const deci = num % 100; const deci = num % 100;
@@ -291,13 +281,18 @@ class RecentBooksPage extends Vue {
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`; return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
} }
async downloadBook(fb2path) { async downloadBook(fb2path, fullTitle) {
try { try {
await readerApi.checkCachedBook(fb2path); await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download; const d = this.$refs.download;
d.href = fb2path; d.href = fb2path;
d.download = path.basename(fb2path).substr(0, 10) + '.fb2'; try {
const fn = utils.makeValidFilename(fullTitle);
d.download = fn.substring(0, 100) + '.fb2';
} catch(e) {
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
}
d.click(); d.click();
} catch (e) { } catch (e) {
@@ -308,14 +303,6 @@ class RecentBooksPage extends Vue {
} }
} }
openOriginal(url) {
window.open(url, '_blank');
}
openFb2(path) {
window.open(path, '_blank');
}
async handleDel(key) { async handleDel(key) {
await bookManager.delRecentBook({key}); await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent //this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
@@ -331,7 +318,7 @@ class RecentBooksPage extends Vue {
isUrl(url) { isUrl(url) {
if (url) if (url)
return (url.indexOf('file://') != 0); return (url.indexOf('disk://') != 0);
else else
return false; return false;
} }
@@ -347,6 +334,8 @@ class RecentBooksPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(RecentBooksPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -381,6 +370,10 @@ class RecentBooksPage extends Vue {
white-space: normal; white-space: normal;
} }
.read-bar {
height: 3px;
background-color: #aaaaaa;
}
</style> </style>
<style> <style>

View File

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

View File

@@ -4,8 +4,8 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
@@ -18,7 +18,7 @@ const ssCacheStore = localForage.createInstance({
name: 'ssCacheStore' name: 'ssCacheStore'
}); });
export default @Component({ const componentOptions = {
watch: { watch: {
serverSyncEnabled: function() { serverSyncEnabled: function() {
this.serverSyncEnabledChanged(); this.serverSyncEnabledChanged();
@@ -39,14 +39,16 @@ export default @Component({
this.debouncedSaveLibs(); this.debouncedSaveLibs();
}, },
}, },
}) };
class ServerStorage extends Vue { class ServerStorage {
_options = componentOptions;
created() { created() {
this.inited = false; this.inited = false;
this.keyInited = false; this.keyInited = false;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.prevServerStorageKey = null; this.prevServerStorageKey = null;
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()}); this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
this.debouncedSaveSettings = _.debounce(() => { this.debouncedSaveSettings = _.debounce(() => {
this.saveSettings(); this.saveSettings();
@@ -216,8 +218,15 @@ class ServerStorage extends Vue {
} }
error(message) { error(message) {
if (this.showServerStorageMessages && !this.offlineModeActive) if (this.showServerStorageMessages && !this.offlineModeActive) {
this.$root.notify.error(message); this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
const hint = (this.errorMessageCounter < 2 ? '' :
'<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
'<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
);
this.$root.notify.error(message + hint);
}
} }
async loadSettings(force = false, doNotifySuccess = true) { async loadSettings(force = false, doNotifySuccess = true) {
@@ -507,10 +516,10 @@ class ServerStorage extends Vue {
if (md.key && result[md.key]) if (md.key && result[md.key])
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true}); result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
if (!bookManager.loaded) { /*if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией'); this.warning('Ожидание загрузки списка книг перед синхронизацией');
while (!bookManager.loaded) await utils.sleep(100); while (!bookManager.loaded) await utils.sleep(100);
} }*/
if (newRecent.rev != this.cachedRecent.rev) if (newRecent.rev != this.cachedRecent.rev)
await this.setCachedRecent(newRecent); await this.setCachedRecent(newRecent);
@@ -567,7 +576,7 @@ class ServerStorage extends Vue {
newRecentPatch.rev++; newRecentPatch.rev++;
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]); newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
let applyMod = this.cachedRecentMod.data; const applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key]) if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true}); newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
@@ -580,8 +589,8 @@ class ServerStorage extends Vue {
let newRecent = {}; let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) { if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent //ждем весь bm.recent
while (!bookManager.loaded) /*while (!bookManager.loaded)
await utils.sleep(100); await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)}; newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}}; newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
@@ -618,7 +627,7 @@ class ServerStorage extends Vue {
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`); this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) { if (!recurse && itemKey) {
this.savingRecent = false; this.savingRecent = false;
this.saveRecent(itemKey, true); await this.saveRecent(itemKey, true);
return; return;
} }
} else if (result.state == 'success') { } else if (result.state == 'success') {
@@ -727,5 +736,7 @@ class ServerStorage extends Vue {
return result; return result;
} }
} }
export default vueComponent(ServerStorage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,16 +1,17 @@
<template> <template>
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close"> <Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header"> <template #header>
Установить позицию Установить позицию
</template> </template>
<div id="set-position-slider" class="slider q-px-md"> <div id="set-position-slider" class="slider q-px-md">
<q-slider <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" v-model="sliderValue"
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
:max="sliderMax" :max="sliderMax"
label label
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)" :label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary" color="primary"
/> />
</div> </div>
@@ -19,12 +20,11 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
@@ -34,8 +34,10 @@ export default @Component({
this.$emit('book-pos-changed', {bookPos: newValue}); this.$emit('book-pos-changed', {bookPos: newValue});
}, },
}, },
}) };
class SetPositionPage extends Vue { class SetPositionPage {
_options = componentOptions;
sliderValue = null; sliderValue = null;
sliderMax = null; sliderMax = null;
@@ -67,6 +69,8 @@ class SetPositionPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SetPositionPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -3,8 +3,7 @@
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'"> <div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div> <div class="label-3"></div>
<div class="col row"> <div class="col row">
<q-checkbox size="xs" @input="changeShowToolButton(item.name)" <q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
:value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/> />
</div> </div>
</div> </div>

View File

@@ -0,0 +1,87 @@
<!---------------------------------------------->
<div class="q-mt-sm column items-center">
<span>Настройки конвертирования применяются ко всем</span>
<span>вновь загружаемым или обновляемым файлам</span>
</div>
<!---------------------------------------------->
<div class="part-header">HTML, XML, TXT</div>
<div class="item row">
<div class="label-7">Текст</div>
<div class="col row">
<q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Опция принудительно включает эвристику разбиения текста на<br>
параграфы в случае, если формат файла определен как html,<br>
xml или txt. Возможна нечитабельная разметка текста.
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-7">Сайты</div>
<div class="col row">
<q-checkbox v-model="enableSitesFilter" 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 v-if="isExternalConverter">
<div class="part-header">PDF</div>
<div class="item row">
<div class="label-7">Формат</div>
<div class="col row">
<q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
Размер получаемого fb2-файла при этом относительно небольшой.<br>
При отключении этой опции, pdf будет представлен как набор<br>
изображений (аналогично ковертированию djvu).
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-7">Качество</div>
<div class="col row">
<NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
размер итогового файла. Если сервер отказывается конвертировать<br>
слишком большой файл, то попробуйте понизить качество.
</q-tooltip>
</NumInput>
</div>
</div>
</div>
<!---------------------------------------------->
<div v-if="isExternalConverter">
<div class="part-header">DJVU</div>
<div class="item row">
<div class="label-7">Качество</div>
<div class="col row">
<NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
размер итогового файла. Если сервер отказывается конвертировать<br>
слишком большой файл, то попробуйте понизить качество.
</q-tooltip>
</NumInput>
</div>
</div>
</div>

View File

@@ -36,12 +36,23 @@
Показывать уведомление "Что нового" Показывать уведомление "Что нового"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомления "Что нового"<br> Показывать уведомления "Что нового"<br>
при каждом выходе новой версии читалки при появлении новой версии читалки
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
</div> </div>
<div class="item row"> <div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
Показывать уведомление о новой версии
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Напоминать о необходимости обновления страницы<br>
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<!--div class="item row">
<div class="label-6">Уведомление</div> <div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showDonationDialog2020"> <q-checkbox size="xs" v-model="showDonationDialog2020">
Показывать "Оплатим хостинг вместе" Показывать "Оплатим хостинг вместе"
@@ -49,27 +60,11 @@
Показывать уведомление "Оплатим хостинг вместе" Показывать уведомление "Оплатим хостинг вместе"
</q-tooltip> </q-tooltip>
</q-checkbox> </q-checkbox>
</div> </div-->
<!----------------------------------------------> <!---------------------------------------------->
<div class="part-header">Другое</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="item row">
<div class="label-6">Обработка</div> <div class="label-6">Обработка</div>
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста"> <q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">

View File

@@ -1,6 +1,6 @@
<template lang="includer"> <template>
<Window ref="window" height="95%" width="600px" @close="close"> <Window ref="window" height="95%" width="600px" @close="close">
<template slot="header"> <template #header>
Настройки Настройки
</template> </template>
@@ -8,8 +8,9 @@
<div class="full-height"> <div class="full-height">
<q-tabs <q-tabs
ref="tabs" ref="tabs"
class="bg-grey-3 text-black"
v-model="selectedTab" v-model="selectedTab"
class="bg-grey-3 text-black"
left-icon="la la-caret-up" left-icon="la la-caret-up"
right-icon="la la-caret-down" right-icon="la la-caret-down"
active-color="white" active-color="white"
@@ -20,70 +21,115 @@
stretch stretch
inline-label inline-label
> >
<div v-show="tabsScrollable" class="q-pt-lg"/> <div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" /> <q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" /> <q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" /> <q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" /> <q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" /> <q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" /> <q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" /> <q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
<div v-show="tabsScrollable" class="q-pt-lg"/> <div v-show="tabsScrollable" class="q-pt-lg" />
</q-tabs> </q-tabs>
</div> </div>
<div class="col fit"> <div class="col fit">
<!-- Профили ---------------------------------------------------------------------> <!-- Профили --------------------------------------------------------------------->
<div v-if="selectedTab == 'profiles'" class="fit tab-panel"> <div v-if="selectedTab == 'profiles'" class="fit tab-panel">
@@include('./include/ProfilesTab.inc'); @@include('./ProfilesTab.inc');
</div> </div>
<!-- Вид -------------------------------------------------------------------------> <!-- Вид ------------------------------------------------------------------------->
<div v-if="selectedTab == 'view'" class="fit column"> <div v-if="selectedTab == 'view'" class="fit column">
@@include('./include/ViewTab.inc'); <q-tabs
v-model="selectedViewTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="mode" label="Режим" />
<q-tab name="color" label="Цвет" />
<q-tab name="font" label="Шрифт" />
<q-tab name="text" label="Текст" />
<q-tab name="status" label="Строка статуса" />
</q-tabs>
<div class="q-mb-sm" />
<div class="col tab-panel">
<div v-if="selectedViewTab == 'mode'">
@@include('./ViewTab/Mode.inc');
</div>
<div v-if="selectedViewTab == 'color'">
@@include('./ViewTab/Color.inc');
</div>
<div v-if="selectedViewTab == 'font'">
@@include('./ViewTab/Font.inc');
</div>
<div v-if="selectedViewTab == 'text'">
@@include('./ViewTab/Text.inc');
</div>
<div v-if="selectedViewTab == 'status'">
@@include('./ViewTab/Status.inc');
</div>
</div>
</div> </div>
<!-- Кнопки ----------------------------------------------------------------------> <!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel"> <div v-if="selectedTab == 'buttons'" class="fit tab-panel">
@@include('./include/ButtonsTab.inc'); @@include('./ButtonsTab.inc');
</div> </div>
<!-- Управление ------------------------------------------------------------------> <!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column"> <div v-if="selectedTab == 'keys'" class="fit column">
@@include('./include/KeysTab.inc'); @@include('./KeysTab.inc');
</div> </div>
<!-- Листание --------------------------------------------------------------------> <!-- Листание -------------------------------------------------------------------->
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel"> <div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
@@include('./include/PageMoveTab.inc'); @@include('./PageMoveTab.inc');
</div>
<!-- Конвертирование ------------------------------------------------------------->
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
@@include('./ConvertTab.inc');
</div> </div>
<!-- Прочее ----------------------------------------------------------------------> <!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel"> <div v-if="selectedTab == 'others'" class="fit tab-panel">
@@include('./include/OthersTab.inc'); @@include('./OthersTab.inc');
</div> </div>
<!-- Сброс -----------------------------------------------------------------------> <!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel"> <div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./include/ResetTab.inc'); @@include('./ResetTab.inc');
</div> </div>
</div> </div>
</div> </div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import { ref, watch } from 'vue';
import Component from 'vue-class-component'; import vueComponent from '../../vueComponent.js';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue'; import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue'; import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
import defPalette from './defPalette'; import defPalette from './defPalette';
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/; const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
NumInput, NumInput,
@@ -97,8 +143,9 @@ export default @Component({
this.settingsChanged(); this.settingsChanged();
}, },
form: function(newValue) { form: function(newValue) {
if (this.inited) if (this.inited) {
this.commit('reader/setSettings', newValue); this.commit('reader/setSettings', _.cloneDeep(newValue));
}
}, },
fontBold: function(newValue) { fontBold: function(newValue) {
this.fontWeight = (newValue ? 'bold' : ''); this.fontWeight = (newValue ? 'bold' : '');
@@ -108,7 +155,7 @@ export default @Component({
}, },
vertShift: function(newValue) { vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName); const font = (this.webFontName ? this.webFontName : this.fontName);
if (this.fontShifts[font] != newValue) { if (this.fontShifts[font] != newValue || this.fontVertShift != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue}); this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue; this.fontVertShift = newValue;
} }
@@ -125,6 +172,10 @@ export default @Component({
if (newValue != '' && this.pageChangeAnimation == 'flip') if (newValue != '' && this.pageChangeAnimation == 'flip')
this.pageChangeAnimation = ''; this.pageChangeAnimation = '';
}, },
dualPageMode(newValue) {
if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
this.pageChangeAnimation = '';
},
textColor: function(newValue) { textColor: function(newValue) {
this.textColorFiltered = newValue; this.textColorFiltered = newValue;
}, },
@@ -139,19 +190,35 @@ export default @Component({
if (hex.test(newValue)) if (hex.test(newValue))
this.backgroundColor = newValue; this.backgroundColor = newValue;
}, },
dualDivColor(newValue) {
this.dualDivColorFiltered = newValue;
},
dualDivColorFiltered(newValue) {
if (hex.test(newValue))
this.dualDivColor = newValue;
},
statusBarColor(newValue) {
this.statusBarColorFiltered = newValue;
},
statusBarColorFiltered(newValue) {
if (hex.test(newValue))
this.statusBarColor = newValue;
},
}, },
}) };
class SettingsPage extends Vue { class SettingsPage {
_options = componentOptions;
selectedTab = 'profiles'; selectedTab = 'profiles';
selectedViewTab = 'color'; selectedViewTab = 'mode';
selectedKeysTab = 'mouse'; selectedKeysTab = 'mouse';
form = {};
fontBold = false; fontBold = false;
fontItalic = false; fontItalic = false;
vertShift = 0; vertShift = 0;
tabsScrollable = false; tabsScrollable = false;
textColorFiltered = ''; textColorFiltered = '';
bgColorFiltered = ''; bgColorFiltered = '';
dualDivColorFiltered = '';
webFonts = []; webFonts = [];
fonts = []; fonts = [];
@@ -160,6 +227,19 @@ class SettingsPage extends Vue {
toolButtons = []; toolButtons = [];
rstore = {}; rstore = {};
setup() {
const settingsProps = { form: ref({}) };
for (let prop in rstore.settingDefaults) {
settingsProps[prop] = ref(_.cloneDeep(rstore.settingDefaults[prop]));
watch(settingsProps[prop], (newValue) => {
settingsProps.form.value = Object.assign({}, settingsProps.form.value, {[prop]: newValue});
}, {deep: true});
}
return settingsProps;
}
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.reader = this.$store.state.reader; this.reader = this.$store.state.reader;
@@ -174,7 +254,7 @@ class SettingsPage extends Vue {
this.$watch( this.$watch(
'$refs.tabs.scrollable', '$refs.tabs.scrollable',
(newValue) => { (newValue) => {
this.tabsScrollable = newValue && !this.$isMobileDevice; this.tabsScrollable = newValue && !this.$root.isMobileDevice;
} }
); );
} }
@@ -189,18 +269,8 @@ class SettingsPage extends Vue {
return; return;
this.form = Object.assign({}, this.settings); this.form = Object.assign({}, this.settings);
if (!this.unwatch) for (const prop in rstore.settingDefaults) {
this.unwatch = {}; this[prop] = _.cloneDeep(this.form[prop]);
for (let prop in rstore.settingDefaults) {
if (this.unwatch && this.unwatch[prop])
this.unwatch[prop]();
this[prop] = this.form[prop];
this.unwatch[prop] = this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue});
});
} }
this.fontBold = (this.fontWeight == 'bold'); this.fontBold = (this.fontWeight == 'bold');
@@ -212,12 +282,18 @@ class SettingsPage extends Vue {
this.vertShift = this.fontShifts[font] || 0; this.vertShift = this.fontShifts[font] || 0;
this.textColorFiltered = this.textColor; this.textColorFiltered = this.textColor;
this.bgColorFiltered = this.backgroundColor; this.bgColorFiltered = this.backgroundColor;
this.dualDivColorFiltered = this.dualDivColor;
this.statusBarColorFiltered = this.statusBarColor;
} }
get mode() { get mode() {
return this.$store.state.config.mode; return this.$store.state.config.mode;
} }
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
get settings() { get settings() {
return this.$store.state.reader.settings; return this.$store.state.reader.settings;
} }
@@ -247,9 +323,19 @@ class SettingsPage extends Vue {
get wallpaperOptions() { get wallpaperOptions() {
let result = [{label: 'Нет', value: ''}]; let result = [{label: 'Нет', value: ''}];
for (let i = 1; i < 10; i++) {
const userWallpapers = _.cloneDeep(this.userWallpapers);
userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
for (const wp of userWallpapers) {
if (wallpaperStorage.keyExists(wp.cssClass))
result.push({label: wp.label, value: wp.cssClass});
}
for (let i = 1; i <= 17; i++) {
result.push({label: i, value: `paper${i}`}); result.push({label: i, value: `paper${i}`});
} }
return result; return result;
} }
@@ -273,13 +359,15 @@ class SettingsPage extends Vue {
let result = [ let result = [
{label: 'Нет', value: ''}, {label: 'Нет', value: ''},
{label: 'Вверх-вниз', value: 'downShift'}, {label: 'Вверх-вниз', value: 'downShift'},
{label: 'Вправо-влево', value: 'rightShift'}, (!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
{label: 'Протаивание', value: 'thaw'}, {label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'}, {label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'}, {label: 'Вращение', value: 'rotate'},
(this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
]; ];
if (this.wallpaper == '')
result.push({label: 'Листание', value: 'flip'}); result = result.filter(v => v);
return result; return result;
} }
@@ -342,6 +430,12 @@ class SettingsPage extends Vue {
case 'bg': case 'bg':
result += `background-color: ${this.backgroundColor};` result += `background-color: ${this.backgroundColor};`
break; break;
case 'div':
result += `background-color: ${this.dualDivColor};`
break;
case 'statusbar':
result += `background-color: ${this.statusBarColor};`
break;
} }
return result; return result;
} }
@@ -371,10 +465,6 @@ class SettingsPage extends Vue {
} }
} }
changeShowToolButton(buttonName) {
this.showToolButton = Object.assign({}, this.showToolButton, {[buttonName]: !this.showToolButton[buttonName]});
}
async addProfile() { async addProfile() {
try { try {
if (Object.keys(this.profiles).length >= 100) { if (Object.keys(this.profiles).length >= 100) {
@@ -407,7 +497,7 @@ class SettingsPage extends Vue {
return; return;
try { try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` + const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` + `<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', { `<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; }, inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
@@ -500,7 +590,8 @@ class SettingsPage extends Vue {
}); });
if (result && result.value && result.value.toLowerCase() == 'да') { if (result && result.value && result.value.toLowerCase() == 'да') {
this.$root.$emit('generateNewServerStorageKey'); if (this.$root.generateNewServerStorageKey)
this.$root.generateNewServerStorageKey();
} }
} catch (e) { } catch (e) {
// //
@@ -508,6 +599,71 @@ class SettingsPage extends Vue {
} }
loadWallpaperFileClick() {
this.$refs.file.click();
}
loadWallpaperFile() {
const file = this.$refs.file.files[0];
if (file.size > 10*1024*1024) {
this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
return;
}
if (file.type != 'image/png' && file.type != 'image/jpeg') {
this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
return;
}
if (this.userWallpapers.length >= 100) {
this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
return;
}
this.$refs.file.value = '';
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
(async() => {
const data = e.target.result;
const key = utils.toHex(cryptoUtils.sha256(data));
const label = `#${key.substring(0, 4)}`;
const cssClass = `user-paper${key}`;
const newUserWallpapers = _.cloneDeep(this.userWallpapers);
const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
if (index < 0)
newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass))
await wallpaperStorage.setData(cssClass, data);
this.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass;
})();
}
reader.readAsDataURL(file);
}
}
async delWallpaper() {
if (this.wallpaper.indexOf('user-paper') == 0) {
const newUserWallpapers = [];
for (const wp of this.userWallpapers) {
if (wp.cssClass != this.wallpaper) {
newUserWallpapers.push(wp);
}
}
await wallpaperStorage.removeData(this.wallpaper);
this.userWallpapers = newUserWallpapers;
this.wallpaper = '';
}
}
keyHook(event) { keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') { if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close(); this.close();
@@ -515,6 +671,8 @@ class SettingsPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SettingsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -544,7 +702,7 @@ class SettingsPage extends Vue {
margin-bottom: 5px; margin-bottom: 5px;
} }
.label-1 { .label-1, .label-7 {
width: 75px; width: 75px;
} }
@@ -556,7 +714,7 @@ class SettingsPage extends Vue {
width: 100px; width: 100px;
} }
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 { .label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

View File

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

View File

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

View File

@@ -0,0 +1,124 @@
<!---------------------------------------------->
<div class="hidden part-header">Режим</div>
<div class="item row">
<div class="label-2"></div>
<div class="col row">
<q-checkbox v-model="dualPageMode" size="xs" label="Двухстраничный режим" />
</div>
</div>
<div class="part-header">Страницы</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 v-show="dualPageMode" class="item row">
<div class="label-2">Отступ внутри</div>
<div class="col row">
<NumInput class="col-left" v-model="dualIndentLR" :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 v-show="dualPageMode">
<div class="part-header">Разделитель</div>
<div class="item row no-wrap">
<div class="label-2">Цвет</div>
<div class="col-left row">
<q-input class="col-left no-mp"
outlined dense
v-model="dualDivColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
:disable="dualDivColorAsText"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('div')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="dualDivColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="q-px-xs"/>
<q-checkbox v-model="dualDivColorAsText" size="xs" label="Как у текста" />
</div>
<div class="item row">
<div class="label-2">Прозрачность</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
</div>
</div>
<div class="item row">
<div class="label-2">Ширина (px)</div>
<div class="col row">
<NumInput class="col-left" v-model="dualDivWidth" :min="0" :max="100">
<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="dualDivHeight" :min="0" :max="100">
<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="dualDivStrokeFill" :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="dualDivStrokeGap" :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="dualDivShadowWidth" :min="0" :max="100"/>
</div>
</div>
</div>

View File

@@ -0,0 +1,64 @@
<!---------------------------------------------->
<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 v-show="showStatusBar" class="q-ml-sm" v-model="statusBarTop" size="xs" label="Вверху/внизу" />
</div>
</div>
<div v-show="showStatusBar" class="item row no-wrap">
<div class="label-2">Цвет</div>
<div class="col-left row">
<q-input class="col-left no-mp"
outlined dense
v-model="statusBarColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
:disable="statusBarColorAsText"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('statusbar')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="statusBarColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="q-px-xs"/>
<q-checkbox v-model="statusBarColorAsText" size="xs" label="Как у текста"/>
</div>
<div v-show="showStatusBar" 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"/>
</div>
</div>
<div v-show="showStatusBar" class="item row">
<div class="label-2">Высота</div>
<div class="col row">
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100"/>
</div>
</div>
<div v-show="showStatusBar" 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

@@ -15,23 +15,6 @@
</div> </div>
</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="item row">
<div class="label-2">Сдвиг</div> <div class="label-2">Сдвиг</div>
<div class="col row"> <div class="col row">
@@ -123,7 +106,7 @@
<div class="item row"> <div class="item row">
<div class="label-2"></div> <div class="label-2"></div>
<div class="col row"> <div class="col row">
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" /> <q-checkbox v-model="imageFitWidth" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
</div> </div>
</div> </div>

View File

@@ -1,34 +0,0 @@
<q-tabs
v-model="selectedViewTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="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

@@ -1,58 +0,0 @@
<!---------------------------------------------->
<div class="hidden part-header">Цвет</div>
<div class="item row">
<div class="label-2">Текст</div>
<div class="col row">
<q-input class="col-left no-mp"
outlined dense
v-model="textColorFiltered"
:rules="['hexColor']"
style="max-width: 150px"
>
<template v-slot:prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="textColor"
no-header default-view="palette" :palette="predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<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

@@ -1,36 +0,0 @@
<!---------------------------------------------->
<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

@@ -2,11 +2,11 @@ import {sleep} from '../../../share/utils';
export default class DrawHelper { export default class DrawHelper {
fontBySize(size) { fontBySize(size) {
return `${size}px ${this.fontName}`; return `${size}px '${this.fontName}'`;
} }
fontByStyle(style) { fontByStyle(style) {
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px ${this.fontName}`; return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
} }
measureText(text, style) {// eslint-disable-line no-unused-vars measureText(text, style) {// eslint-disable-line no-unused-vars
@@ -19,6 +19,109 @@ export default class DrawHelper {
return this.context.measureText(text).width; return this.context.measureText(text).width;
} }
drawLine(line, lineIndex, baseLineIndex, sel, imageDrawn) {
/* line:
{
begin: Number,
end: Number,
first: Boolean,
last: Boolean,
parts: array of {
style: {bold: Boolean, italic: Boolean, center: Boolean},
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
text: String,
}
}*/
let out = '<div>';
let lineText = '';
let center = false;
let space = 0;
let j = 0;
//формируем строку
for (const part of line.parts) {
let tOpen = '';
tOpen += (part.style.bold ? '<b>' : '');
tOpen += (part.style.italic ? '<i>' : '');
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
let tClose = '';
tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : '');
tClose += (part.style.bold ? '</b>' : '');
let text = '';
if (lineIndex == 0 && this.searching) {
for (let k = 0; k < part.text.length; k++) {
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
j++;
}
} else
text = part.text;
if (text && text.trim() == '')
text = `<span style="white-space: pre">${text}</span>`;
lineText += `${tOpen}${text}${tClose}`;
center = center || part.style.center;
space = (part.style.space > space ? part.style.space : space);
//избражения
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
const img = part.image;
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > img.h) {
resize = `height: ${img.h}px`;
}
const left = (this.w - img.w)/2;
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (lineIndex - baseLineIndex - img.imageLine)*this.lineHeight;
if (img.local) {
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
} else {
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
}
}
imageDrawn.add(img.paraIndex);
}
if (img && img.id && img.inline) {
if (img.local) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > this.fontSize) {
resize = `height: ${this.fontSize - 3}px`;
}
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
}
} else {
//
}
}
}
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
if ((line.first || space) && !center) {
let p = (line.first ? this.p : 0);
p = (space ? p + this.p*space : p);
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
}
if (line.last || center)
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
out += lineText + '</div>';
return out;
}
drawPage(lines, isScrolling) { drawPage(lines, isScrolling) {
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength) if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
return ''; return '';
@@ -26,140 +129,78 @@ export default class DrawHelper {
const font = this.fontByStyle({}); const font = this.fontByStyle({});
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : ''); const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` + const boxH = this.h + (isScrolling ? this.lineHeight : 0);
let out = `<div class="row no-wrap" style="width: ${this.boxW}px; height: ${boxH}px;` +
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` + ` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
` line-height: ${this.lineHeight}px; white-space: nowrap;">`; ` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
let imageDrawn = new Set(); let imageDrawn1 = new Set();
let imageDrawn2 = new Set();
let len = lines.length; let len = lines.length;
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0); const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
len = (len > lineCount ? lineCount : len); len = (len > lineCount ? lineCount : len);
for (let i = 0; i < len; i++) { //поиск
const line = lines[i]; let sel = new Set();
/* line: if (len > 0 && this.searching) {
{ const line = lines[0];
begin: Number, let pureText = '';
end: Number, for (const part of line.parts) {
first: Boolean, pureText += part.text;
last: Boolean,
parts: array of {
style: {bold: Boolean, italic: Boolean, center: Boolean},
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
text: String,
}
}*/
let sel = new Set();
//поиск
if (i == 0 && this.searching) {
let pureText = '';
for (const part of line.parts) {
pureText += part.text;
}
pureText = pureText.toLowerCase();
let j = 0;
while (1) {// eslint-disable-line no-constant-condition
j = pureText.indexOf(this.needle, j);
if (j >= 0) {
for (let k = 0; k < this.needle.length; k++) {
sel.add(j + k);
}
} else
break;
j++;
}
} }
let lineText = ''; pureText = pureText.toLowerCase();
let center = false;
let space = 0;
let j = 0; let j = 0;
//формируем строку while (1) {// eslint-disable-line no-constant-condition
for (const part of line.parts) { j = pureText.indexOf(this.needle, j);
let tOpen = (part.style.bold ? '<b>' : ''); if (j >= 0) {
tOpen += (part.style.italic ? '<i>' : ''); for (let k = 0; k < this.needle.length; k++) {
let tClose = (part.style.italic ? '</i>' : ''); sel.add(j + k);
tClose += (part.style.bold ? '</b>' : '');
let text = '';
if (i == 0 && this.searching) {
for (let k = 0; k < part.text.length; k++) {
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
j++;
} }
} else } else
text = part.text; break;
j++;
if (text && text.trim() == '')
text = `<span style="white-space: pre">${text}</span>`;
lineText += `${tOpen}${text}${tClose}`;
center = center || part.style.center;
space = (part.style.space > space ? part.style.space : space);
//избражения
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
const img = part.image;
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > img.h) {
resize = `height: ${img.h}px`;
}
const left = (this.w - img.w)/2;
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (i - img.imageLine)*this.lineHeight;
if (img.local) {
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
} else {
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
}
}
imageDrawn.add(img.paraIndex);
}
if (img && img.id && img.inline) {
if (img.local) {
const bin = this.parsed.binary[img.id];
if (bin) {
let resize = '';
if (bin.h > this.fontSize) {
resize = `height: ${this.fontSize - 3}px`;
}
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
}
} else {
//
}
}
} }
}
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '') //отрисовка строк
if ((line.first || space) && !center) { if (!this.dualPageMode) {
let p = (line.first ? this.p : 0); out += `<div class="fit">`;
p = (space ? p + this.p*space : p); for (let i = 0; i < len; i++) {
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`; out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
} }
out += `</div>`;
} else {
//левая страница
out += `<div style="width: ${this.w}px; margin-left: ${this.dualIndentLR}px; position: relative;">`;
const l2 = (this.pageRowsCount > len ? len : this.pageRowsCount);
for (let i = 0; i < l2; i++) {
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
}
out += '</div>';
if (line.last || center) //разделитель
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`; out += `<div style="width: ${this.dualIndentLR*2}px;"></div>`;
out += (i > 0 ? '<br>' : '') + lineText; //правая страница
out += `<div style="width: ${this.w}px; margin-right: ${this.dualIndentLR}px; position: relative;">`;
for (let i = l2; i < len; i++) {
out += this.drawLine(lines[i], i, l2, sel, imageDrawn2);
}
out += '</div>';
} }
out += '</div>'; out += '</div>';
return out; return out;
} }
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength) { drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength, imageNum, imageLength) {
const pad = 3; const pad = 3;
const fh = h - 2*pad; const fh = h - 2*pad;
const fh2 = fh/2; const fh2 = fh/2;
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`; const tImg = (imageNum > 0 ? ` (${imageNum}/${imageLength})` : '');
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}${tImg}`;
const w1 = this.measureTextFont(t1, font) + fh2; const w1 = this.measureTextFont(t1, font) + fh2;
const read = (bookPos + 1)/textLength; const read = (bookPos + 1)/textLength;
const t2 = `${(read*100).toFixed(2)}%`; const t2 = `${(read*100).toFixed(2)}%`;
@@ -172,8 +213,8 @@ export default class DrawHelper {
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) { if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
const barWidth = w - w1 - w2 - fh2; const barWidth = w - w1 - w2 - fh2;
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarColor); out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarRgbaColor);
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarColor); out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarRgbaColor);
} }
if (w1 <= w) if (w1 <= w)
@@ -182,16 +223,16 @@ export default class DrawHelper {
return out; return out;
} }
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) { drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
let out = `<div class="layout" style="` + let out = `<div class="layout" style="` +
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` + `width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
`color: ${this.statusBarColor}">`; `color: ${this.statusBarRgbaColor}">`;
const fontSize = statusBarHeight*0.75; const fontSize = statusBarHeight*0.75;
const font = 'bold ' + this.fontBySize(fontSize); const font = 'bold ' + this.fontBySize(fontSize);
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarColor); out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarRgbaColor);
const date = new Date(); const date = new Date();
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
@@ -200,7 +241,7 @@ export default class DrawHelper {
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize); out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength); out += this.drawPercentBar(this.realWidth/2 + fontSize, 2, this.realWidth/2 - timeW - 3*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
out += '</div>'; out += '</div>';
return out; return out;
@@ -267,7 +308,7 @@ export default class DrawHelper {
} }
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) { async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
const s = this.w + this.fontSize; const s = this.boxW + this.fontSize;
if (isDown) { if (isDown) {
page1.style.transform = `translateX(${s}px)`; page1.style.transform = `translateX(${s}px)`;

View File

@@ -0,0 +1,93 @@
@keyframes page1-animation-thaw {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes page2-animation-thaw {
0% { opacity: 1; }
100% { opacity: 0; }
}
.paper1 {
background: url("images/paper1.jpg") center;
background-size: 100% 100%;
}
.paper2 {
background: url("images/paper2.jpg") center;
background-size: 100% 100%;
}
.paper3 {
background: url("images/paper3.jpg") center;
background-size: 100% 100%;
}
.paper4 {
background: url("images/paper4.jpg") center;
background-size: 100% 100%;
}
.paper5 {
background: url("images/paper5.jpg") center;
background-size: 100% 100%;
}
.paper6 {
background: url("images/paper6.jpg") center;
background-size: 100% 100%;
}
.paper7 {
background: url("images/paper7.jpg") center;
background-size: 100% 100%;
}
.paper8 {
background: url("images/paper8.jpg") center;
background-size: 100% 100%;
}
.paper9 {
background: url("images/paper9.jpg");
}
.paper10 {
background: url("images/paper10.png") center;
background-size: 100% 100%;
}
.paper11 {
background: url("images/paper11.png") center;
background-size: 100% 100%;
}
.paper12 {
background: url("images/paper12.png") center;
background-size: 100% 100%;
}
.paper13 {
background: url("images/paper13.png") center;
background-size: 100% 100%;
}
.paper14 {
background: url("images/paper14.png") center;
background-size: 100% 100%;
}
.paper15 {
background: url("images/paper15.png") center;
background-size: 100% 100%;
}
.paper16 {
background: url("images/paper16.png") center;
background-size: 100% 100%;
}
.paper17 {
background: url("images/paper17.png") center;
background-size: 100% 100%;
}

View File

@@ -1,8 +1,8 @@
<template> <template>
<div ref="main" class="main"> <div ref="main" class="main">
<div class="layout back" @wheel.prevent.stop="onMouseWheel"> <div class="layout back" @wheel.prevent.stop="onMouseWheel">
<div v-html="background"></div> <div class="absolute" v-html="background"></div>
<!-- img --> <div class="absolute" v-html="pageDivider"></div>
</div> </div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd"> <div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
@@ -17,17 +17,24 @@
<div v-show="showStatusBar" ref="statusBar" class="layout"> <div v-show="showStatusBar" ref="statusBar" class="layout">
<div v-html="statusBar"></div> <div v-html="statusBar"></div>
</div> </div>
<div v-show="clickControl" ref="layoutEvents" class="layout events" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp" <div v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@wheel.prevent.stop="onMouseWheel" @wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel" @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
oncontextmenu="return false;"> >
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop <div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div> @click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div> </div>
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop <div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
@click.prevent.stop="onStatusBarClick"> @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
>
</div> </div>
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты --> <!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas> <canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div> <div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
</div> </div>
@@ -35,12 +42,15 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import {loadCSS} from 'fg-loadcss'; import {loadCSS} from 'fg-loadcss';
import _ from 'lodash'; import _ from 'lodash';
import {sleep} from '../../../share/utils'; import './TextPage.css';
import * as utils from '../../../share/utils';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
import DrawHelper from './DrawHelper'; import DrawHelper from './DrawHelper';
import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
@@ -48,7 +58,7 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100; const minLayoutWidth = 100;
export default @Component({ const componentOptions = {
watch: { watch: {
bookPos: function() { bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -67,13 +77,16 @@ export default @Component({
this.updateLayout(); this.updateLayout();
}, },
}, },
}) };
class TextPage extends Vue { class TextPage {
_options = componentOptions;
toggleLayout = false; toggleLayout = false;
showStatusBar = false; showStatusBar = false;
clickControl = true; clickControl = true;
background = null; background = null;
pageDivider = null;
page1 = null; page1 = null;
page2 = null; page2 = null;
statusBar = null; statusBar = null;
@@ -112,6 +125,10 @@ class TextPage extends Vue {
this.drawStatusBar(); this.drawStatusBar();
}, 60); }, 60);
this.debouncedDrawPageDividerAndOrnament = _.throttle(() => {
this.drawPageDividerAndOrnament();
}, 65);
this.debouncedLoadSettings = _.debounce(() => { this.debouncedLoadSettings = _.debounce(() => {
this.loadSettings(); this.loadSettings();
}, 50); }, 50);
@@ -132,9 +149,9 @@ class TextPage extends Vue {
await this.doPageAnimation(); await this.doPageAnimation();
}, 10); }, 10);
this.$root.$on('resize', async() => { this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
await sleep(500); await utils.sleep(500);
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
}); });
} }
@@ -152,7 +169,7 @@ class TextPage extends Vue {
const wideLetter = 'Щ'; const wideLetter = 'Щ';
//preloaded fonts //preloaded fonts
this.fontList = [`12px ${this.fontName}`]; this.fontList = [`12px '${this.fontName}'`];
//widths //widths
this.realWidth = this.$refs.main.clientWidth; this.realWidth = this.$refs.main.clientWidth;
@@ -161,14 +178,16 @@ class TextPage extends Vue {
this.$refs.layoutEvents.style.width = this.realWidth + 'px'; this.$refs.layoutEvents.style.width = this.realWidth + 'px';
this.$refs.layoutEvents.style.height = this.realHeight + 'px'; this.$refs.layoutEvents.style.height = this.realHeight + 'px';
this.w = this.realWidth - 2*this.indentLR; const dual = (this.dualPageMode ? 2 : 1);
this.boxW = this.realWidth - 2*this.indentLR;
this.w = this.boxW/dual - (this.dualPageMode ? 2*this.dualIndentLR : 0);
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0); this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
this.h = this.scrollHeight - 2*this.indentTB; this.h = this.scrollHeight - 2*this.indentTB;
this.lineHeight = this.fontSize + this.lineInterval;
this.pageLineCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
this.$refs.scrollingPage1.style.width = this.w + 'px'; this.lineHeight = this.fontSize + this.lineInterval;
this.$refs.scrollingPage2.style.width = this.w + 'px'; this.pageRowsCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
this.pageLineCount = (this.dualPageMode ? this.pageRowsCount*2 : this.pageRowsCount)
//stuff //stuff
this.currentAnimation = ''; this.currentAnimation = '';
@@ -180,7 +199,10 @@ class TextPage extends Vue {
this.$refs.statusBar.style.left = '0px'; this.$refs.statusBar.style.left = '0px';
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px'; this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha); const sbColor = (this.statusBarColorAsText ? this.textColor : this.statusBarColor);
this.statusBarRgbaColor = this.hex2rgba(sbColor || '#000000', this.statusBarColorAlpha);
const ddColor = (this.dualDivColorAsText ? this.textColor : this.dualDivColor);
this.dualDivRgbaColor = this.hex2rgba(ddColor || '#000000', this.dualDivColorAlpha);
//drawHelper //drawHelper
this.drawHelper.realWidth = this.realWidth; this.drawHelper.realWidth = this.realWidth;
@@ -188,10 +210,20 @@ class TextPage extends Vue {
this.drawHelper.lastBook = this.lastBook; this.drawHelper.lastBook = this.lastBook;
this.drawHelper.book = this.book; this.drawHelper.book = this.book;
this.drawHelper.parsed = this.parsed; this.drawHelper.parsed = this.parsed;
this.drawHelper.pageRowsCount = this.pageRowsCount;
this.drawHelper.pageLineCount = this.pageLineCount; this.drawHelper.pageLineCount = this.pageLineCount;
this.drawHelper.dualPageMode = this.dualPageMode;
this.drawHelper.dualIndentLR = this.dualIndentLR;
/*this.drawHelper.dualDivWidth = this.dualDivWidth;
this.drawHelper.dualDivHeight = this.dualDivHeight;
this.drawHelper.dualDivRgbaColor = this.dualDivRgbaColor;
this.drawHelper.dualDivStrokeFill = this.dualDivStrokeFill;
this.drawHelper.dualDivStrokeGap = this.dualDivStrokeGap;
this.drawHelper.dualDivShadowWidth = this.dualDivShadowWidth;*/
this.drawHelper.backgroundColor = this.backgroundColor; this.drawHelper.backgroundColor = this.backgroundColor;
this.drawHelper.statusBarColor = this.statusBarColor; this.drawHelper.statusBarRgbaColor = this.statusBarRgbaColor;
this.drawHelper.fontStyle = this.fontStyle; this.drawHelper.fontStyle = this.fontStyle;
this.drawHelper.fontWeight = this.fontWeight; this.drawHelper.fontWeight = this.fontWeight;
this.drawHelper.fontSize = this.fontSize; this.drawHelper.fontSize = this.fontSize;
@@ -200,6 +232,7 @@ class TextPage extends Vue {
this.drawHelper.textColor = this.textColor; this.drawHelper.textColor = this.textColor;
this.drawHelper.textShift = this.textShift; this.drawHelper.textShift = this.textShift;
this.drawHelper.p = this.p; this.drawHelper.p = this.p;
this.drawHelper.boxW = this.boxW;
this.drawHelper.w = this.w; this.drawHelper.w = this.w;
this.drawHelper.h = this.h; this.drawHelper.h = this.h;
this.drawHelper.indentLR = this.indentLR; this.drawHelper.indentLR = this.indentLR;
@@ -228,32 +261,33 @@ class TextPage extends Vue {
//parsed //parsed
if (this.parsed) { if (this.parsed) {
this.parsed.p = this.p; let wideLine = wideLetter;
this.parsed.w = this.w;// px, ширина текста if (!this.drawHelper.measureText(wideLine, {}))
this.parsed.font = this.font;
this.parsed.fontSize = this.fontSize;
this.parsed.wordWrap = this.wordWrap;
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
let t = wideLetter;
if (!this.drawHelper.measureText(t, {}))
throw new Error('Ошибка measureText'); throw new Error('Ошибка measureText');
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter; while (this.drawHelper.measureText(wideLine, {}) < this.w) wideLine += wideLetter;
this.parsed.maxWordLength = t.length - 1;
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
this.parsed.lineHeight = this.lineHeight;
this.parsed.showImages = this.showImages;
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
this.parsed.imageHeightLines = this.imageHeightLines;
this.parsed.imageFitWidth = this.imageFitWidth;
this.parsed.compactTextPerc = this.compactTextPerc;
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.'; this.parsed.setSettings({
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {}); p: this.p,
w: this.w,
font: this.font,
fontSize: this.fontSize,
wordWrap: this.wordWrap,
cutEmptyParagraphs: this.cutEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs,
maxWordLength: wideLine.length - 1,
lineHeight: this.lineHeight,
showImages: this.showImages,
showInlineImagesInCenter: this.showInlineImagesInCenter,
imageHeightLines: this.imageHeightLines,
imageFitWidth: this.imageFitWidth,
compactTextPerc: this.compactTextPerc,
testWidth: 0,
measureText: this.drawHelper.measureText.bind(this.drawHelper),
});
} }
//scrolling page //scrolling page
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight; const pageSpace = this.scrollHeight - this.pageRowsCount*this.lineHeight;
let top = pageSpace/2; let top = pageSpace/2;
if (this.showStatusBar) if (this.showStatusBar)
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0); top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
@@ -262,14 +296,14 @@ class TextPage extends Vue {
page1.perspective = page2.perspective = '3072px'; page1.perspective = page2.perspective = '3072px';
page1.width = page2.width = this.w + this.indentLR + 'px'; page1.width = page2.width = this.boxW + this.indentLR + 'px';
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px'; page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
page1.top = page2.top = top + 'px'; page1.top = page2.top = top + 'px';
page1.left = page2.left = this.indentLR + 'px'; page1.left = page2.left = this.indentLR + 'px';
page1 = this.$refs.scrollingPage1.style; page1 = this.$refs.scrollingPage1.style;
page2 = this.$refs.scrollingPage2.style; page2 = this.$refs.scrollingPage2.style;
page1.width = page2.width = this.w + this.indentLR + 'px'; page1.width = page2.width = this.boxW + this.indentLR + 'px';
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px'; page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
} }
@@ -285,7 +319,7 @@ class TextPage extends Vue {
let close = null; let close = null;
(async() => { (async() => {
await sleep(500); await utils.sleep(500);
if (this.fontsLoading) if (this.fontsLoading)
close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>'); close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
})(); })();
@@ -333,20 +367,36 @@ class TextPage extends Vue {
if (!omitLoadFonts) if (!omitLoadFonts)
await this.loadFonts(); await this.loadFonts();
this.draw(); if (omitLoadFonts) {
this.draw();
} else {
// ширина шрифта некоторое время выдается неверно,
// не удалось событийно отловить этот момент, поэтому костыль
while (this.checkingFont) {
this.stopCheckingFont = true;
await utils.sleep(100);
}
// ширина шрифта некоторое время выдается неверно, поэтому this.checkingFont = true;
if (!omitLoadFonts) { this.stopCheckingFont = false;
const parsed = this.parsed; try {
const parsed = this.parsed;
let i = 0; let i = 0;
const t = this.parsed.testText; const t = 'Это тестовый текст. Его ширина выдается системой неправильно некоторое время.';
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth) let twprev = 0;
await sleep(100); //5 секунд проверяем изменения шрифта
while (!this.stopCheckingFont && i++ < 50 && this.parsed === parsed) {
if (this.parsed === parsed) { const tw = this.drawHelper.measureText(t, {});
this.parsed.testWidth = this.drawHelper.measureText(t, {}); if (tw !== twprev) {
this.draw(); this.parsed.setSettings({testWidth: tw});
this.draw();
twprev = tw;
}
await utils.sleep(100);
}
} finally {
this.checkingFont = false;
} }
} }
} }
@@ -366,7 +416,6 @@ class TextPage extends Vue {
this.updateLayout(); this.updateLayout();
this.book = null; this.book = null;
this.meta = null; this.meta = null;
this.fb2 = null;
this.parsed = null; this.parsed = null;
this.linesUp = null; this.linesUp = null;
@@ -383,7 +432,7 @@ class TextPage extends Vue {
try { try {
//подождем ленивый парсинг //подождем ленивый парсинг
this.stopLazyParse = true; this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10); while (this.doingLazyParse) await utils.sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook); const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) { if (!isParsed) {
@@ -392,23 +441,11 @@ class TextPage extends Vue {
this.book = await bookManager.getBook(this.lastBook); this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book); this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2; const bt = utils.getBookTitle(this.meta.fb2);
let authorNames = []; this.title = bt.fullTitle;
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([ this.$root.setAppTitle(this.title);
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title);
this.parsed = this.book.parsed; this.parsed = this.book.parsed;
@@ -443,8 +480,18 @@ class TextPage extends Vue {
} }
setBackground() { setBackground() {
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` + if (this.wallpaperIgnoreStatusBar) {
` background-color: ${this.backgroundColor}"></div>`; this.background = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` background-color: ${this.backgroundColor}">` +
`<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
`</div>` +
`</div>`;
} else {
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` background-color: ${this.backgroundColor}"></div>`;
}
} }
async onResize() { async onResize() {
@@ -462,7 +509,7 @@ class TextPage extends Vue {
} }
get font() { get font() {
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontName}`; return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
} }
onPage1TransitionEnd() { onPage1TransitionEnd() {
@@ -493,7 +540,7 @@ class TextPage extends Vue {
let wait = (timeout + 201)/100; let wait = (timeout + 201)/100;
while (wait > 0 && !this[stopPropertyName]) { while (wait > 0 && !this[stopPropertyName]) {
wait--; wait--;
await sleep(100); await utils.sleep(100);
} }
resolve(); resolve();
})().catch(reject); }); })().catch(reject); });
@@ -503,13 +550,13 @@ class TextPage extends Vue {
async startTextScrolling() { async startTextScrolling() {
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 || if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
this.linesDown.length <= this.pageLineCount) { this.linesDown.length <= this.pageLineCount || this.dualPageMode) {
this.doStopScrolling(); this.doStopScrolling();
return; return;
} }
//ждем анимацию //ждем анимацию
while (this.inAnimation) await sleep(10); while (this.inAnimation) await utils.sleep(10);
this.stopScrolling = false; this.stopScrolling = false;
this.doingScrolling = true; this.doingScrolling = true;
@@ -520,7 +567,7 @@ class TextPage extends Vue {
this.page1 = this.page2; this.page1 = this.page2;
this.toggleLayout = true; this.toggleLayout = true;
await this.$nextTick(); await this.$nextTick();
await sleep(50); await utils.sleep(50);
this.cachedPos = -1; this.cachedPos = -1;
this.draw(); this.draw();
@@ -557,7 +604,7 @@ class TextPage extends Vue {
page.style.transform = 'none'; page.style.transform = 'none';
page.offsetHeight; page.offsetHeight;
while (this.doingScrolling) await sleep(10); while (this.doingScrolling) await utils.sleep(10);
} }
draw() { draw() {
@@ -621,6 +668,7 @@ class TextPage extends Vue {
if (!this.pageChangeAnimation) if (!this.pageChangeAnimation)
this.debouncedPrepareNextPage(); this.debouncedPrepareNextPage();
this.debouncedDrawStatusBar(); this.debouncedDrawStatusBar();
this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) { if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true); this.doEnd(true);
@@ -735,8 +783,24 @@ class TextPage extends Vue {
message = this.statusBarMessage; message = this.statusBarMessage;
if (!message) if (!message)
message = this.title; message = this.title;
//check image num
let imageNum = 0;
const len = (lines.length > 2 ? 2 : lines.length);
loop:
for (let j = 0; j < len; j++) {
const line = lines[j];
for (const part of line.parts) {
if (part.image) {
imageNum = part.image.num;
break loop;
}
}
}
//drawing
this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight, this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
lines[i].end, this.parsed.textLength, message); lines[i].end, this.parsed.textLength, message, imageNum, this.parsed.images.length);
this.bookPosSeen = lines[i].end; this.bookPosSeen = lines[i].end;
} }
} else { } else {
@@ -744,6 +808,25 @@ class TextPage extends Vue {
} }
} }
drawPageDividerAndOrnament() {
if (this.dualPageMode) {
this.pageDivider = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
`<div class="fit row justify-center items-center no-wrap">` +
`<div style="height: ${Math.round(this.scrollHeight*this.dualDivHeight/100)}px; width: ${this.dualDivWidth}px; ` +
`box-shadow: 0 0 ${this.dualDivShadowWidth}px ${this.dualDivRgbaColor}; ` +
`background-image: url(&quot;data:image/svg+xml;utf8,<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'>` +
`<line x1='${this.dualDivWidth/2}' y1='0' x2='${this.dualDivWidth/2}' y2='100%' stroke='${this.dualDivRgbaColor}' ` +
`stroke-width='${this.dualDivWidth}' stroke-dasharray='${this.dualDivStrokeFill} ${this.dualDivStrokeGap}'/>` +
`</svg>&quot;);">` +
`</div>` +
`</div>` +
`</div>`;
} else {
this.pageDivider = null;
}
}
blinkCachedLoadMessage(state) { blinkCachedLoadMessage(state) {
if (state === 'finish') { if (state === 'finish') {
this.statusBarMessage = ''; this.statusBarMessage = '';
@@ -766,7 +849,7 @@ class TextPage extends Vue {
for (let i = 0; i < this.parsed.para.length; i++) { for (let i = 0; i < this.parsed.para.length; i++) {
j++; j++;
if (j > 1) { if (j > 1) {
await sleep(1); await utils.sleep(1);
j = 0; j = 0;
} }
if (this.stopLazyParse) if (this.stopLazyParse)
@@ -788,7 +871,7 @@ class TextPage extends Vue {
async refreshTime() { async refreshTime() {
if (!this.timeRefreshing) { if (!this.timeRefreshing) {
this.timeRefreshing = true; this.timeRefreshing = true;
await sleep(60*1000); await utils.sleep(60*1000);
if (this.book && this.parsed.textLength) { if (this.book && this.parsed.textLength) {
this.debouncedDrawStatusBar(); this.debouncedDrawStatusBar();
@@ -905,7 +988,7 @@ class TextPage extends Vue {
this.settingsChanging = true; this.settingsChanging = true;
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100); const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
this.commit('reader/setSettings', {fontSize: newSize}); this.commit('reader/setSettings', {fontSize: newSize});
await sleep(50); await utils.sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -915,7 +998,7 @@ class TextPage extends Vue {
this.settingsChanging = true; this.settingsChanging = true;
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5); const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
this.commit('reader/setSettings', {fontSize: newSize}); this.commit('reader/setSettings', {fontSize: newSize});
await sleep(50); await utils.sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -925,7 +1008,7 @@ class TextPage extends Vue {
this.settingsChanging = true; this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1); const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
this.commit('reader/setSettings', {scrollingDelay: newDelay}); this.commit('reader/setSettings', {scrollingDelay: newDelay});
await sleep(50); await utils.sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -935,7 +1018,7 @@ class TextPage extends Vue {
this.settingsChanging = true; this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000); const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
this.commit('reader/setSettings', {scrollingDelay: newDelay}); this.commit('reader/setSettings', {scrollingDelay: newDelay});
await sleep(50); await utils.sleep(50);
this.settingsChanging = false; this.settingsChanging = false;
} }
} }
@@ -949,7 +1032,7 @@ class TextPage extends Vue {
let delay = 400; let delay = 400;
while (this.repDoing) { while (this.repDoing) {
this.handleClick(pointX, pointY); this.handleClick(pointX, pointY);
await sleep(delay); await utils.sleep(delay);
if (delay > 15) if (delay > 15)
delay *= 0.8; delay *= 0.8;
} }
@@ -962,7 +1045,7 @@ class TextPage extends Vue {
} }
onTouchStart(event) { onTouchStart(event) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
@@ -990,7 +1073,7 @@ class TextPage extends Vue {
} }
onTouchEnd(event) { onTouchEnd(event) {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
@@ -1026,13 +1109,13 @@ class TextPage extends Vue {
} }
onTouchCancel() { onTouchCancel() {
if (!this.$isMobileDevice) if (!this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
} }
onMouseDown(event) { onMouseDown(event) {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
if (event.button == 0) { if (event.button == 0) {
@@ -1049,13 +1132,13 @@ class TextPage extends Vue {
} }
onMouseUp() { onMouseUp() {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
this.endClickRepeat(); this.endClickRepeat();
} }
onMouseWheel(event) { onMouseWheel(event) {
if (this.$isMobileDevice) if (this.$root.isMobileDevice)
return; return;
if (event.deltaY > 0) { if (event.deltaY > 0) {
this.doDown(); this.doDown();
@@ -1066,7 +1149,7 @@ class TextPage extends Vue {
onStatusBarClick() { onStatusBarClick() {
const url = this.meta.url; const url = this.meta.url;
if (url && url.indexOf('file://') != 0) { if (url && url.indexOf('disk://') != 0) {
window.open(url, '_blank'); window.open(url, '_blank');
} else { } else {
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'}); this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
@@ -1121,6 +1204,8 @@ class TextPage extends Vue {
} }
} }
export default vueComponent(TextPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
@@ -1158,60 +1243,3 @@ class TextPage extends Vue {
} }
</style> </style>
<style>
.paper1 {
background: url("images/paper1.jpg") center;
background-size: cover;
}
.paper2 {
background: url("images/paper2.jpg") center;
background-size: cover;
}
.paper3 {
background: url("images/paper3.jpg") center;
background-size: cover;
}
.paper4 {
background: url("images/paper4.jpg") center;
background-size: cover;
}
.paper5 {
background: url("images/paper5.jpg") center;
background-size: cover;
}
.paper6 {
background: url("images/paper6.jpg") center;
background-size: cover;
}
.paper7 {
background: url("images/paper7.jpg") center;
background-size: cover;
}
.paper8 {
background: url("images/paper8.jpg") center;
background-size: cover;
}
.paper9 {
background: url("images/paper9.jpg");
}
@keyframes page1-animation-thaw {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes page2-animation-thaw {
0% { opacity: 1; }
100% { opacity: 0; }
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,24 +1,56 @@
import he from 'he'; import he from 'he';
import sax from '../../../../server/core/sax'; import sax from '../../../../server/core/sax';
import {sleep} from '../../../share/utils'; import * as utils from '../../../share/utils';
const maxImageLineCount = 100; const maxImageLineCount = 100;
// defaults
const defaultSettings = {
p: 30, //px, отступ параграфа
w: 500, //px, ширина страницы
font: '', //css описание шрифта
fontSize: 20, //px, размер шрифта
wordWrap: false, //перенос по слогам
cutEmptyParagraphs: false, //убирать пустые параграфы
addEmptyParagraphs: 0, //добавлять n пустых параграфов перед непустым
maxWordLength: 500, //px, максимальная длина слова без пробелов
lineHeight: 26, //px, высота строки
showImages: true, //показыввать изображения
showInlineImagesInCenter: true, //выносить изображения в центр, работает на этапе первичного парсинга (parse)
imageHeightLines: 100, //кол-во строк, максимальная высота изображения
imageFitWidth: true, //ширина изображения не более ширины страницы
dualPageMode: false, //двухстраничный режим
compactTextPerc: 0, //проценты, степень компактности текста
testWidth: 0, //ширина тестовой строки, пересчитывается извне при изменении шрифта браузером
isTesting: false, //тестовый режим
//заглушка, измеритель ширины текста
measureText: (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
},
};
//for splitToSlogi()
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
const soglas = new Set([
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
]);
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
const alpha = new Set([...glas, ...soglas, ...znak]);
export default class BookParser { export default class BookParser {
constructor(settings) { constructor(settings = {}) {
if (settings) { this.sets = {};
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
}
// defaults this.setSettings(defaultSettings);
this.p = 30;// px, отступ параграфа this.setSettings(settings);
this.w = 300;// px, ширина страницы }
this.wordWrap = false;// перенос по слогам
//заглушка setSettings(settings = {}) {
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars this.sets = Object.assign({}, this.sets, settings);
return text.length*20; this.measureText = this.sets.measureText;
};
} }
async parse(data, callback) { async parse(data, callback) {
@@ -54,12 +86,14 @@ export default class BookParser {
//оглавление //оглавление
this.contents = []; this.contents = [];
this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []}; let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''}; let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false; let inTitle = false;
let inSubtitle = false; let inSubtitle = false;
let sectionLevel = 0; let sectionLevel = 0;
let bodyIndex = 0; let bodyIndex = 0;
let imageNum = 0;
let paraIndex = -1; let paraIndex = -1;
let paraOffset = 0; let paraOffset = 0;
@@ -74,6 +108,7 @@ export default class BookParser {
*/ */
const getImageDimensions = (binaryId, binaryType, data) => { const getImageDimensions = (binaryId, binaryType, data) => {
return new Promise ((resolve, reject) => { (async() => { return new Promise ((resolve, reject) => { (async() => {
data = data.replace(/[\n\r\s]/g, '');
const i = new Image(); const i = new Image();
let resolved = false; let resolved = false;
i.onload = () => { i.onload = () => {
@@ -90,7 +125,7 @@ export default class BookParser {
i.onerror = reject; i.onerror = reject;
i.src = `data:${binaryType};base64,${data}`; i.src = `data:${binaryType};base64,${data}`;
await sleep(30*1000); await utils.sleep(30*1000);
if (!resolved) if (!resolved)
reject('Не удалось получить размер изображения'); reject('Не удалось получить размер изображения');
})().catch(reject); }); })().catch(reject); });
@@ -112,20 +147,73 @@ export default class BookParser {
i.onerror = reject; i.onerror = reject;
i.src = src; i.src = src;
await sleep(30*1000); await utils.sleep(30*1000);
if (!resolved) if (!resolved)
reject('Не удалось получить размер изображения'); reject('Не удалось получить размер изображения');
})().catch(reject); }); })().catch(reject); });
}; };
const newParagraph = (text, len, addIndex) => { const correctCurrentPara = () => {
//коррекция текущего параграфа
if (paraIndex >= 0) {
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
let newParaText = p.text.trim();
newParaText = (newParaText.length ? newParaText : ' ');
const ldiff = p.text.length - newParaText.length;
if (ldiff != 0) {
p.text = newParaText;
p.length -= ldiff;
}
//удаление параграфов, которые содержат только разметку, такого не должно быть
if (!p.length) {
delete para[paraIndex];
paraIndex--;
return;
}
//добавление пустых (не)видимых (addEmptyParagraphs) параграфов перед текущим непустым
if (p.text.trim() != '') {
for (let i = 0; i < 2; i++) {
para[paraIndex] = {
index: paraIndex,
offset: paraOffset,
length: 1,
text: ' ',
addIndex: i + 1,
};
paraIndex++;
paraOffset++;
}
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
}
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
paraOffset += p.length;
}
};
const newParagraph = (text = '', len = 0) => {
correctCurrentPara();
//новый параграф
paraIndex++; paraIndex++;
let p = { let p = {
index: paraIndex, index: paraIndex,
offset: paraOffset, offset: paraOffset,
length: len, length: len,//длина текста внутри параграфа без учета длины разметки
text: text, text: text,
addIndex: (addIndex ? addIndex : 0), addIndex: 0,
}; };
if (inSubtitle) { if (inSubtitle) {
@@ -135,53 +223,26 @@ export default class BookParser {
} }
para[paraIndex] = p; para[paraIndex] = p;
paraOffset += p.length; paraOffset += len;
}; };
const growParagraph = (text, len) => { const growParagraph = (text, len) => {
if (paraIndex < 0) { if (paraIndex < 0) {
newParagraph(' ', 1); newParagraph();
growParagraph(text, len); growParagraph(text, len);
return; return;
} }
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
paraIndex--;
for (let i = 0; i < 2; i++) {
newParagraph(' ', 1, i + 1);
}
paraIndex++;
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
//уберем начальный пробел
p.length = 0;
p.text = p.text.substr(1);
}
p.length += len;
p.text += text;
if (inSubtitle) { if (inSubtitle) {
curSubtitle.title += text; curSubtitle.title += text;
} else if (inTitle) { } else if (inTitle) {
curTitle.title += text; curTitle.title += text;
} }
para[paraIndex] = p; const p = para[paraIndex];
paraOffset += p.length; p.length += len;
p.text += text;
paraOffset += len;
}; };
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
@@ -194,7 +255,8 @@ export default class BookParser {
if (tag == 'binary') { if (tag == 'binary') {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : ''); binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream') binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
binaryId = (attrs.id.value ? attrs.id.value : ''); binaryId = (attrs.id.value ? attrs.id.value : '');
} }
@@ -202,36 +264,88 @@ export default class BookParser {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
const href = attrs.href.value; const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
if (href[0] == '#') {//local if (href[0] == '#') {//local
if (inPara && !this.showInlineImagesInCenter && !center) imageNum++;
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
if (inPara && !this.sets.showInlineImagesInCenter && !center)
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
else else
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount); newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
if (inPara && this.showInlineImagesInCenter)
newParagraph(' ', 1); this.images.push({paraIndex, num: imageNum, id, local, alt});
if (inPara && this.sets.showInlineImagesInCenter)
newParagraph();
} else {//external } else {//external
dimPromises.push(getExternalImageDimensions(href)); imageNum++;
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
if (!this.sets.isTesting) {
dimPromises.push(getExternalImageDimensions(href));
} else {
dimPromises.push(this.sets.getExternalImageDimensions(this, href));
}
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
this.images.push({paraIndex, num: imageNum, id, local, alt});
} }
} }
} }
if (tag == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) { if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author) if (!fb2.author)
fb2.author = []; fb2.author = [];
fb2.author.push({}); fb2.author.push({});
} }
const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
if (!fb2.sequence)
fb2.sequence = [];
if (!isPublishSequence || !fb2.sequence.length) {
const attrs = sax.getAttrsSync(tail);
const seq = {};
if (attrs.name && attrs.name.value) {
seq.name = attrs.name.value;
}
if (attrs.number && attrs.number.value) {
seq.number = attrs.number.value;
}
fb2.sequence.push(seq);
}
}
if (path.indexOf('/fictionbook/body') == 0) { if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') { if (tag == 'body') {
if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => {
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
});
if (ann.length)
newParagraph();
}
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
const bt = utils.getBookTitle(fb2);
if (bt.sequence) {
newParagraph(bt.sequence, bt.sequence.length);
newParagraph();
}
}
if (!isFirstBody) if (!isFirstBody)
newParagraph(' ', 1); newParagraph();
isFirstBody = false; isFirstBody = false;
bodyIndex++; bodyIndex++;
} }
if (tag == 'title') { if (tag == 'title') {
newParagraph(' ', 1); newParagraph();
isFirstTitlePara = true; isFirstTitlePara = true;
bold = true; bold = true;
center = true; center = true;
@@ -243,18 +357,18 @@ export default class BookParser {
if (tag == 'section') { if (tag == 'section') {
if (!isFirstSection) if (!isFirstSection)
newParagraph(' ', 1); newParagraph();
isFirstSection = false; isFirstSection = false;
sectionLevel++; sectionLevel++;
} }
if (tag == 'emphasis' || tag == 'strong') { if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`<${tag}>`, 0); growParagraph(`<${tag}>`, 0);
} }
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) { if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
if (!(tag == 'p' && isFirstTitlePara)) if (!(tag == 'p' && isFirstTitlePara))
newParagraph(' ', 1); newParagraph();
if (tag == 'p') { if (tag == 'p') {
inPara = true; inPara = true;
isFirstTitlePara = false; isFirstTitlePara = false;
@@ -262,11 +376,16 @@ export default class BookParser {
} }
if (tag == 'subtitle') { if (tag == 'subtitle') {
newParagraph(' ', 1); newParagraph();
isFirstTitlePara = true; isFirstTitlePara = true;
bold = true; bold = true;
center = true; center = true;
if (curTitle.paraIndex < 0) {
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
this.contents.push(curTitle);
}
inSubtitle = true; inSubtitle = true;
curSubtitle = {paraIndex, inset: sectionLevel, title: ''}; curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
curTitle.subtitles.push(curSubtitle); curTitle.subtitles.push(curSubtitle);
@@ -278,11 +397,12 @@ export default class BookParser {
} }
if (tag == 'poem') { if (tag == 'poem') {
newParagraph(' ', 1); newParagraph();
} }
if (tag == 'text-author') { if (tag == 'text-author') {
newParagraph(' ', 1); newParagraph();
bold = true;
space += 1; space += 1;
} }
} }
@@ -306,7 +426,7 @@ export default class BookParser {
sectionLevel--; sectionLevel--;
} }
if (tag == 'emphasis' || tag == 'strong') { if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0); growParagraph(`</${tag}>`, 0);
} }
@@ -324,15 +444,15 @@ export default class BookParser {
if (tag == 'epigraph' || tag == 'annotation') { if (tag == 'epigraph' || tag == 'annotation') {
italic = false; italic = false;
space -= 1; space -= 1;
if (tag == 'annotation') newParagraph();
newParagraph(' ', 1);
} }
if (tag == 'stanza') { if (tag == 'stanza') {
newParagraph(' ', 1); newParagraph();
} }
if (tag == 'text-author') { if (tag == 'text-author') {
bold = false;
space -= 1; space -= 1;
} }
} }
@@ -349,17 +469,14 @@ export default class BookParser {
const onTextNode = (text) => {// eslint-disable-line no-unused-vars const onTextNode = (text) => {// eslint-disable-line no-unused-vars
text = he.decode(text); text = he.decode(text);
text = text.replace(/>/g, '&gt;'); text = text.replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/[\t\n\r\xa0]/g, ' ');
text = text.replace(/</g, '&lt;');
if (text && text.trim() == '') if (text && text.trim() == '')
text = (text.indexOf(' ') >= 0 ? ' ' : ''); text = ' ';
if (!text) if (!text)
return; return;
text = text.replace(/[\t\n\r\xa0]/g, ' ');
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0); const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
switch (path) { switch (path) {
case '/fictionbook/description/title-info/author/first-name': case '/fictionbook/description/title-info/author/first-name':
@@ -397,35 +514,43 @@ export default class BookParser {
fb2.annotation += text; fb2.annotation += text;
} }
let tOpen = (center ? '<center>' : ''); if (binaryId) {
tOpen += (bold ? '<strong>' : ''); if (!this.sets.isTesting) {
tOpen += (italic ? '<emphasis>' : ''); dimPromises.push(getImageDimensions(binaryId, binaryType, text));
tOpen += (space ? `<space w="${space}">` : ''); } else {
let tClose = (space ? '</space>' : ''); dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
tClose += (italic ? '</emphasis>' : ''); }
tClose += (bold ? '</strong>' : ''); }
tClose += (center ? '</center>' : '');
if (path.indexOf('/fictionbook/body/title') == 0 || if (path.indexOf('/fictionbook/body/title') == 0 ||
path.indexOf('/fictionbook/body/section') == 0 || path.indexOf('/fictionbook/body/section') == 0 ||
path.indexOf('/fictionbook/body/epigraph') == 0 path.indexOf('/fictionbook/body/epigraph') == 0
) { ) {
growParagraph(`${tOpen}${text}${tClose}`, text.length); let tOpen = (center ? '<center>' : '');
} tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
tOpen += (space ? `<space w="${space}">` : '');
let tClose = (space ? '</space>' : '');
tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
tClose += (center ? '</center>' : '');
if (binaryId) { if (text != ' ')
dimPromises.push(getImageDimensions(binaryId, binaryType, text)); growParagraph(`${tOpen}${text}${tClose}`, text.length);
else
growParagraph(' ', 1);
} }
}; };
const onProgress = async(prog) => { const onProgress = async(prog) => {
await sleep(1); await utils.sleep(1);
callback(prog); callback(prog);
}; };
await sax.parse(data, { await sax.parse(data, {
onStartNode, onEndNode, onTextNode, onProgress onStartNode, onEndNode, onTextNode, onProgress
}); });
correctCurrentPara();
if (dimPromises.length) { if (dimPromises.length) {
try { try {
@@ -441,11 +566,20 @@ export default class BookParser {
this.textLength = paraOffset; this.textLength = paraOffset;
callback(100); callback(100);
await sleep(10); await utils.sleep(10);
return {fb2}; return {fb2};
} }
imageHrefToId(id) {
let local = false;
if (id[0] == '#') {
id = id.substr(1);
local = true;
}
return {id, local};
}
findParaIndex(bookPos) { findParaIndex(bookPos) {
let result = undefined; let result = undefined;
//дихотомия //дихотомия
@@ -470,16 +604,26 @@ export default class BookParser {
splitToStyle(s) { splitToStyle(s) {
let result = [];/*array of { let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number}, style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
image: {local: Boolean, inline: Boolean, id: String}, image: {local: Boolean, inline: Boolean, id: String},
text: String, text: String,
}*/ }*/
let style = {}; let style = {};
let image = {}; let image = {};
//оптимизация по памяти
const copyStyle = (s) => {
const r = {};
for (const prop in s) {
if (s[prop])
r[prop] = s[prop];
}
return r;
};
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
result.push({ result.push({
style: Object.assign({}, style), style: copyStyle(style),
image, image,
text text
}); });
@@ -493,6 +637,12 @@ export default class BookParser {
case 'emphasis': case 'emphasis':
style.italic = true; style.italic = true;
break; break;
case 'sup':
style.sup = true;
break;
case 'sub':
style.sub = true;
break;
case 'center': case 'center':
style.center = true; style.center = true;
break; break;
@@ -505,28 +655,21 @@ export default class BookParser {
case 'image': { case 'image': {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
let id = attrs.href.value; image = this.imageHrefToId(attrs.href.value);
let local = false; image.inline = false;
if (id[0] == '#') { image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
id = id.substr(1);
local = true;
}
image = {local, inline: false, id};
} }
break; break;
} }
case 'image-inline': { case 'image-inline': {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
let id = attrs.href.value; const img = this.imageHrefToId(attrs.href.value);
let local = false; img.inline = true;
if (id[0] == '#') { img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
id = id.substr(1);
local = true;
}
result.push({ result.push({
style: Object.assign({}, style), style: copyStyle(style),
image: {local, inline: true, id}, image: img,
text: '' text: ''
}); });
} }
@@ -543,6 +686,12 @@ export default class BookParser {
case 'emphasis': case 'emphasis':
style.italic = false; style.italic = false;
break; break;
case 'sup':
style.sup = false;
break;
case 'sub':
style.sub = false;
break;
case 'center': case 'center':
style.center = false; style.center = false;
break; break;
@@ -562,7 +711,7 @@ export default class BookParser {
}); });
//длинные слова (или белиберду без пробелов) тоже разобьем //длинные слова (или белиберду без пробелов) тоже разобьем
const maxWordLength = this.maxWordLength; const maxWordLength = this.sets.maxWordLength;
const parts = result; const parts = result;
result = []; result = [];
for (const part of parts) { for (const part of parts) {
@@ -575,7 +724,7 @@ export default class BookParser {
spaceIndex = i; spaceIndex = i;
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 && if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) { this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.sets.w - this.sets.p) {
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)}); result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)}; p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
spaceIndex = -1; spaceIndex = -1;
@@ -593,86 +742,87 @@ export default class BookParser {
splitToSlogi(word) { splitToSlogi(word) {
let result = []; let result = [];
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
const soglas = new Set([
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
]);
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
const alpha = new Set([...glas, ...soglas, ...znak]);
let slog = '';
let slogLen = 0;
const len = word.length; const len = word.length;
word += ' '; if (len > 3) {
for (let i = 0; i < len; i++) { let slog = '';
slog += word[i]; let slogLen = 0;
if (alpha.has(word[i])) word += ' ';
slogLen++; for (let i = 0; i < len; i++) {
slog += word[i];
if (alpha.has(word[i]))
slogLen++;
if (slogLen > 1 && i < len - 2 && ( if (slogLen > 1 && i < len - 2 && (
//гласная, а следом не 2 согласные буквы //гласная, а следом не 2 согласные буквы
(glas.has(word[i]) && !(soglas.has(word[i + 1]) && (glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2]) alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) || ) ||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы //предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && (alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
soglas.has(word[i]) && soglas.has(word[i + 1]) && ( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
alpha.has(word[i + 1]) && alpha.has(word[i + 2]) ) ||
) || //мягкий или твердый знак или Й
//мягкий или твердый знак или Й (znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) || (word[i] == '-')
(word[i] == '-') ) &&
) && //нельзя оставлять окончания на ь, ъ, й
//нельзя оставлять окончания на ь, ъ, й !(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
) { ) {
result.push(slog); result.push(slog);
slog = ''; slog = '';
slogLen = 0; slogLen = 0;
}
} }
if (slog)
result.push(slog);
} else {
result.push(word);
} }
if (slog)
result.push(slog);
return result; return result;
} }
parsePara(paraIndex) { parsePara(paraIndex) {
const para = this.para[paraIndex]; const para = this.para[paraIndex];
const s = this.sets;
//перераспарсиваем только при изменении одного из параметров
if (!this.force && if (!this.force &&
para.parsed && para.parsed &&
para.parsed.testWidth === this.testWidth && para.parsed.p === s.p &&
para.parsed.w === this.w && para.parsed.w === s.w &&
para.parsed.p === this.p && para.parsed.font === s.font &&
para.parsed.wordWrap === this.wordWrap && para.parsed.fontSize === s.fontSize &&
para.parsed.maxWordLength === this.maxWordLength && para.parsed.wordWrap === s.wordWrap &&
para.parsed.font === this.font && para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs && para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs && para.parsed.maxWordLength === s.maxWordLength &&
para.parsed.showImages === this.showImages && para.parsed.lineHeight === s.lineHeight &&
para.parsed.imageHeightLines === this.imageHeightLines && para.parsed.showImages === s.showImages &&
para.parsed.imageFitWidth === this.imageFitWidth && para.parsed.imageHeightLines === s.imageHeightLines &&
para.parsed.compactTextPerc === this.compactTextPerc para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
para.parsed.compactTextPerc === s.compactTextPerc &&
para.parsed.testWidth === s.testWidth
) )
return para.parsed; return para.parsed;
const parsed = { const parsed = {
testWidth: this.testWidth, p: s.p,
w: this.w, w: s.w,
p: this.p, font: s.font,
wordWrap: this.wordWrap, fontSize: s.fontSize,
maxWordLength: this.maxWordLength, wordWrap: s.wordWrap,
font: this.font, cutEmptyParagraphs: s.cutEmptyParagraphs,
cutEmptyParagraphs: this.cutEmptyParagraphs, addEmptyParagraphs: s.addEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs, maxWordLength: s.maxWordLength,
showImages: this.showImages, lineHeight: s.lineHeight,
imageHeightLines: this.imageHeightLines, showImages: s.showImages,
imageFitWidth: this.imageFitWidth, imageHeightLines: s.imageHeightLines,
compactTextPerc: this.compactTextPerc, imageFitWidth: (s.imageFitWidth || s.dualPageMode),
compactTextPerc: s.compactTextPerc,
testWidth: s.testWidth,
visible: true, //вычисляется позже visible: true, //вычисляется позже
}; };
@@ -704,7 +854,7 @@ export default class BookParser {
let ofs = 0;//смещение от начала параграфа para.offset let ofs = 0;//смещение от начала параграфа para.offset
let imgW = 0; let imgW = 0;
let imageInPara = false; let imageInPara = false;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100; const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения // тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) { for (const part of parts) {
style = part.style; style = part.style;
@@ -717,14 +867,14 @@ export default class BookParser {
if (!bin) if (!bin)
bin = {h: 1, w: 1}; bin = {h: 1, w: 1};
let lineCount = this.imageHeightLines; let lineCount = parsed.imageHeightLines;
let c = Math.ceil(bin.h/this.lineHeight); let c = Math.ceil(bin.h/parsed.lineHeight);
const maxH = lineCount*this.lineHeight; const maxH = lineCount*parsed.lineHeight;
let maxH2 = maxH; let maxH2 = maxH;
if (this.imageFitWidth && bin.w > this.w) { if (parsed.imageFitWidth && bin.w > parsed.w) {
maxH2 = bin.h*this.w/bin.w; maxH2 = bin.h*parsed.w/bin.w;
c = Math.ceil(maxH2/this.lineHeight); c = Math.ceil(maxH2/parsed.lineHeight);
} }
lineCount = (c < lineCount ? c : lineCount); lineCount = (c < lineCount ? c : lineCount);
@@ -747,6 +897,7 @@ export default class BookParser {
paraIndex, paraIndex,
w: imageWidth, w: imageWidth,
h: imageHeight, h: imageHeight,
num: part.image.num
}}); }});
lines.push(line); lines.push(line);
line = {begin: line.end + 1, parts: []}; line = {begin: line.end + 1, parts: []};
@@ -757,19 +908,19 @@ export default class BookParser {
line.last = true; line.last = true;
line.parts.push({style, text: ' ', line.parts.push({style, text: ' ',
image: {local: part.image.local, inline: false, id: part.image.id, image: {local: part.image.local, inline: false, id: part.image.id,
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight} imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight, num: part.image.num}
}); });
continue; continue;
} }
if (part.image.id && part.image.inline && this.showImages) { if (part.image.id && part.image.inline && parsed.showImages) {
const bin = this.binary[part.image.id]; const bin = this.binary[part.image.id];
if (bin) { if (bin) {
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h); let imgH = (bin.h > parsed.fontSize ? parsed.fontSize : bin.h);
imgW += bin.w*imgH/bin.h; imgW += bin.w*imgH/bin.h;
line.parts.push({style, text: '', line.parts.push({style, text: '',
image: {local: part.image.local, inline: true, id: part.image.id}}); image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
} }
} }
@@ -881,11 +1032,11 @@ export default class BookParser {
//parsed.visible //parsed.visible
if (imageInPara) { if (imageInPara) {
parsed.visible = this.showImages; parsed.visible = parsed.showImages;
} else { } else {
parsed.visible = !( parsed.visible = !(
(para.addIndex > this.addEmptyParagraphs) || (para.addIndex > parsed.addEmptyParagraphs) ||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '') (para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
); );
} }

View File

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

View File

@@ -0,0 +1,40 @@
import localForage from 'localforage';
//import _ from 'lodash';
const wpStore = localForage.createInstance({
name: 'wallpaperStorage'
});
class WallpaperStorage {
constructor() {
this.cachedKeys = [];
}
async init() {
this.cachedKeys = await wpStore.keys();
}
async getLength() {
return await wpStore.length();
}
async setData(key, data) {
await wpStore.setItem(key, data);
this.cachedKeys = await wpStore.keys();
}
async getData(key) {
return await wpStore.getItem(key);
}
async removeData(key) {
await wpStore.removeItem(key);
this.cachedKeys = await wpStore.keys();
}
keyExists(key) {//не асинхронная
return this.cachedKeys.includes(key);
}
}
export default new WallpaperStorage();

View File

@@ -1,53 +1,240 @@
export const versionHistory = [ export const versionHistory = [
{ {
showUntil: '2020-11-12', version: '0.11.4',
header: '0.9.8 (2020-11-13)', releaseDate: '2022-04-14',
showUntil: '2022-04-13',
content: content:
` `
<ul> <ul>
<li>добавлено окно "Оглавление/закладки"</li> <li>небольшие дополнения интерфейса</li>
<li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
showUntil: '2020-11-11', version: '0.11.3',
header: '0.9.7 (2020-11-12)', releaseDate: '2022-03-29',
showUntil: '2022-03-28',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.11.2',
releaseDate: '2022-01-11',
showUntil: '2022-01-10',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.1',
releaseDate: '2021-12-03',
showUntil: '2021-12-02',
content:
`
<ul>
<li>переход на JembaDb вместо SQLite</li>
</ul>
`
},
{
version: '0.11.0',
releaseDate: '2021-11-18',
showUntil: '2021-11-17',
content:
`
<ul>
<li>переход на Vue 3</li>
</ul>
`
},
{
version: '0.10.3',
releaseDate: '2021-10-24',
showUntil: '2021-10-23',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.2',
releaseDate: '2021-10-19',
showUntil: '2021-10-18',
content:
`
<ul>
<li>актуализация версий пакетов и стека используемых технологий</li>
</ul>
`
},
{
version: '0.10.1',
releaseDate: '2021-10-10',
showUntil: '2021-10-09',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.0',
releaseDate: '2021-02-09',
showUntil: '2021-02-16',
content:
`
<ul>
<li>добавлен двухстраничный режим</li>
<li>в настройки добавлены все кириллические веб-шрифты от google</li>
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
<li>немного улучшен парсинг fb2</li>
</ul>
`
},
{
version: '0.9.12',
releaseDate: '2020-12-18',
showUntil: '2020-12-17',
content:
`
<ul>
<li>добавлена вкладка "Изображения" в окно оглавления</li>
<li>настройки конвертирования вынесены в отдельную вкладку</li>
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
<li>улучшения работы конвертеров</li>
</ul>
`
},
{
version: '0.9.11',
releaseDate: '2020-12-09',
showUntil: '2020-12-08',
content:
`
<ul>
<li>оптимизации, улучшения работы конвертеров</li>
</ul>
`
},
{
version: '0.9.10',
releaseDate: '2020-12-03',
showUntil: '2020-12-10',
content:
`
<ul>
<li>добавлена частичная поддержка формата Djvu</li>
<li>добавлена поддержка Rar-архивов</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.9',
releaseDate: '2020-11-21',
showUntil: '2020-11-20',
content:
`
<ul>
<li>оптимизации, исправления багов</li>
</ul>
`
},
{
version: '0.9.8',
releaseDate: '2020-11-13',
showUntil: '2020-11-12',
content:
`
<ul>
<li>добавлено окно "Оглавление/закладки"</li>
</ul>
`
},
{
version: '0.9.7',
releaseDate: '2020-11-12',
showUntil: '2020-11-11',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.6',
releaseDate: '2020-11-06',
showUntil: '2020-11-05', showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content: content:
` `
<ul> <ul>
<li>завершена работа над новым окном "Библиотека"</li> <li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.5',
releaseDate: '2020-11-01',
showUntil: '2020-10-31', showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)',
content: content:
` `
<ul> <ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li> <li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.4',
releaseDate: '2020-10-29',
showUntil: '2020-10-28', showUntil: '2020-10-28',
header: '0.9.4 (2020-10-29)',
content: content:
` `
<ul> <ul>
@@ -55,23 +242,27 @@ export const versionHistory = [
<li>для liberama.top добавлено новое окно: "Библиотека"</li> <li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.3',
releaseDate: '2020-05-21',
showUntil: '2020-05-20', showUntil: '2020-05-20',
header: '0.9.3 (2020-05-21)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.2',
releaseDate: '2020-03-15',
showUntil: '2020-04-25', showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content: content:
` `
<ul> <ul>
@@ -79,119 +270,139 @@ export const versionHistory = [
<li>переход на Service Worker вместо AppCache для автономного режима работы</li> <li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.1',
releaseDate: '2020-03-03',
showUntil: '2020-03-02', showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content: content:
` `
<ul> <ul>
<li>улучшение работы серверной части</li> <li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li> <li>незначительные изменения интерфейса</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.0',
releaseDate: '2020-02-26',
showUntil: '2020-02-25', showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',
content: content:
` `
<ul> <ul>
<li>переход на UI-фреймфорк Quasar</li> <li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li> <li>незначительные изменения интерфейса</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.4',
releaseDate: '2020-02-06',
showUntil: '2020-02-05', showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content: content:
` `
<ul> <ul>
<li>добавлен paypal-адрес для пожертвований</li> <li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.3',
releaseDate: '2020-01-28',
showUntil: '2020-01-27', showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content: content:
` `
<ul> <ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li> <li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li> <li>внутренние оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.2',
releaseDate: '2020-01-20',
showUntil: '2020-01-19', showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content: content:
` `
<ul> <ul>
<li>внутренние оптимизации</li> <li>внутренние оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.1',
releaseDate: '2020-01-07',
showUntil: '2020-01-06', showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content: content:
` `
<ul> <ul>
<li>добавлена частичная поддержка формата FB3</li> <li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li> <li>исправлен баг "Request path contains unescaped characters"</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.0',
releaseDate: '2020-01-02',
showUntil: '2020-01-05', showUntil: '2020-01-05',
header: '0.8.0 (2020-01-02)',
content: content:
` `
<ul> <ul>
<li>окончательный переход на https</li> <li>окончательный переход на https</li>
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li> <li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.9',
releaseDate: '2019-11-27',
showUntil: '2019-11-26', showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content: content:
` `
<ul> <ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li> <li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.8',
releaseDate: '2019-11-25',
showUntil: '2019-11-24', showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)',
content: content:
` `
<ul> <ul>
<li>улучшение html-фильтров для сайтов</li> <li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.7',
releaseDate: '2019-11-06',
showUntil: '2019-11-10', showUntil: '2019-11-10',
header: '0.7.7 (2019-11-06)',
content: content:
` `
<ul> <ul>
@@ -203,34 +414,40 @@ export const versionHistory = [
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li> <li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
</ul> </ul>
</ul> </ul>
` `
}, },
{ {
version: '0.7.6',
releaseDate: '2019-10-30',
showUntil: '2019-10-29', showUntil: '2019-10-29',
header: '0.7.6 (2019-10-30)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.5',
releaseDate: '2019-10-22',
showUntil: '2019-10-21', showUntil: '2019-10-21',
header: '0.7.5 (2019-10-22)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.3',
releaseDate: '2019-10-18',
showUntil: '2019-10-17', showUntil: '2019-10-17',
header: '0.7.3 (2019-10-18)',
content: content:
` `
<ul> <ul>
@@ -239,12 +456,14 @@ export const versionHistory = [
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li> <li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.1',
releaseDate: '2019-09-20',
showUntil: '2019-09-19', showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content: content:
` `
<ul> <ul>
@@ -252,12 +471,14 @@ export const versionHistory = [
<li>на панель управления добавлена кнопка "Автономный режим"</li> <li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li> <li>актуализирована справка</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.0',
releaseDate: '2019-09-07',
showUntil: '2019-10-01', showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content: content:
` `
<ul> <ul>
@@ -268,23 +489,27 @@ export const versionHistory = [
<li>немного улучшен внешний вид и управление на смартфонах</li> <li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li> <li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.10',
releaseDate: '2019-07-21',
showUntil: '2019-07-20', showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.9',
releaseDate: '2019-06-23',
showUntil: '2019-06-22', showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content: content:
` `
<ul> <ul>
@@ -295,12 +520,14 @@ export const versionHistory = [
<li>улучшены прогрессбары</li> <li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li> <li>исправления недочетов, небольшие оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.7',
releaseDate: '2019-05-30',
showUntil: '2019-06-05', showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content: content:
` `
<ul> <ul>
@@ -313,36 +540,42 @@ export const versionHistory = [
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li> <li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li> <li>исправления багов и недочетов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.6',
releaseDate: '2019-03-29',
showUntil: '2019-03-29', showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content: content:
` `
<ul> <ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li> <li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li> <li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.4',
releaseDate: '2019-03-24',
showUntil: '2019-03-24', showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content: content:
` `
<ul> <ul>
<li>исправления багов, оптимизации</li> <li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li> <li>добавлена возможность синхронизации данных между устройствами</li>
</ul> </ul>
` `
}, },
{ {
version: '0.5.4',
releaseDate: '2019-03-04',
showUntil: '2019-03-04', showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content: content:
` `
<ul> <ul>
@@ -351,12 +584,14 @@ export const versionHistory = [
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li> <li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li> <li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul> </ul>
` `
}, },
{ {
version: '0.3.0',
releaseDate: '2019-02-17',
showUntil: '2019-02-17', showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content: content:
` `
<ul> <ul>
@@ -364,12 +599,14 @@ export const versionHistory = [
<li>улучшено распознавание текста</li> <li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li> <li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul> </ul>
` `
}, },
{ {
version: '0.1.7',
releaseDate: '2019-02-14',
showUntil: '2019-02-14', showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content: content:
` `
<ul> <ul>
@@ -379,17 +616,20 @@ export const versionHistory = [
<li>добавлена возможность сброса настроек</li> <li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li> <li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul> </ul>
` `
}, },
{ {
version: '0.1.0',
releaseDate: '2019-02-12',
showUntil: '2019-02-12', showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content: content:
` `
<ul> <ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li> <li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul> </ul>
` `
}, },

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,9 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
export default @Component({ class Notify {
})
class Notify extends Vue {
notify(opts) { notify(opts) {
let { let {
caption = null, caption = null,
@@ -55,5 +52,7 @@ class Notify extends Vue {
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options)); this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
} }
} }
export default vueComponent(Notify);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import Vue from 'vue';
import 'quasar/dist/quasar.css'; import 'quasar/dist/quasar.css';
import Quasar from 'quasar/src/vue-plugin.js' //import Quasar from 'quasar/dist/quasar.umd.prod.js';
import Quasar from 'quasar/src/vue-plugin.js';
//config //config
const config = {}; const config = {};
@@ -21,7 +20,8 @@ import {QSlider} from 'quasar/src/components/slider';
import {QTabs, QTab} from 'quasar/src/components/tabs'; import {QTabs, QTab} from 'quasar/src/components/tabs';
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels'; //import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
import {QSeparator} from 'quasar/src/components/separator'; import {QSeparator} from 'quasar/src/components/separator';
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item'; //import {QList} from 'quasar/src/components/item';
import {QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
import {QTooltip} from 'quasar/src/components/tooltip'; import {QTooltip} from 'quasar/src/components/tooltip';
import {QSpinner} from 'quasar/src/components/spinner'; import {QSpinner} from 'quasar/src/components/spinner';
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table'; import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
@@ -32,8 +32,7 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog'; import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip'; import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree'; import {QTree} from 'quasar/src/components/tree';
import {QExpansionItem} from 'quasar/src/components/expansion-item'; //import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = { const components = {
//QLayout, //QLayout,
@@ -50,7 +49,8 @@ const components = {
QTabs, QTab, QTabs, QTab,
//QTabPanels, QTabPanel, //QTabPanels, QTabPanel,
QSeparator, QSeparator,
QList, QItem, QItemSection, QItemLabel, //QList,
QItem, QItemSection, QItemLabel,
QTooltip, QTooltip,
QSpinner, QSpinner,
QTable, QTh, QTr, QTd, QTable, QTh, QTr, QTd,
@@ -61,7 +61,7 @@ const components = {
QDialog, QDialog,
QChip, QChip,
QTree, QTree,
QExpansionItem, //QExpansionItem,
}; };
//directives //directives
@@ -79,16 +79,17 @@ const plugins = {
Notify, Notify,
}; };
//use
Vue.use(Quasar, { config, components, directives, plugins });
//icons //icons
//import '@quasar/extras/material-icons/material-icons.css';
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css'; //import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import '@quasar/extras/line-awesome/line-awesome.css'; import '@quasar/extras/line-awesome/line-awesome.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import lineAwesome from 'quasar/icon-set/line-awesome.js' import lineAwesome from 'quasar/icon-set/line-awesome.js'
Quasar.iconSet.set(lineAwesome);
export default {
quasar: Quasar,
options: { config, components, directives, plugins },
init: () => {
Quasar.iconSet.set(lineAwesome);
}
};

View File

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

View File

@@ -0,0 +1,22 @@
class DynamicCss {
constructor() {
this.cssNodes = {};
}
replace(name, cssText) {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = cssText;
const parent = document.getElementsByTagName('head')[0];
if (this.cssNodes[name]) {
parent.removeChild(this.cssNodes[name]);
delete this.cssNodes[name];
}
this.cssNodes[name] = parent.appendChild(style);
}
}
export default new DynamicCss();

View File

@@ -13,6 +13,10 @@ export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
export function toHex(buf) {
return Buffer.from(buf).toString('hex');
}
export function stringToHex(str) { export function stringToHex(str) {
return Buffer.from(str).toString('hex'); return Buffer.from(str).toString('hex');
} }
@@ -304,3 +308,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
} }
return result; return result;
} }
export function removeHtmlTags(s) {
return s.replace(/(<([^>]+)>)/ig, '');
}
export function makeValidFilename(filename, repl = '_') {
let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
f = f.trim();
while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
f = f.substring(0, f.length - 1);
}
if (f)
return f;
else
throw new Error('Invalid filename');
}
export function getBookTitle(fb2) {
fb2 = (fb2 ? fb2 : {});
const result = {};
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
result.author = authorNames.join(', ');
}
if (fb2.sequence) {
const seqs = fb2.sequence.map(s => _.compact([
s.name,
(s.number ? `#${s.number}` : null),
]).join(' '));
result.sequence = seqs.join(', ');
if (result.sequence)
result.sequenceTitle = `(${result.sequence})`;
}
result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
result.fullTitle = _.compact([
result.author,
result.bookTitle
]).join(' - ');
return result;
}

View File

@@ -1,5 +1,4 @@
import Vue from 'vue'; import { createStore } from 'vuex';
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate'; import createPersistedState from 'vuex-persistedstate';
import root from './root.js'; import root from './root.js';
@@ -7,11 +6,9 @@ import uistate from './modules/uistate';
import config from './modules/config'; import config from './modules/config';
import reader from './modules/reader'; import reader from './modules/reader';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store(Object.assign({}, root, { export default createStore(Object.assign({}, root, {
modules: { modules: {
uistate, uistate,
config, config,

View File

@@ -10,18 +10,7 @@ const state = {
const getters = {}; const getters = {};
// actions // actions
const actions = { const actions = {};
async loadConfig({ commit, state }) {
commit('setApiError', null, { root: true });
commit('setConfig', {});
try {
const config = await miscApi.loadConfig();
commit('setConfig', config);
} catch (e) {
commit('setApiError', e, { root: true });
}
},
};
// mutations // mutations
const mutations = { const mutations = {

View File

@@ -0,0 +1 @@
["Alegreya","Alegreya SC","Alegreya Sans","Alegreya Sans SC","Alice","Amatic SC","Andika","Anonymous Pro","Arimo","Arsenal","Bad Script","Balsamiq Sans","Bellota","Bellota Text","Bitter","Caveat","Comfortaa","Commissioner","Cormorant","Cormorant Garamond","Cormorant Infant","Cormorant SC","Cormorant Unicase","Cousine","Cuprum","Didact Gothic","EB Garamond","El Messiri","Exo 2","Fira Code","Fira Mono","Fira Sans","Fira Sans Condensed","Fira Sans Extra Condensed","Forum","Gabriela","Hachi Maru Pop","IBM Plex Mono","IBM Plex Sans","IBM Plex Serif","Inter","Istok Web","JetBrains Mono","Jost","Jura","Kelly Slab","Kosugi","Kosugi Maru","Kurale","Ledger","Literata","Lobster","Lora","M PLUS 1p","M PLUS Rounded 1c","Manrope","Marck Script","Marmelad","Merriweather","Montserrat","Montserrat Alternates","Neucha","Noto Sans","Noto Serif","Nunito","Old Standard TT","Open Sans","Open Sans Condensed","Oranienbaum","Oswald","PT Mono","PT Sans","PT Sans Caption","PT Sans Narrow","PT Serif","PT Serif Caption","Pacifico","Pangolin","Pattaya","Philosopher","Piazzolla","Play","Playfair Display","Playfair Display SC","Podkova","Poiret One","Prata","Press Start 2P","Prosto One","Raleway","Roboto","Roboto Condensed","Roboto Mono","Roboto Slab","Rubik","Rubik Mono One","Ruda","Ruslan Display","Russo One","Sawarabi Gothic","Scada","Seymour One","Source Code Pro","Source Sans Pro","Source Serif Pro","Spectral","Spectral SC","Stalinist One","Tenor Sans","Tinos","Ubuntu","Ubuntu Condensed","Ubuntu Mono","Underdog","Viaoda Libre","Vollkorn","Vollkorn SC","Yanone Kaffeesatz","Yeseva One"]

View File

@@ -0,0 +1,13 @@
const fs = require('fs-extra');
async function main() {
const webfonts = await fs.readFile('webfonts.json');
let fonts = JSON.parse(webfonts);
fonts = fonts.items.filter(item => item.subsets.includes('cyrillic'));
fonts = fonts.map(item => item.family);
fonts.sort();
await fs.writeFile('fonts.json', JSON.stringify(fonts));
}
main();

View File

@@ -1,8 +1,11 @@
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json';
const readerActions = { const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки', 'loader': 'На страницу загрузки',
'loadFile': 'Загрузить файл с диска',
'loadBuffer': 'Загрузить из буфера обмена',
'help': 'Вызвать cправку',
'settings': 'Настроить', 'settings': 'Настроить',
'undoAction': 'Действие назад', 'undoAction': 'Действие назад',
'redoAction': 'Действие вперед', 'redoAction': 'Действие вперед',
@@ -12,11 +15,12 @@ const readerActions = {
'setPosition': 'Установить позицию', 'setPosition': 'Установить позицию',
'search': 'Найти в тексте', 'search': 'Найти в тексте',
'copyText': 'Скопировать текст со страницы', 'copyText': 'Скопировать текст со страницы',
'splitToPara': 'Обновить с разбиением на параграфы', 'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу', 'refresh': 'Принудительно обновить книгу',
'clickControl': 'Управление кликом',
'offlineMode': 'Автономный режим (без интернета)', 'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки', 'contents': 'Оглавление/закладки',
'libs': 'Библиотека', 'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние', 'recentBooks': 'Открыть недавние',
'switchToolbar': 'Показать/скрыть панель управления', 'switchToolbar': 'Показать/скрыть панель управления',
'donate': '', 'donate': '',
@@ -34,6 +38,9 @@ const readerActions = {
//readerActions[name] //readerActions[name]
const toolButtons = [ const toolButtons = [
{name: 'loadFile', show: true},
{name: 'loadBuffer', show: true},
{name: 'help', show: true},
{name: 'undoAction', show: true}, {name: 'undoAction', show: true},
{name: 'redoAction', show: true}, {name: 'redoAction', show: true},
{name: 'fullScreen', show: true}, {name: 'fullScreen', show: true},
@@ -41,18 +48,21 @@ const toolButtons = [
{name: 'setPosition', show: true}, {name: 'setPosition', show: true},
{name: 'search', show: true}, {name: 'search', show: true},
{name: 'copyText', show: false}, {name: 'copyText', show: false},
{name: 'splitToPara', show: false}, {name: 'convOptions', show: true},
{name: 'refresh', show: true}, {name: 'refresh', show: true},
{name: 'contents', show: true}, {name: 'contents', show: true},
{name: 'libs', show: true}, {name: 'libs', show: true},
{name: 'recentBooks', show: true}, {name: 'recentBooks', show: true},
{name: 'clickControl', show: false},
{name: 'offlineMode', show: false}, {name: 'offlineMode', show: false},
]; ];
//readerActions[name] //readerActions[name]
const hotKeys = [ const hotKeys = [
{name: 'help', codes: ['F1', 'H']},
{name: 'loader', codes: ['Escape']}, {name: 'loader', codes: ['Escape']},
{name: 'loadFile', codes: ['F3']},
{name: 'loadBuffer', codes: ['F4']},
{name: 'help', codes: ['F1', 'H']},
{name: 'settings', codes: ['S']}, {name: 'settings', codes: ['S']},
{name: 'undoAction', codes: ['Ctrl+BracketLeft']}, {name: 'undoAction', codes: ['Ctrl+BracketLeft']},
{name: 'redoAction', codes: ['Ctrl+BracketRight']}, {name: 'redoAction', codes: ['Ctrl+BracketRight']},
@@ -61,11 +71,12 @@ const hotKeys = [
{name: 'setPosition', codes: ['P']}, {name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']}, {name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']}, {name: 'copyText', codes: ['Ctrl+C']},
{name: 'splitToPara', codes: ['Shift+R']}, {name: 'convOptions', codes: ['Ctrl+M']},
{name: 'refresh', codes: ['R']}, {name: 'refresh', codes: ['R']},
{name: 'contents', codes: ['C']}, {name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']}, {name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']}, {name: 'recentBooks', codes: ['X']},
{name: 'clickControl', codes: ['Ctrl+B']},
{name: 'offlineMode', codes: ['O']}, {name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']}, {name: 'switchToolbar', codes: ['Tab', 'Q']},
@@ -91,125 +102,22 @@ const fonts = [
{name: 'Rubik', fontVertShift: 0}, {name: 'Rubik', fontVertShift: 0},
]; ];
const webFonts = [ //webFonts: [{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: 0}, ...],
{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: -5}, const webFonts = [];
{css: 'https://fonts.googleapis.com/css?family=Alegreya+Sans', name: 'Alegreya Sans', fontVertShift: 5}, for (const family of googleFonts) {
{css: 'https://fonts.googleapis.com/css?family=Alegreya+SC', name: 'Alegreya SC', fontVertShift: -5}, webFonts.push({
{css: 'https://fonts.googleapis.com/css?family=Alice', name: 'Alice', fontVertShift: 5}, css: `https://fonts.googleapis.com/css?family=${family.replace(/\s/g, '+')}`,
{css: 'https://fonts.googleapis.com/css?family=Amatic+SC', name: 'Amatic SC', fontVertShift: 0}, name: family,
{css: 'https://fonts.googleapis.com/css?family=Andika', name: 'Andika', fontVertShift: -35}, fontVertShift: 0,
{css: 'https://fonts.googleapis.com/css?family=Anonymous+Pro', name: 'Anonymous Pro', fontVertShift: 5}, });
{css: 'https://fonts.googleapis.com/css?family=Arsenal', name: 'Arsenal', fontVertShift: 0}, }
{css: 'https://fonts.googleapis.com/css?family=Bad+Script', name: 'Bad Script', fontVertShift: -30},
{css: 'https://fonts.googleapis.com/css?family=Caveat', name: 'Caveat', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Comfortaa', name: 'Comfortaa', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Cormorant', name: 'Cormorant', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Garamond', name: 'Cormorant Garamond', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Infant', name: 'Cormorant Infant', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Unicase', name: 'Cormorant Unicase', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cousine', name: 'Cousine', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Cuprum', name: 'Cuprum', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Didact+Gothic', name: 'Didact Gothic', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=EB+Garamond', name: 'EB Garamond', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=El+Messiri', name: 'El Messiri', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Mono', name: 'Fira Mono', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans', name: 'Fira Sans', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Condensed', name: 'Fira Sans Condensed', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Extra+Condensed', name: 'Fira Sans Extra Condensed', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Forum', name: 'Forum', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Gabriela', name: 'Gabriela', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Mono', name: 'IBM Plex Mono', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Sans', name: 'IBM Plex Sans', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Serif', name: 'IBM Plex Serif', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Istok+Web', name: 'Istok Web', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Jura', name: 'Jura', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Kelly+Slab', name: 'Kelly Slab', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Kosugi', name: 'Kosugi', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Kosugi+Maru', name: 'Kosugi Maru', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Kurale', name: 'Kurale', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Ledger', name: 'Ledger', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Lobster', name: 'Lobster', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Lora', name: 'Lora', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Marck+Script', name: 'Marck Script', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Marmelad', name: 'Marmelad', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Merriweather', name: 'Merriweather', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Montserrat', name: 'Montserrat', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Montserrat+Alternates', name: 'Montserrat Alternates', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Neucha', name: 'Neucha', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans', name: 'Noto Sans', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans+SC', name: 'Noto Sans SC', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif', name: 'Noto Serif', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif+TC', name: 'Noto Serif TC', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Old+Standard+TT', name: 'Old Standard TT', fontVertShift: 15},
{css: 'https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300', name: 'Open Sans Condensed', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Oranienbaum', name: 'Oranienbaum', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Oswald', name: 'Oswald', fontVertShift: -20},
{css: 'https://fonts.googleapis.com/css?family=Pacifico', name: 'Pacifico', fontVertShift: -35},
{css: 'https://fonts.googleapis.com/css?family=Pangolin', name: 'Pangolin', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Pattaya', name: 'Pattaya', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Philosopher', name: 'Philosopher', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Play', name: 'Play', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display', name: 'Playfair Display', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display+SC', name: 'Playfair Display SC', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Podkova', name: 'Podkova', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Poiret+One', name: 'Poiret One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Prata', name: 'Prata', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Prosto+One', name: 'Prosto One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=PT+Mono', name: 'PT Mono', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=PT+Sans', name: 'PT Sans', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Caption', name: 'PT Sans Caption', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Narrow', name: 'PT Sans Narrow', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Serif', name: 'PT Serif', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=PT+Serif+Caption', name: 'PT Serif Caption', fontVertShift: -10},
{css: 'https://fonts.googleapis.com/css?family=Roboto+Condensed', name: 'Roboto Condensed', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Roboto+Mono', name: 'Roboto Mono', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Roboto+Slab', name: 'Roboto Slab', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Ruslan+Display', name: 'Ruslan Display', fontVertShift: 20},
{css: 'https://fonts.googleapis.com/css?family=Russo+One', name: 'Russo One', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Sawarabi+Gothic', name: 'Sawarabi Gothic', fontVertShift: -15},
{css: 'https://fonts.googleapis.com/css?family=Scada', name: 'Scada', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Seymour+One', name: 'Seymour One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro', name: 'Source Sans Pro', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Spectral', name: 'Spectral', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Stalinist+One', name: 'Stalinist One', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Tinos', name: 'Tinos', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Tenor+Sans', name: 'Tenor Sans', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Underdog', name: 'Underdog', fontVertShift: 10},
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Mono', name: 'Ubuntu Mono', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Condensed', name: 'Ubuntu Condensed', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Vollkorn', name: 'Vollkorn', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Vollkorn+SC', name: 'Vollkorn SC', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz', name: 'Yanone Kaffeesatz', fontVertShift: 20},
{css: 'https://fonts.googleapis.com/css?family=Yeseva+One', name: 'Yeseva One', fontVertShift: 10},
];
//---------------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------------
const settingDefaults = { const settingDefaults = {
textColor: '#000000', textColor: '#000000',
backgroundColor: '#EBE2C9', backgroundColor: '#ebe2c9',
wallpaper: '', wallpaper: '',
wallpaperIgnoreStatusBar: false,
fontStyle: '',// 'italic' fontStyle: '',// 'italic'
fontWeight: '',// 'bold' fontWeight: '',// 'bold'
fontSize: 20,// px fontSize: 20,// px
@@ -226,9 +134,22 @@ const settingDefaults = {
wordWrap: true,//перенос по слогам wordWrap: true,//перенос по слогам
keepLastToFirst: false,// перенос последней строки в первую при листании keepLastToFirst: false,// перенос последней строки в первую при листании
dualPageMode: false,
dualIndentLR: 10,// px, отступ слева и справа внутри страницы в двухстраничном режиме
dualDivWidth: 2,// px, ширина разделителя
dualDivHeight: 100,// процент, высота разделителя
dualDivColorAsText: true,//цвет как у текста
dualDivColor: '#000000',
dualDivColorAlpha: 0.7,// прозрачность разделителя
dualDivStrokeFill: 1,// px, заполнение пунктира
dualDivStrokeGap: 1,// px, промежуток пунктира
dualDivShadowWidth: 0,// px, ширина тени
showStatusBar: true, showStatusBar: true,
statusBarTop: false,// top, bottom statusBarTop: false,// top, bottom
statusBarHeight: 19,// px statusBarHeight: 19,// px
statusBarColorAsText: true,//цвет как у текста
statusBarColor: '#000000',
statusBarColorAlpha: 0.4, statusBarColorAlpha: 0.4,
statusBarClickOpen: true, statusBarClickOpen: true,
@@ -251,15 +172,21 @@ const settingDefaults = {
compactTextPerc: 0, compactTextPerc: 0,
imageHeightLines: 100, imageHeightLines: 100,
imageFitWidth: true, imageFitWidth: true,
enableSitesFilter: true,
splitToPara: false,
djvuQuality: 20,
pdfAsText: true,
pdfQuality: 20,
showServerStorageMessages: true, showServerStorageMessages: true,
showWhatsNewDialog: true, showWhatsNewDialog: true,
showDonationDialog2020: true, showDonationDialog2020: true,
showLiberamaTopDialog2020: true, showNeedUpdateNotify: true,
enableSitesFilter: true,
fontShifts: {}, fontShifts: {},
showToolButton: {}, showToolButton: {},
userHotKeys: {}, userHotKeys: {},
userWallpapers: [],
}; };
for (const font of fonts) for (const font of fonts)
@@ -271,12 +198,13 @@ for (const button of toolButtons)
for (const hotKey of hotKeys) for (const hotKey of hotKeys)
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes; settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
const excludeDiffHotKeys = []; const diffExclude = [];
for (const hotKey of hotKeys) for (const hotKey of hotKeys)
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`); diffExclude.push(`userHotKeys/${hotKey.name}`);
diffExclude.push('userWallpapers');
function addDefaultsToSettings(settings) { function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys}); const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) { if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false}); return utils.applyObjDiff(settings, diff, {isApplyChange: false});
} }

View File

@@ -0,0 +1,43 @@
server {
listen 80;
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;
}

View File

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

View File

@@ -35,6 +35,7 @@ server {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ server {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
} }
location / { location / {
@@ -66,6 +67,6 @@ server {
location ~ \.php$ { location ~ \.php$ {
try_files $uri =404; try_files $uri =404;
include /etc/nginx/fastcgi.conf; include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock; fastcgi_pass unix:/run/php/php7.4-fpm.sock;
} }
} }

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