Compare commits

...

622 Commits

Author SHA1 Message Date
Book Pauk
9405ce2cc0 Merge branch 'release/0.11.7-3' 2022-07-13 16:34:16 +07:00
Book Pauk
115277d88a Поправки разметки 2022-07-13 16:34:00 +07:00
Book Pauk
6925c11dbd Merge tag '0.11.7-2' into develop
0.11.7-2
2022-07-13 16:25:11 +07:00
Book Pauk
984d835892 Merge branch 'release/0.11.7-2' 2022-07-13 16:25:05 +07:00
Book Pauk
23353a4960 Улучшен парсинг fb2 2022-07-13 16:23:52 +07:00
Book Pauk
955bcda032 Поправки разметки 2022-07-13 15:01:35 +07:00
Book Pauk
81ad5d7a2c Поправки разметки 2022-07-13 14:47:24 +07:00
Book Pauk
dada7980ec Merge tag '0.11.7-1' into develop
0.11.7-1
2022-07-12 19:23:38 +07:00
Book Pauk
511a308646 Merge branch 'release/0.11.7-1' 2022-07-12 19:23:33 +07:00
Book Pauk
65c8f2cc81 Небольшие поправки на панели, изменена нумерация на обратную 2022-07-12 19:21:26 +07:00
Book Pauk
238c18bc48 Merge tag '0.11.7' into develop
0.11.7
2022-07-12 19:08:35 +07:00
Book Pauk
873a08fee1 Merge branch 'release/0.11.7' 2022-07-12 19:08:27 +07:00
Book Pauk
7e89228803 Версия 0.11.7 2022-07-12 19:07:39 +07:00
Book Pauk
fc630923a4 Настройка методов сортировки 2022-07-12 18:50:35 +07:00
Book Pauk
928f911d03 Добавлены подсказки к кнопкам 2022-07-12 17:53:14 +07:00
Book Pauk
7ffcd3fe1b Поправки поведения при скроллинге 2022-07-12 17:33:03 +07:00
Book Pauk
0efbaf643a Поправил сообщение об ошибке 2022-07-12 17:32:19 +07:00
Book Pauk
f1bf8e54ae Добавлен метод scrollToActiveBook 2022-07-12 17:10:50 +07:00
Book Pauk
b4aa6ab6c8 Поправки поиска 2022-07-12 16:58:34 +07:00
Book Pauk
72431f0202 Работа над группировкой 2022-07-12 16:51:32 +07:00
Book Pauk
04a326c0e4 Работа над группировкой 2022-07-12 15:51:43 +07:00
Book Pauk
931966f4f3 Поправки разметки 2022-07-12 15:05:17 +07:00
Book Pauk
8808cc4779 Работа над группировкой по файлам 2022-07-12 14:46:34 +07:00
Book Pauk
988c959eba Работа над группировкой файлов 2022-07-12 04:05:51 +07:00
Book Pauk
c0b658d9e6 К предыдущему 2022-07-12 01:41:18 +07:00
Book Pauk
3190246f34 Улучшена реакция на onResize 2022-07-12 01:35:19 +07:00
Book Pauk
d957b4a5f9 Добавлена возможность автосокрытия панели при прокрутке 2022-07-12 01:03:44 +07:00
Book Pauk
bef9e5705c Поправки текстовых строк 2022-07-11 23:53:54 +07:00
Book Pauk
eb2affa518 Приведение input к единому стилю 2022-07-11 23:50:51 +07:00
Book Pauk
07b9a3c033 Мелкие правки 2022-07-11 22:28:48 +07:00
Book Pauk
3ca14ae06a Работа над группировкой 2022-07-11 22:26:34 +07:00
Book Pauk
7caa0c2112 Начало добавления группировки в RecentBooksPage 2022-07-11 20:11:38 +07:00
Book Pauk
9c69f5bc01 Поправил размер иконки 2022-07-11 20:10:51 +07:00
Book Pauk
125a2e0f17 Исправление багов 2022-07-11 17:12:17 +07:00
Book Pauk
1b4360b897 Дополнение в convertRecent 2022-07-11 16:26:03 +07:00
Book Pauk
4775d6e47b Поправлен баг 2022-07-10 20:07:33 +07:00
Book Pauk
33fc553c55 Добавлен запрос на объединение позиций при
обнаружении похожего файла в загруженных
2022-07-10 19:54:00 +07:00
Book Pauk
25cad81c50 Улучшение отображения загруженных 2022-07-10 19:53:30 +07:00
Book Pauk
02a2099c1f Поправлен z-index 2022-07-10 19:52:58 +07:00
Book Pauk
1cda186b1a Добавлен диалог askYesNo 2022-07-10 19:52:29 +07:00
Book Pauk
f10291b6c6 Поправка названия действия 2022-07-10 19:51:31 +07:00
Book Pauk
26ab5d6765 Рефакторинг 2022-07-10 18:27:05 +07:00
Book Pauk
5edeed0747 Изменение механизма хранения книг 2022-07-10 17:31:21 +07:00
Book Pauk
c878ce432f Небольшое исправление опознававния кодировки 2022-07-10 17:20:47 +07:00
Book Pauk
81798897c8 Изменения в механизме хранения книг:
теперь ориентируемся на "ключ-filepath", а не "ключ-url"
2022-07-10 16:38:54 +07:00
Book Pauk
63840fadbc К предыдущему 2022-07-10 14:59:39 +07:00
Book Pauk
36aa057035 Поправка цвета 2022-07-09 21:00:09 +07:00
Book Pauk
30afd2421c Рефакторинг 2022-07-09 20:50:31 +07:00
Book Pauk
53a1d90bd8 Улучшение поведения при очереди загрузки книг 2022-07-09 02:01:14 +07:00
Book Pauk
2ecf6beef2 Небольшой багфикс 2022-07-09 01:56:42 +07:00
Book Pauk
85910a20e9 Улучшение ContentsPage 2022-07-08 20:50:55 +07:00
Book Pauk
66cf7790b3 Улучшения ContentsPage 2022-07-08 19:09:57 +07:00
Book Pauk
4a9eb7e4bb Удалил устаревшее 2022-07-08 14:30:44 +07:00
Book Pauk
07446696c1 Поправлен цвет заголовка 2022-07-08 13:52:45 +07:00
Book Pauk
a29f9d9a4b Унификация размеров окон 2022-07-08 13:43:59 +07:00
Book Pauk
d49c9baec3 Унификация интерфейса 2022-07-08 13:34:53 +07:00
Book Pauk
8c9d4a12ee Настройка цветов 2022-07-08 13:24:13 +07:00
Book Pauk
fce69e4657 Настройка цветов 2022-07-08 13:21:42 +07:00
Book Pauk
b387509f88 Добавил блокировку при загрузке книг, теперь загружаются последовательно 2022-07-08 12:26:47 +07:00
Book Pauk
8dc8bdc0d6 Merge tag '0.11.6-2' into develop
0.11.6-2
2022-07-07 19:43:47 +07:00
Book Pauk
00caae8363 Merge branch 'release/0.11.6-2' 2022-07-07 19:43:40 +07:00
Book Pauk
2ead8570a7 Небольшая поправка 2022-07-07 19:39:02 +07:00
Book Pauk
408315466b Частичный откат предыдущих изменений 2022-07-07 19:38:17 +07:00
Book Pauk
c651836554 Поправки скриптов запуска 2022-07-07 19:33:32 +07:00
Book Pauk
03a1e70fce Поправки, чтобы не падал в случае детача скрина 2022-07-07 19:05:54 +07:00
Book Pauk
ab5a11a24f Убрал сайт flibs.in из сетевых библиотек 2022-07-07 17:42:05 +07:00
Book Pauk
8cd6ed472c Изменил client_max_body_size 100m 2022-07-07 17:37:25 +07:00
Book Pauk
055181b744 Исправлен баг выпадающих списков в оглавлении 2022-07-07 17:34:03 +07:00
Book Pauk
e331a3920b Актуализация пакетов 2022-07-07 17:29:47 +07:00
Book Pauk
c62bccb470 Улучшил журналирование ошибок БД 2022-07-07 16:24:59 +07:00
Book Pauk
ea351ea293 Merge tag '0.11.6-1' into develop
0.11.6-1
2022-07-04 12:23:55 +07:00
Book Pauk
d806a07c60 Merge branch 'release/0.11.6-1' 2022-07-04 12:23:48 +07:00
Book Pauk
c0ea096f1f Обновил jembadb 2022-07-04 12:22:27 +07:00
Book Pauk
011d4a1672 Merge tag '0.11.6' into develop
0.11.6
2022-07-02 17:41:42 +07:00
Book Pauk
4836a737c6 Merge branch 'release/0.11.6' 2022-07-02 17:41:34 +07:00
Book Pauk
5712b2ee17 Версия 0.11.6 2022-07-02 17:40:28 +07:00
Book Pauk
32dd17694e Улучшено копирование текстов со страницы 2022-07-02 17:36:12 +07:00
Book Pauk
3ebc932a6a Поправил список расширений 2022-07-02 14:46:22 +07:00
Book Pauk
8f351d9bef Удалил неиспользуемый код 2022-07-02 14:18:16 +07:00
Book Pauk
5ae3ea94e4 Добавлены типы файлов в диалог загрузки 2022-07-02 13:57:44 +07:00
Book Pauk
f203d453a4 Актуализация пакетов 2022-07-02 13:21:30 +07:00
Book Pauk
0d5cba121b Мелкий рефакторинг 2022-07-02 13:02:22 +07:00
Book Pauk
0cd6a48a46 Актуализация пакетов 2022-07-02 12:59:07 +07:00
Book Pauk
4e07ce2b5c Актуализация пакетов 2022-07-02 12:55:39 +07:00
Book Pauk
85a525e301 Актуализация пакета base-x 2022-07-02 12:46:10 +07:00
Book Pauk
03e4a6d723 Мелкий рефакторинг 2022-07-02 12:36:59 +07:00
Book Pauk
ab28af1abe Актуализация пакетов 2022-07-02 12:16:52 +07:00
Book Pauk
7fceed5301 Переход на axios 2022-07-02 12:16:19 +07:00
Book Pauk
0077816afa Улучшена обработка и журналирование ошибок 2022-07-02 12:07:42 +07:00
Book Pauk
cb01423147 Поправил настройки прокси 2022-07-02 00:00:13 +07:00
Book Pauk
61b0712d36 Переход на axios 2022-07-01 21:38:32 +07:00
Book Pauk
12d7843377 Merge tag '0.11.5' into develop
0.11.5
2022-04-15 16:42:40 +07:00
Book Pauk
9293c0a0d4 Merge branch 'release/0.11.5' 2022-04-15 16:42:35 +07:00
Book Pauk
bb9522197a 0.11.5 2022-04-15 16:41:24 +07:00
Book Pauk
450a2e0664 Поправки css 2022-04-15 16:38:34 +07:00
Book Pauk
41e35f3ec8 Поправки css 2022-04-15 16:09:41 +07:00
Book Pauk
a9bc98abe3 Рефакторинг 2022-04-15 15:12:28 +07:00
Book Pauk
47bca03532 Поправки подсказок 2022-04-15 15:02:02 +07:00
Book Pauk
942021371c Merge tag '0.11.4-2' into develop
0.11.4-2
2022-04-14 19:54:20 +07:00
Book Pauk
ea2f178730 Merge branch 'release/0.11.4-2' 2022-04-14 19:54:14 +07:00
Book Pauk
4b5c8d9efe Добавил подсказку 2022-04-14 19:53:47 +07:00
Book Pauk
28ebf13c3a Merge tag '0.11.4-1' into develop
0.11.4-1
2022-04-14 19:19:21 +07:00
Book Pauk
5d52e63dd9 Merge branch 'release/0.11.4-1' 2022-04-14 19:19:14 +07:00
Book Pauk
1a0e024050 Поправил баг 2022-04-14 19:18:49 +07:00
Book Pauk
e627a0d970 Merge tag '0.11.4' into develop
0.11.4
2022-04-14 19:05:36 +07:00
Book Pauk
48668d94ad Merge branch 'release/0.11.4' 2022-04-14 19:05:31 +07:00
Book Pauk
e08c431dd9 Версия 0.11.4 2022-04-14 19:05:07 +07:00
Book Pauk
5ee58ad6f0 Поправка багов 2022-04-14 19:00:04 +07:00
Book Pauk
ac0a4f0586 Добавлена кнопка 'Управление кликом' 2022-04-14 18:50:11 +07:00
Book Pauk
b6f4c153e5 Добавлена кнопка 'Загрузить из буфера обмена' 2022-04-14 18:34:41 +07:00
Book Pauk
4fdaf5f555 Добавлена кнопка 'Загрузить файл с диска' 2022-04-14 17:48:51 +07:00
Book Pauk
b4ee9d6c00 Скрыта опция "Помочь проекту".
Добавлена кнопка "Вызвать справку".
2022-04-14 17:27:29 +07:00
Book Pauk
7c73c74730 Добавлена подсказка при невалидном URL книги 2022-04-14 17:13:38 +07:00
Book Pauk
c20aa089fa npm 2022-03-29 17:45:57 +07:00
Book Pauk
b0e15c22ea Merge tag '0.11.3' into develop
0.11.3
2022-03-29 17:41:03 +07:00
Book Pauk
d58a2c065a Merge branch 'release/0.11.3' 2022-03-29 17:40:57 +07:00
Book Pauk
53135e7ee8 Поправка даты 2022-03-29 17:40:29 +07:00
Book Pauk
5c48ca9e6c Рефакторинг versionHistory, небольшие поправки 2022-03-29 17:37:24 +07:00
Book Pauk
c4a280f3d8 Скрыл устаревший чекбокс 2022-03-29 16:52:03 +07:00
Book Pauk
ba2943c722 Поправлен баг 2022-03-29 16:49:04 +07:00
Book Pauk
26f6ffc83a Убрал PayPal из списка 2022-03-29 16:25:26 +07:00
Book Pauk
bcf075a72c Доработки WebSocketConnection 2022-03-29 16:23:34 +07:00
Book Pauk
02d458d192 Миграция "jembadb" => "^2.3.0" 2022-03-29 15:49:48 +07:00
Book Pauk
a349d8af68 Обновил пакет JembaDb 2022-02-08 20:55:31 +07:00
Book Pauk
0dbaf32aac Merge tag '0.11.2' into develop
0.11.2
2022-01-11 23:25:23 +07:00
Book Pauk
e8c41ef3a8 Merge branch 'release/0.11.2' 2022-01-11 23:24:58 +07:00
Book Pauk
e43a44e986 0.11.2 2022-01-11 23:24:37 +07:00
Book Pauk
f14b8ed277 Добавлена реакция на сигнал SIGUSR2 2022-01-11 23:23:54 +07:00
Book Pauk
bbfe8a64cb Мелкая поправка 2022-01-11 23:11:04 +07:00
Book Pauk
bcf3c2dab0 Улучшение обработки ошибок 2022-01-11 22:23:35 +07:00
Book Pauk
d5404fd260 Убрал устаревший код 2022-01-11 21:30:43 +07:00
Book Pauk
54bc662e43 Поправил конфиг для nginx 2021-12-24 17:59:26 +07:00
Book Pauk
42546ca97e Обновление jembadb до версии 1.3.0 2021-12-21 20:21:32 +07:00
Book Pauk
5c13cf0eb9 Добавил -C GZip для pkg 2021-12-20 17:27:04 +07:00
Book Pauk
2a9d44ae9a Поправка конфига для eslint 2021-12-20 17:26:19 +07:00
Book Pauk
38414ae7b6 Переход на пакет jembadb 2021-12-17 20:05:57 +07:00
Book Pauk
3ecb3e80ac Удалил комментарии 2021-12-12 01:56:24 +07:00
Book Pauk
4968828488 Merge tag '0.11.1-2' into develop
0.11.1-2
2021-12-03 15:25:17 +07:00
Book Pauk
4db3cd24df Merge branch 'release/0.11.1-2' 2021-12-03 15:25:11 +07:00
Book Pauk
45c6d3da77 Поправил таймаут, улучшение скорости синхронизации 2021-12-03 15:16:39 +07:00
Book Pauk
4aab1da3c6 Merge tag '0.11.1-1' into develop
0.11.1-1
2021-12-03 15:03:46 +07:00
Book Pauk
bf5dfa1c15 Merge branch 'release/0.11.1-1' 2021-12-03 15:03:37 +07:00
Book Pauk
7549bdd2b4 Обновил pkg 2021-12-03 15:02:56 +07:00
Book Pauk
1bb2525ab2 Merge tag '0.11.1' into develop
0.11.1
2021-12-03 14:35:04 +07:00
Book Pauk
22a556f612 Merge branch 'release/0.11.1' 2021-12-03 14:34:56 +07:00
Book Pauk
056611e87c Версия 0.11.1 2021-12-03 14:34:36 +07:00
Book Pauk
6debe24880 Удален более ненужный файл 2021-12-03 14:30:57 +07:00
Book Pauk
56559bddab Мелкий рефакторинг 2021-12-03 14:28:17 +07:00
Book Pauk
9ec74eccb4 Добавлен папаметр forceAutoRepair 2021-12-03 14:21:50 +07:00
Book Pauk
3d2f45c20d Мелие поправки 2021-12-03 14:21:36 +07:00
Book Pauk
fb2eedd5ba Добавлен конвертер SQLITE -> JambaDb 2021-12-03 14:07:32 +07:00
Book Pauk
e278b4a00e Мелкие поправки 2021-12-02 18:39:28 +07:00
Book Pauk
0beaa611f6 Переход на JembaDb 2021-12-02 18:36:49 +07:00
Book Pauk
14ca2daa39 Небольшой рефакторинг 2021-12-01 22:09:48 +07:00
Book Pauk
714eb3ae83 Поправки по результату тестирования 2021-12-01 21:26:26 +07:00
Book Pauk
6286d663c9 Поправлен баг 2021-12-01 19:27:16 +07:00
Book Pauk
b5db2079d2 Jemba-миграции 2021-12-01 17:50:48 +07:00
Book Pauk
b3b30b9bd9 Поправил триггер для autorepair 2021-11-24 15:15:22 +07:00
Book Pauk
0b6a726503 Новый движок БД 2021-11-24 14:15:09 +07:00
Book Pauk
609334c5a6 Пометил модули устаревшими 2021-11-24 14:14:24 +07:00
Book Pauk
4852c7aec3 Добавлен модуль AsyncExit для выполненния cleanup-процедур перед выходом из приложения 2021-11-24 14:13:13 +07:00
Book Pauk
b1e3d33694 Merge tag '0.11.0-1' into develop
0.11.0-1
2021-11-22 21:12:42 +07:00
Book Pauk
2bfc557071 Merge branch 'release/0.11.0-1' 2021-11-22 21:12:35 +07:00
Book Pauk
e1216109bc Поправлен баг с maxBodyLength клиента WebDav 2021-11-22 21:12:02 +07:00
Book Pauk
990b8f390c Merge tag '0.11.0' into develop
0.11.0
2021-11-18 18:43:53 +07:00
Book Pauk
e6f6cd4ff3 Merge branch 'release/0.11.0' 2021-11-18 18:43:45 +07:00
Book Pauk
7deb745651 Поправил ссылку на инструкцию certbot 2021-11-18 18:28:25 +07:00
Book Pauk
70f3ca8067 Добавил beta-конфиг nginx для http 2021-11-18 18:25:04 +07:00
Book Pauk
bb8497a997 Поправил ошибку в доке 2021-11-18 18:22:08 +07:00
Book Pauk
2127e2ec0a Версия 0.11.0 2021-11-18 18:17:22 +07:00
Book Pauk
9be4011d54 Небольшая поправка формирования заголовка 2021-11-18 17:56:54 +07:00
Book Pauk
c534edfeb5 Поправки 2021-11-16 15:41:51 +07:00
Book Pauk
adc8cd7243 Переход на Vue 3 2021-11-16 15:05:00 +07:00
Book Pauk
522d2d3b9c Актуализация пакетов 2021-11-16 14:41:53 +07:00
Book Pauk
046933a05e Поправка багов 2021-11-16 14:32:54 +07:00
Book Pauk
9143288de2 Поправка копирования assets 2021-11-16 14:03:21 +07:00
Book Pauk
6053ca6c0e Настройка правильных редиректов роутера 2021-11-07 15:38:17 +07:00
Book Pauk
084197530e Форматирование кода 2021-11-07 15:38:05 +07:00
Book Pauk
9f366ca811 Поправлен баг resize 2021-11-07 14:49:33 +07:00
Book Pauk
7c07e6f004 Поправка бага 2021-11-01 19:08:17 +07:00
Book Pauk
3d4d7e0342 Переход на Vue 3 2021-11-01 18:23:58 +07:00
Book Pauk
1a8f241aad Переход на Vue 3, небольшая реструктуризация файлов 2021-11-01 17:56:45 +07:00
Book Pauk
33e938b76a Поправлен баг 2021-10-31 21:51:03 +07:00
Book Pauk
e2db546066 Переход на Vue 3 2021-10-31 21:28:31 +07:00
Book Pauk
def9ee52e2 Поправка разметки 2021-10-31 13:19:11 +07:00
Book Pauk
1afe10be03 Переход на Vue 3 2021-10-31 13:14:12 +07:00
Book Pauk
fa44641fa2 Актуализация пакетов 2021-10-31 13:00:41 +07:00
Book Pauk
9a1ef85c93 Переход на Vue 3 2021-10-29 19:11:10 +07:00
Book Pauk
b848cf5aa7 Переход на Vue 3 2021-10-29 18:24:23 +07:00
Book Pauk
8057e18ebc Переход на Vue 3 2021-10-29 16:33:38 +07:00
Book Pauk
76e09ef34e Переход на Vue 3, в процессе 2021-10-29 15:27:04 +07:00
Book Pauk
00cb2dc274 Переход на Vue 3, в процессе 2021-10-29 12:56:28 +07:00
Book Pauk
ed46e91432 Переход на Vue 3, в процессе 2021-10-29 12:21:53 +07:00
Book Pauk
88d75fb0d8 Переход на Vue 3, в процессе 2021-10-28 16:55:44 +07:00
Book Pauk
a1d7a73459 Переход на Vue 3, в процессе 2021-10-28 15:17:19 +07:00
Book Pauk
687f89729b Переход на Vue 3, в процессе 2021-10-28 14:53:22 +07:00
Book Pauk
6bf678e01f Функция для преобразования Vue-класса во Vue-компонент 2021-10-28 13:52:25 +07:00
Book Pauk
a18aec2f96 Переход на Vue 3 - начало, пока ничего не работает 2021-10-27 23:09:20 +07:00
Book Pauk
1c0cf303a0 Поправка настроек eslint 2021-10-27 16:11:20 +07:00
Book Pauk
5c7ae73982 Поправки по требованиям eslint 2021-10-27 15:07:25 +07:00
Book Pauk
4e9c69a1cf Настройка eslint 2021-10-27 15:05:37 +07:00
Book Pauk
78375be8bf Поправки по требованиям eslint 2021-10-27 15:05:18 +07:00
Book Pauk
b684725094 Настройка eslint 2021-10-27 04:11:07 +07:00
Book Pauk
ff52602c3a Мелкие поправки 2021-10-27 04:10:19 +07:00
Book Pauk
ce704c5e26 Актуализация пакетов 2021-10-27 01:28:16 +07:00
Book Pauk
4503e4ed17 Актуализация пакетов 2021-10-27 01:03:47 +07:00
Book Pauk
01c384c43a Актуализация пакетов 2021-10-27 00:52:57 +07:00
Book Pauk
dda2de58a8 Актуализация пакетов, в процессе 2021-10-27 00:06:42 +07:00
Book Pauk
0365acbf7a Актуализация пакетов 2021-10-26 01:05:09 +07:00
Book Pauk
bbf1ab7180 Актуализация пакетов 2021-10-26 00:35:45 +07:00
Book Pauk
83bf1f1d3a Актуализация пакетов 2021-10-26 00:18:18 +07:00
Book Pauk
fdf04fed0e Актуализация пакетов 2021-10-25 23:55:26 +07:00
Book Pauk
acce32bfa7 Актуализация пакетов 2021-10-25 16:15:15 +07:00
Book Pauk
614c45ac7d Актуализировал readme 2021-10-25 15:31:07 +07:00
Book Pauk
c4c0199a1b Merge tag '0.10.3' into develop
0.10.3
2021-10-25 01:54:07 +07:00
Book Pauk
a53ebb9355 Merge branch 'release/0.10.3' 2021-10-25 01:54:00 +07:00
Book Pauk
06e12930c7 Актуализирован конвертер для samlib.ru 2021-10-25 01:01:26 +07:00
Book Pauk
0f7655773a Версия 0.10.3 2021-10-20 18:29:52 +07:00
Book Pauk
26660461d4 Исправлен баг парсера с пустыми параграфами (содержащими только разметку) 2021-10-20 18:05:38 +07:00
Book Pauk
b41ee91db5 Актуализировал инструкцию 2021-10-20 15:49:07 +07:00
Book Pauk
746dd8d37a Актуализация инструкции 2021-10-20 15:42:09 +07:00
Book Pauk
fb4a57027d Merge tag '0.10.2' into develop
0.10.2
2021-10-19 23:33:48 +07:00
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
Book Pauk
4cf5a0f4c8 Merge branch 'release/0.9.8-2' 2020-11-14 12:01:39 +07:00
Book Pauk
8f0d526af2 Скрыл хоткей для "Библиотека" в режиме omnireader 2020-11-14 12:00:55 +07:00
Book Pauk
6ca3881841 Merge tag '0.9.8-1' into develop
0.9.8-1
2020-11-14 11:49:06 +07:00
Book Pauk
d8e765a04f Merge branch 'release/0.9.8-1' 2020-11-14 11:49:02 +07:00
Book Pauk
40ff572f94 Добавил проксирование для fantasy-worlds.org 2020-11-14 11:48:03 +07:00
Book Pauk
cc4275dc03 Merge tag '0.9.8' into develop
0.9.8
2020-11-14 00:05:14 +07:00
Book Pauk
b387f4a0db Merge branch 'release/0.9.8' 2020-11-14 00:05:07 +07:00
Book Pauk
1a096031c4 Версия 0.9.8 2020-11-14 00:04:30 +07:00
Book Pauk
83a60b4091 Небольшие поправки парсера 2020-11-13 23:59:55 +07:00
Book Pauk
b292407ec2 Работа над ContentsPage 2020-11-13 23:33:36 +07:00
Book Pauk
952c337b76 Работа над ContentsPage 2020-11-13 22:45:34 +07:00
Book Pauk
e947b887fe Работа над ContentsPage 2020-11-13 19:10:15 +07:00
Book Pauk
bd1e5485d7 Работа над ContentsPage 2020-11-13 18:47:11 +07:00
Book Pauk
e095c3318b Начало работы над ContentsPage 2020-11-13 14:23:55 +07:00
Book Pauk
d75a08b519 Merge tag '0.9.7' into develop
0.9.7
2020-11-12 19:44:15 +07:00
Book Pauk
d55a616fe0 Merge branch 'release/0.9.7' 2020-11-12 19:44:08 +07:00
Book Pauk
2146cb3576 Версия 0.9.7 2020-11-12 19:43:33 +07:00
Book Pauk
ae260e74f6 Поправка механизма чистки TempPublicDir 2020-11-12 19:39:57 +07:00
Book Pauk
355410c03c Исправление бага - не изменялись хоткеи в настройках 2020-11-12 19:13:24 +07:00
Book Pauk
718ad51fac Merge tag '0.9.6-1' into develop
0.9.6-1
2020-11-07 20:57:02 +07:00
Book Pauk
4242a8679f Merge branch 'release/0.9.6-1' 2020-11-07 20:56:57 +07:00
Book Pauk
4ff9ff699b Добавил ссылку на flibs.in в предустановленные закладки 2020-11-07 20:56:07 +07:00
Book Pauk
7a76673274 Merge tag '0.9.6' into develop
0.9.6
2020-11-06 18:00:31 +07:00
Book Pauk
bd03ca5136 Merge branch 'release/0.9.6' 2020-11-06 18:00:22 +07:00
Book Pauk
b3e1e4b909 Версия 0.9.6 2020-11-06 17:59:54 +07:00
Book Pauk
4bb22183df Добавлен диалог с сообщением о новой читалке 2020-11-06 17:57:16 +07:00
Book Pauk
e72f8f4245 Мелкий рефакторинг 2020-11-06 17:36:05 +07:00
Book Pauk
5b52f48bce Поправка для автоматического отображения справки 2020-11-06 16:50:17 +07:00
Book Pauk
f07a157a2a Поправка бага при resize 2020-11-06 01:08:22 +07:00
Book Pauk
ca40854106 Рефакторинг, версия 0.9.6 2020-11-05 22:21:52 +07:00
Book Pauk
d6a8209b31 Работа над ExternalLibs 2020-11-05 21:52:34 +07:00
Book Pauk
731e1f1f15 Мелкая поправка 2020-11-05 20:18:23 +07:00
Book Pauk
b4a2a8fb98 Merge tag '0.9.5-2' into develop
0.9.5-2
2020-11-05 16:58:53 +07:00
Book Pauk
5a3e4ee5ca Merge branch 'release/0.9.5-2' 2020-11-05 16:58:37 +07:00
Book Pauk
ab2cf0aeec Работа над ExternalLibs 2020-11-05 16:56:27 +07:00
Book Pauk
9de6a02b30 Поправка безопасности 2020-11-05 16:42:00 +07:00
Book Pauk
9fb7892bfe Работа над ExternalLibs 2020-11-05 16:40:33 +07:00
Book Pauk
546f4556f6 Мелкий рефакторинг 2020-11-05 15:43:15 +07:00
Book Pauk
471de104bc Работа над ExternalLibs 2020-11-05 15:36:58 +07:00
Book Pauk
d30be1536d Работа над BookmarkSettings 2020-11-05 15:06:36 +07:00
Book Pauk
6c0678ed61 Рефакторинг 2020-11-05 14:08:15 +07:00
Book Pauk
4883b8a190 Работа над ExternalLibs 2020-11-05 03:21:54 +07:00
Book Pauk
14742ed4ad Работа над BookmarkSettings 2020-11-04 22:56:28 +07:00
Book Pauk
1d8bd56862 Поправки багов 2020-11-04 22:23:39 +07:00
Book Pauk
94b8f9fe1c Работа над BookmarkSettings 2020-11-04 19:06:11 +07:00
Book Pauk
27412211a5 Работа над BookmarkSettings 2020-11-04 18:53:14 +07:00
Book Pauk
f8c4960079 Работа над BookmarkSettings 2020-11-04 14:43:06 +07:00
Book Pauk
b2e0bcf995 Улучшение безопасности отображения списка 2020-11-04 14:42:35 +07:00
Book Pauk
fcf6639d38 Небольшие поправки обработки клавиш 2020-11-04 14:41:38 +07:00
Book Pauk
d540cb91a9 Добавлен пакет vue-sanitize 2020-11-04 14:41:11 +07:00
Book Pauk
f69cc6f1b1 Работа над BookmarkSettings 2020-11-03 23:04:06 +07:00
Book Pauk
607f2ff407 Работа над BookmarkSettings 2020-11-03 22:24:32 +07:00
Book Pauk
ba6bf8c091 Работа над BookmarkSettings 2020-11-03 17:03:57 +07:00
Book Pauk
7e4c938dfd Поправлен небольшой баг отображения окна 2020-11-03 14:42:02 +07:00
Book Pauk
7f36d55320 Мелкая поправка 2020-11-03 14:41:32 +07:00
Book Pauk
d9634a134c Обновление caniuse -lite 2020-11-03 00:35:44 +07:00
Book Pauk
4f8868d4b1 Merge tag '0.9.5-1' into develop
0.9.5-1
2020-11-03 00:29:22 +07:00
Book Pauk
956546585c Merge branch 'release/0.9.5-1' 2020-11-03 00:29:12 +07:00
Book Pauk
3ca0a92442 Временно отключил кнопку "Настроить закладки" 2020-11-03 00:28:15 +07:00
Book Pauk
213f7e48c9 Работа над BookmarkSettings 2020-11-03 00:26:25 +07:00
Book Pauk
8b66fd522d Добавлено восстановление дефолтных настроек в объекте settings при их отсутствии 2020-11-03 00:00:31 +07:00
Book Pauk
fdf5009999 Мелкая поправка 2020-11-02 23:51:41 +07:00
Book Pauk
bbdba0ef16 Поправки по результату тестирования 2020-11-02 23:45:59 +07:00
Book Pauk
a63602df7a Поправки багов 2020-11-02 14:33:15 +07:00
Book Pauk
587120f984 Merge tag '0.9.5' into develop
0.9.5
2020-11-01 19:55:53 +07:00
Book Pauk
e72ca0de7e Merge branch 'release/0.9.5' 2020-11-01 19:55:45 +07:00
Book Pauk
c44c27d3d2 Версия 0.9.5 2020-11-01 19:36:13 +07:00
Book Pauk
df4e201ccd Небольшие поправки 2020-11-01 19:33:02 +07:00
Book Pauk
c8c0e9ec1a Добавлена кнопка 'Обновить с разбиением на параграфы' 2020-11-01 16:42:20 +07:00
Book Pauk
9a4a84a367 Сертификаты для beta.liberama 2020-11-01 15:00:23 +07:00
Book Pauk
1dc3424411 Убрал лишнее 2020-11-01 14:53:51 +07:00
Book Pauk
c13745e913 Добавлен конфиг nginx для beta.liberama 2020-11-01 14:50:18 +07:00
Book Pauk
25c12309f2 Поправки поведения диалога на "Enter" 2020-11-01 14:41:40 +07:00
Book Pauk
4b632da5af Поправки поведения Select 2020-11-01 14:17:35 +07:00
Book Pauk
87c364b8ee Исправление поведения компонента select 2020-11-01 13:38:05 +07:00
Book Pauk
efa48fbc8a Поправки margin окна 2020-11-01 11:42:54 +07:00
Book Pauk
21df6c1d21 Поправка goToLink 2020-11-01 11:34:51 +07:00
Book Pauk
39d2ceb94b Мелкая поправка для fullscreen 2020-11-01 11:26:58 +07:00
Book Pauk
1dad013d60 Исправлен баг синхронизации при первом включении опции 2020-11-01 11:23:15 +07:00
Book Pauk
add7a03f88 Убираем ссылку на гитхаб для liberama.top 2020-10-31 21:20:30 +07:00
Book Pauk
0cefaa6d48 Мелкая поправка 2020-10-30 19:16:06 +07:00
Book Pauk
f08e73f359 Merge tag '0.9.4' into develop
Версия 0.9.4
2020-10-30 19:13:17 +07:00
Book Pauk
cf9ce26438 Merge branch 'release/0.9.4' 2020-10-30 19:13:00 +07:00
Book Pauk
fd74a5a82e 0.9.4 2020-10-30 19:12:49 +07:00
Book Pauk
3109104928 Замена event.code на event.key 2020-10-30 19:07:33 +07:00
Book Pauk
b1ec4df2e4 Вернул обратно хук на keydown 2020-10-30 18:15:12 +07:00
Book Pauk
1609e149a8 Поправлен конфиг 2020-10-30 17:01:58 +07:00
Book Pauk
298c483d0e Исправления скриптов запуска сервера для крона 2020-10-30 16:53:54 +07:00
Book Pauk
dc917b75b1 Убрал дебаг 2020-10-30 16:11:02 +07:00
Book Pauk
ad32bdab44 Исправление проблемы нескольких одновременных попыток подключения по WebSocket 2020-10-30 16:02:09 +07:00
Book Pauk
5e8b2e1c87 Отладка 2020-10-30 12:22:54 +07:00
Book Pauk
08b4afd287 Libs добавлены в синхронизацию 2020-10-30 12:01:22 +07:00
Book Pauk
0de31f643b Мелкая поправка 2020-10-30 11:35:41 +07:00
Book Pauk
5e815eb3c4 Мелкий рефакторинг 2020-10-30 11:30:21 +07:00
Book Pauk
48c93a2120 Небольшие поправки 2020-10-30 11:23:18 +07:00
Book Pauk
ad5de42172 Небольшие поправки 2020-10-30 11:13:09 +07:00
Book Pauk
32bafedaad Небольшие доработки 2020-10-30 00:10:42 +07:00
Book Pauk
c67fd11be9 Небольшие доделки 2020-10-30 00:00:05 +07:00
Book Pauk
8b59c72848 Небольшие доработки 2020-10-29 23:39:13 +07:00
Book Pauk
c35b2f3bfc Работа над LibsPage 2020-10-29 23:29:10 +07:00
Book Pauk
ac63ad4612 Версия 0.9.4 2020-10-29 23:28:42 +07:00
Book Pauk
e1dea1c752 Версия 0.9.4 2020-10-29 23:28:17 +07:00
Book Pauk
25648e2327 Работа над LibsPage 2020-10-29 23:05:26 +07:00
Book Pauk
18ac04bb0f Работа над ExternalLibs 2020-10-29 22:54:02 +07:00
Book Pauk
5263ee58b2 Работа над ExternalLibs 2020-10-29 21:09:23 +07:00
Book Pauk
af542b89f7 Работа над LibsPage 2020-10-29 16:31:09 +07:00
Book Pauk
684b675fca Поправка конфига 2020-10-29 15:40:32 +07:00
Book Pauk
c29044eca1 Работа над LibsPage 2020-10-29 15:35:09 +07:00
Book Pauk
a36510fcc8 Мелкая поправка 2020-10-29 15:27:28 +07:00
Book Pauk
bc21ace416 Работа над LibsPage 2020-10-29 15:14:27 +07:00
Book Pauk
57e521e2ff Работа над LibsPage 2020-10-29 14:45:10 +07:00
Book Pauk
ac6ebb9e8d Настройки перенаправления 2020-10-28 23:41:15 +07:00
Book Pauk
54bef54635 Поправки конфига nginx 2020-10-28 22:47:30 +07:00
Book Pauk
593e201f79 Работа над LibsPage (ExternalLibs) 2020-10-28 22:45:05 +07:00
Book Pauk
d7b24253fe Работа над LibsPage 2020-10-28 22:04:47 +07:00
Book Pauk
33961abd86 Поправлен баг редиректа 2020-10-28 21:06:07 +07:00
Book Pauk
37e0e1d42f Добавки для правильной работы с nginx 2020-10-28 20:42:47 +07:00
Book Pauk
1121f9c918 Работа над LibsPage 2020-10-28 20:30:57 +07:00
Book Pauk
582203f5da Работа над LibsPage (ExternalLibs) 2020-10-28 20:14:42 +07:00
Book Pauk
8c0f193738 Исправлен баг редиректа 2020-10-28 18:28:28 +07:00
Book Pauk
ebe42956ad Все компоненты загружаются динамически 2020-10-28 18:27:24 +07:00
Book Pauk
b8f8df8927 Поправил настройки nginx 2020-10-28 11:19:31 +07:00
Book Pauk
2c66ca4fdd Поправки конфига nginx 2020-10-27 23:46:44 +07:00
Book Pauk
49f813e880 Работа над LibsPage 2020-10-27 23:10:45 +07:00
Book Pauk
da6fed80d1 Работа над LibsPage 2020-10-27 23:02:28 +07:00
Book Pauk
b901d9b8c9 Убрал эксперимент 2020-10-27 17:45:08 +07:00
Book Pauk
b41d46ac57 Эксперимент 2020-10-27 17:40:56 +07:00
Book Pauk
4f0189f3e0 Эксперимент 2020-10-27 16:24:59 +07:00
Book Pauk
c956e7a802 Убрал дебаг 2020-10-27 14:34:12 +07:00
Book Pauk
dcbc8409e0 Работа над LibsPage 2020-10-27 14:31:07 +07:00
Book Pauk
fd58568cf0 Работа над LibsPage 2020-10-27 14:16:30 +07:00
Book Pauk
0f81fa53d2 Работа над LibsPage 2020-10-27 13:30:05 +07:00
Book Pauk
44655dc81c Quasar update 2020-10-27 13:28:54 +07:00
Book Pauk
749667aefd Работа над LibsPage 2020-10-27 11:17:49 +07:00
Book Pauk
dd94418c26 Процесс добавления нового окна: "Библиотеки" 2020-10-26 00:23:30 +07:00
Book Pauk
55a5375e46 Скрываем ссылки "Отзывы о читалке" и "Старая версия" в режиме "liberama.top" 2020-10-25 23:36:02 +07:00
Book Pauk
df76de7352 Поправки для нового сайта liberama.top 2020-10-25 18:35:42 +07:00
Book Pauk
1fb1a1b2b1 Временная настройка liberama.top на beta-среду 2020-10-25 18:08:04 +07:00
Book Pauk
f998edb2aa Добавлены сертификаты для ssl 2020-10-25 17:46:14 +07:00
Book Pauk
7c2cb9a0c7 Поправка дока в связи с переименованиями 2020-10-25 17:39:26 +07:00
Book Pauk
0690a365da Добавлена настройка прокси для flibusta.is 2020-10-25 17:35:53 +07:00
Book Pauk
a20d05aba8 Переименования в доках, добавлены настройки nginx для сайта liberama.top 2020-10-25 17:30:34 +07:00
Book Pauk
4362ae95ba Поправки по результату тестирования 2020-10-24 17:25:36 +07:00
Book Pauk
d658814399 Поправки по результатам тестирования 2020-10-24 13:11:17 +07:00
Book Pauk
39e14d70ee Исправление бага распаковки Zip-архивов с плохими именами файлов 2020-10-13 16:24:35 +07:00
Book Pauk
2e58cfdb75 Исправлен мелкий баг PageDown 2020-09-18 18:05:32 +07:00
Book Pauk
fcaa724c00 Убрал ya-share 2020-09-18 17:45:27 +07:00
Book Pauk
8806b4141e Поправки истории версий 2020-09-18 17:43:05 +07:00
Book Pauk
7bd159766b Исправление бага исчезновения картинок при включенной настройке "Убирать пустые строки" 2020-09-18 17:40:37 +07:00
Book Pauk
4df15d603f Merge tag '0.9.3-2' into develop
0.9.3-2
2020-06-30 16:54:31 +07:00
Book Pauk
b453c3efe5 Merge branch 'release/0.9.3-2' 2020-06-30 16:54:25 +07:00
Book Pauk
56590ef8a4 Исправление бага при определении пустых параграфов 2020-06-30 16:51:43 +07:00
Book Pauk
7c133136b9 Merge tag '0.9.3-1' into develop
0.9.3-1
2020-06-04 19:13:23 +07:00
Book Pauk
41881639aa Merge branch 'release/0.9.3-1' 2020-06-04 19:13:15 +07:00
Book Pauk
416003f078 Поправки сигнатур файлов 2020-06-04 19:12:05 +07:00
Book Pauk
bbfcd0efa3 Merge tag '0.9.3' into develop
0.9.3
2020-05-21 05:04:37 +07:00
Book Pauk
150e4332c3 Merge branch 'release/0.9.3' 2020-05-21 05:04:17 +07:00
Book Pauk
49649765c7 Версия 0.9.3 2020-05-21 05:03:08 +07:00
Book Pauk
726b7bfa93 Вернул обратно 2020-05-21 05:02:10 +07:00
Book Pauk
265f838868 Эксперимент 2020-05-21 04:58:32 +07:00
Book Pauk
6e2e5b5520 Вернул обратно 2020-05-21 04:54:39 +07:00
Book Pauk
100ea2f64a Эксперимент 2020-05-21 04:50:16 +07:00
Book Pauk
4e7ed1ee33 Поправки Content-Type 2020-05-21 04:36:03 +07:00
Book Pauk
8ab6aed1aa Поправлен баг 2020-05-21 04:04:57 +07:00
Book Pauk
4ff096014c Эксперимент 2020-05-21 02:56:44 +07:00
Book Pauk
03b60b6ca9 Эксперимент 2020-05-21 02:24:53 +07:00
Book Pauk
e30b832e05 Эксперимент 2020-05-21 02:06:17 +07:00
Book Pauk
e646de85a7 Поправка настроек SW 2020-05-21 01:51:47 +07:00
Book Pauk
70a7a0e344 Замена sw-precache-webpack-plugin на workbox-webpack-plugin 2020-05-21 01:31:39 +07:00
Book Pauk
b444abeb3e Исправлен баг перехвата клавиш в диалогах 2020-05-21 00:14:47 +07:00
Book Pauk
c72f56917d Quasar upgrade 2020-05-21 00:06:20 +07:00
Book Pauk
192283d6b2 Добавил gitignore 2020-05-20 23:13:08 +07:00
Book Pauk
6be6fa1966 Merge tag '0.9.2-2' into develop
0.9.2-2
2020-05-01 14:33:54 +07:00
Book Pauk
510553b055 Merge branch 'release/0.9.2-2' 2020-05-01 14:33:45 +07:00
Book Pauk
6c4616892e Поправлен баг распознавания html 2020-05-01 14:32:31 +07:00
Book Pauk
1e79a099b8 Merge tag '0.9.2-1' into develop
0.9.2-1
2020-04-15 16:37:17 +07:00
Book Pauk
31a22327f1 Merge branch 'release/0.9.2-1' 2020-04-15 16:37:10 +07:00
Book Pauk
c1712bebc6 0.9.2-1 2020-04-15 16:36:29 +07:00
Book Pauk
cd91541245 Исправлен баг "Не удалось определить формат файла" при загрузке html-страниц 2020-04-15 16:35:05 +07:00
Book Pauk
4c1fc83256 Merge tag '0.9.2' into develop
0.9.2
2020-04-15 15:37:59 +07:00
178 changed files with 26666 additions and 14931 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": {
"parser": "babel-eslint"
"parser": "@babel/eslint-parser",
"sourceType": "module"
},
"extends": [
"eslint:recommended",
"plugin:vue/essential"
"plugin:vue/recommended"
],
"plugins": [
"vue",
"html",
"node"
"@babel"
],
"env": {
"es6": true,
"browser": true,
"node": true
},
@@ -24,6 +25,14 @@
"LM_TOTAL": false
},
"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,
"indent": [0, 4, {
"SwitchCase": 1

View File

@@ -2,16 +2,16 @@
Браузерная онлайн-читалка книг и децентрализованная библиотека.
Читалка ![](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/reader.jpg)
## VPS
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader](docs/omnireader/README.md)
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
## Сборка проекта
Необходима версия node.js не ниже 10.
Необходима версия node.js не ниже 14.
```
$ git clone https://github.com/bookpauk/liberama

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
const path = require('path');
//const webpack = require('webpack');
const merge = require('webpack-merge');
const { merge } = require('webpack-merge');
const baseWpConfig = require('./webpack.base.config');
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 CleanWebpackPlugin = require('clean-webpack-plugin');
//const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const {GenerateSW} = require('workbox-webpack-plugin');
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
const clientDir = path.resolve(__dirname, '../client');
@@ -34,19 +34,18 @@ module.exports = merge(baseWpConfig, {
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
terserOptions: {
output: {
format: {
comments: false,
},
},
}),
new OptimizeCSSAssetsPlugin()
new CssMinimizerWebpackPlugin()
]
},
plugins: [
new CleanWebpackPlugin([publicDir], {root: path.resolve(__dirname, '..')}),
//new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css"
}),
@@ -54,13 +53,15 @@ module.exports = merge(baseWpConfig, {
template: `${clientDir}/index.html.template`,
filename: `${publicDir}/index.html`
}),
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
new SWPrecacheWebpackPlugin({
new CopyWebpackPlugin({patterns:
[{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
}),
new GenerateSW({
cacheId: 'liberama',
filepath: `${publicDir}/service-worker.js`,
minify: true,
swDest: `${publicDir}/service-worker.js`,
navigateFallback: '/index.html',
stripPrefix: publicDir,
navigateFallbackDenylist: [new RegExp('^/api'), new RegExp('^/ws'), new RegExp('^/tmp'),],
skipWaiting: true,
}),
]
});

View File

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

View File

@@ -9,12 +9,11 @@ class Misc {
async loadConfig() {
const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
]};
try {
await wsc.open();
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error)
throw new Error(config.error);
return config;

View File

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

View File

@@ -1,172 +1,3 @@
const cleanPeriod = 60*1000;//1 минута
class WebSocketConnection {
//messageLifeTime в минутах (cleanPeriod)
constructor(messageLifeTime = 5) {
this.ws = null;
this.timer = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTime;
this.requestId = 0;
}
addListener(listener) {
if (this.listeners.indexOf(listener) < 0)
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
}
//рассылаем сообщение и удаляем те обработчики, которые его получили
emit(mes, isError) {
const len = this.listeners.length;
if (len > 0) {
let newListeners = [];
for (const listener of this.listeners) {
let emitted = false;
if (isError) {
if (listener.onError)
listener.onError(mes);
emitted = true;
} else {
if (listener.onMessage) {
if (listener.requestId) {
if (listener.requestId === mes.requestId) {
listener.onMessage(mes);
emitted = true;
}
} else {
listener.onMessage(mes);
emitted = true;
}
} else {
emitted = true;
}
}
if (!emitted)
newListeners.push(listener);
}
this.listeners = newListeners;
}
return this.listeners.length != len;
}
open(url) {
return new Promise((resolve, reject) => {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
let protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:'
}
url = url || `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(url);
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
let resolved = false;
this.ws.onopen = (e) => {
resolved = true;
resolve(e);
};
this.ws.onmessage = (e) => {
try {
const mes = JSON.parse(e.data);
this.messageQueue.push({regTime: Date.now(), mes});
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (!this.emit(message.mes)) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} catch (e) {
this.emit(e.message, true);
}
};
this.ws.onerror = (e) => {
this.emit(e.message, true);
if (!resolved)
reject(e);
};
}
});
}
//timeout в минутах (cleanPeriod)
message(requestId, timeout = 2) {
return new Promise((resolve, reject) => {
this.addListener({
requestId,
timeout,
onMessage: (mes) => {
resolve(mes);
},
onError: (e) => {
reject(e);
}
});
});
}
send(req) {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
const requestId = ++this.requestId;
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
return requestId;
} else {
throw new Error('WebSocket connection is not ready');
}
}
close() {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
this.ws.close();
}
}
periodicClean() {
try {
this.timer = null;
const now = Date.now();
//чистка listeners
let newListeners = [];
for (const listener of this.listeners) {
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
newListeners.push(listener);
} else {
if (listener.onError)
listener.onError('Время ожидания ответа истекло');
}
}
this.listeners = newListeners;
//чистка messageQueue
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} finally {
if (this.ws.readyState == WebSocket.OPEN) {
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
}
}
import WebSocketConnection from '../../server/core/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,74 +1,28 @@
<template>
<!--q-layout view="lhr lpr lfr">
<q-drawer v-model="showAsideBar" :width="asideWidth">
<div class="app-name"><span v-html="appName"></span></div>
<q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
<q-list>
<q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="inbox" />
</q-item-section>
<q-item-section>Inbox</q-item-section>
</q-item>
</q-list-->
<!--el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
<el-menu-item index="/cardindex">
<i class="el-icon-search"></i>
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
</el-menu-item>
<el-menu-item index="/reader">
<i class="el-icon-tickets"></i>
<span :class="itemTitleClass('/reader')" slot="title">{{ this.itemRuText['/reader'] }}</span>
</el-menu-item>
<el-menu-item index="/forum" disabled>
<i class="el-icon-message"></i>
<span :class="itemTitleClass('/forum')" slot="title">{{ this.itemRuText['/forum'] }}</span>
</el-menu-item>
<el-menu-item index="/income">
<i class="el-icon-upload"></i>
<span :class="itemTitleClass('/income')" slot="title">{{ this.itemRuText['/income'] }}</span>
</el-menu-item>
<el-menu-item index="/sources">
<i class="el-icon-menu"></i>
<span :class="itemTitleClass('/sources')" slot="title">{{ this.itemRuText['/sources'] }}</span>
</el-menu-item>
<el-menu-item index="/settings">
<i class="el-icon-setting"></i>
<span :class="itemTitleClass('/settings')" slot="title">{{ this.itemRuText['/settings'] }}</span>
</el-menu-item>
<el-menu-item index="/help">
<i class="el-icon-question"></i>
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
</el-menu-item>
</el-menu-->
<!--/q-drawer>
<q-page-container>
<keep-alive>
<router-view></router-view>
</keep-alive>
</q-page-container>
</q-layout-->
<div class="fit row">
<Notify ref="notify"/>
<StdDialog ref="stdDialog"/>
<keep-alive>
<router-view class="col"></router-view>
</keep-alive>
<Notify ref="notify" />
<StdDialog ref="stdDialog" />
<router-view v-slot="{ Component }">
<keep-alive v-if="showPage">
<component :is="Component" class="col" />
</keep-alive>
</router-view>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from './vueComponent.js';
import Notify from './share/Notify.vue';
import StdDialog from './share/StdDialog.vue';
import sanitizeHtml from 'sanitize-html';
import miscApi from '../api/misc';
import * as utils from '../share/utils';
export default @Component({
const componentOptions = {
components: {
Notify,
StdDialog,
@@ -80,8 +34,11 @@ export default @Component({
}
},
})
class App extends Vue {
};
class App {
_options = componentOptions;
showPage = false;
itemRuText = {
'/cardindex': 'Картотека',
'/reader': 'Читалка',
@@ -90,10 +47,10 @@ class App extends Vue {
'/sources': 'Источники',
'/settings': 'Параметры',
'/help': 'Справка',
}
};
created() {
this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
this.state = this.$store.state;
this.uistate = this.$store.state.uistate;
this.config = this.$store.state.config;
@@ -101,44 +58,69 @@ class App extends Vue {
//root route
let cachedRoute = '';
let cachedPath = '';
this.$root.rootRoute = () => {
this.$root.getRootRoute = () => {
if (this.$route.path != cachedPath) {
cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i);
cachedRoute = (m ? m[1] : this.$route.path);
}
return cachedRoute;
}
// set-app-title
this.$root.$on('set-app-title', this.setAppTitle);
this.$router.beforeEach((to, from, next) => {
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
if (window.location.host.indexOf('b.') == 0 && to.path != '/external-libs' && to.path != '/404') {
next('/404');
} else {
next();
}
});
//global keyHooks
this.keyHooks = [];
this.keyHook = (event) => {
for (const hook of this.keyHooks)
this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
// setAppTitle
this.$root.setAppTitle = this.setAppTitle;
//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);
}
this.$root.addKeyHook = (hook) => {
if (this.keyHooks.indexOf(hook) < 0)
this.keyHooks.push(hook);
this.$root.addEventHook = (hookName, hook) => {
if (!this.eventHooks[hookName])
this.eventHooks[hookName] = [];
if (this.eventHooks[hookName].indexOf(hook) < 0)
this.eventHooks[hookName].push(hook);
}
this.$root.removeKeyHook = (hook) => {
const i = this.keyHooks.indexOf(hook);
this.$root.removeEventHook = (hookName, hook) => {
if (!this.eventHooks[hookName])
return;
const i = this.eventHooks[hookName].indexOf(hook);
if (i >= 0)
this.keyHooks.splice(i, 1);
this.eventHooks[hookName].splice(i, 1);
}
document.addEventListener('keyup', (event) => {
this.keyHook(event);
});
this.$root.eventHook('key', event);
});
document.addEventListener('keypress', (event) => {
this.$root.eventHook('key', event);
});
document.addEventListener('keydown', (event) => {
this.keyHook(event);
});
window.addEventListener('resize', () => {
this.$root.$emit('resize');
this.$root.eventHook('key', event);
});
window.addEventListener('resize', (event) => {
this.$root.eventHook('resize', event);
});
}
@@ -146,23 +128,36 @@ class App extends Vue {
this.$root.notify = this.$refs.notify;
this.$root.stdDialog = this.$refs.stdDialog;
this.dispatch('config/loadConfig');
this.$watch('apiError', function(newError) {
if (newError) {
let mes = newError.message;
if (newError.response && newError.response.config)
mes = newError.response.config.url + '<br>' + newError.response.statusText;
this.$root.notify.error(mes, 'Ошибка API');
}
});
this.setAppTitle();
this.redirectIfNeeded();
(async() => {
//загрузим конфиг сревера
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();
})();
}
toggleCollapse() {
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
this.$root.$emit('resize');
this.$root.eventHook('resize');
}
get isCollapse() {
@@ -197,15 +192,17 @@ class App extends Vue {
}
get rootRoute() {
return this.$root.rootRoute();
return this.$root.getRootRoute();
}
setAppTitle(title) {
if (!title) {
if (this.mode == 'omnireader') {
if (this.mode == 'liberama.top') {
document.title = `Liberama Reader - всегда с вами`;
} else if (this.mode == 'omnireader') {
document.title = `Omni Reader - всегда с вами`;
} else if (this.config && this.mode !== null) {
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
}
} else {
document.title = title;
@@ -221,32 +218,37 @@ class App extends Vue {
}
get showAsideBar() {
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
}
set showAsideBar(value) {
}
get isReaderActive() {
return this.rootRoute == '/reader';
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
}
redirectIfNeeded() {
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
//старый url
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
const search = window.location.search.substr(1);
const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) {
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
//распознавание параметра url вида "?url=<link>" и редирект при необходимости
if (!this.isReaderActive) {
const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) {
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
}
}
}
}
export default vueComponent(App);
//-----------------------------------------------------------------------------
</script>
@@ -270,7 +272,7 @@ body, html, #app {
}
.dborder {
border: 2px solid yellow !important;
border: 2px solid magenta !important;
}
.icon-rotate {
@@ -278,6 +280,14 @@ body, html, #app {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
from {
transform: rotate(0deg);
} to {
transform: rotate(360deg);
}
}
.notify-button-icon {
font-size: 16px !important;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,543 @@
<template>
<Window ref="window" width="600px" @close="close">
<template #header>
Оглавление/закладки
</template>
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
inline-label
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="contents" icon="la la-list" label="Оглавление" />
<q-tab name="images" icon="la la-image" label="Изображения" />
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
</q-tabs>
</div>
<div class="q-mb-sm" />
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
<div>
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
</div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div>
<div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
<div
v-for="subitem in item.list"
:ref="`subitem${subitem.key}`"
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
>
<div class="col row clickable" @click="setBookPos(subitem.offset)">
<div class="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 v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
<i class="loading-img-icon la la-images"></i>
</div>
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
</div>
<div class="no-expand-button column justify-center items-center">
<div class="image-num">
{{ item.num }}
</div>
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">
JPG
</div>
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">
PNG
</div>
<div v-show="!item.local" class="image-type it-net-color row justify-center">
INET
</div>
</div>
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div>
</div>
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
Изображения отсутствуют
</div>
</div>
</div>
<div v-show="selectedTab == 'bookmarks'" class="tab-panel">
<div class="column justify-center items-center" style="height: 100px">
Раздел находится в разработке
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../vueComponent.js';
//import _ from 'lodash';
import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils';
const componentOptions = {
components: {
Window,
},
watch: {
bookPos() {
this.updateBookPosSelection();
},
selectedTab() {
this.updateBookPosScrollTop();
},
},
};
class ContentsPage {
_options = componentOptions;
_props = {
bookPos: Number,
isVisible: Boolean,
};
selectedTab = 'contents';
contents = [];
images = [];
imageSrc = [];
imageLoaded = [];
created() {
}
async init(currentBook, parsed) {
this.$refs.window.init();
//закладки
//проверим, надо ли обновлять списки
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) => {
let titleParts = title.split('<p>');
const textParts = titleParts.filter(v => v).map(v => `<div>${utils.removeHtmlTags(v)}</div>`);
if (bolder && textParts.length > 1)
textParts[0] = `<b>${textParts[0]}</b>`;
return textParts.join('');
}
const getIndentStyle = inset => `width: ${inset*20}px`;
const getLabelStyle = (inset) => {
const fontSizes = ['110%', '100%', '90%', '85%'];
inset = (inset > 3 ? 3 : inset);
return `font-size: ${fontSizes[inset]}`;
};
//формируем newContents
let i = 0;
const newContents = [];
newpc.forEach((cont) => {
const label = prepareLabel(cont.title, true);
const indentStyle = getIndentStyle(cont.inset);
const labelStyle = getLabelStyle(cont.inset);
let j = 0;
const list = [];
cont.subtitles.forEach((sub) => {
const l = prepareLabel(sub.title);
const s = getIndentStyle(sub.inset + 1);
const ls = getLabelStyle(cont.inset + 1);
const p = parsed.para[sub.paraIndex];
list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s, labelStyle: ls};
j++;
});
const p = parsed.para[cont.paraIndex];
newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
i++;
});
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 this.$nextTick();
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);
if (bp >= item.offset && bp < nextOffset) {
item.isBookPos = true;
} else if (item.isBookPos) {
item.isBookPos = false;
}
for (let j = 0; j < item.list.length; j++) {
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.updateBookPosScrollTop('contents', item, subitem, j);
} else if (subitem.isBookPos) {
subitem.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].isBookPos = true;
} else if (img.isBookPos) {
this.images[i].isBookPos = false;
}
}
this.updateBookPosScrollTop();
}
/*getOffsetTop(key) {
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
return (el ? el.offsetTop : 0);
}*/
async updateBookPosScrollTop() {
try {
await this.$nextTick();
if (this.selectedTab == 'contents') {
let item;
let subitem;
let i;
//ищем выделенные item
for(const _item of this.contents) {
if (_item.isBookPos) {
item = _item;
for (let ii = 0; ii < item.list.length; ii++) {
const _subitem = item.list[ii];
if (_subitem.isBookPos) {
subitem = _subitem;
i = ii;
break;
}
}
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
let elShift = 0;
if (subitem && item.expanded) {
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
} else {
elShift = el.offsetHeight;
}
const tabPanel = this.$refs.tabPanelContents;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - elShift;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
if (this.selectedTab == 'images') {
let item;
//ищем выделенные item
for(const _item of this.images) {
if (_item.isBookPos) {
item = _item;
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
const tabPanel = this.$refs.tabPanelImages;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
} catch (e) {
console.error(e);
}
}
getFirstElem(items) {
return (Array.isArray(items) ? items[0] : items);
}
async expandClick(key) {
const item = this.contents[key];
const expanded = !item.expanded;
if (!expanded) {
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = '0';
await utils.sleep(200);
}
this.contents[key].expanded = expanded;
if (expanded) {
await this.$nextTick();
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = subdiv.scrollHeight + 'px';
}
}
async setBookPos(newValue) {
this.$emit('book-pos-changed', {bookPos: newValue});
this.close();
}
close() {
this.$emit('do-action', {action: 'contents'});
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
export default vueComponent(ContentsPage);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.tab-panel {
overflow-x: hidden;
overflow-y: auto;
font-size: 90%;
padding: 0 10px 0px 10px;
}
.clickable {
cursor: pointer;
padding: 10px 0 10px 0;
}
.item, .subitem, .item-book-pos, .subitem-book-pos {
border-bottom: 1px solid #e0e0e0;
}
.item:hover, .subitem:hover {
background-color: #f0f0f0;
}
.item-book-pos {
background-color: #b0f0b0;
}
.subitem-book-pos {
background-color: #d0f5d0;
}
.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>

View File

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

View File

@@ -18,19 +18,24 @@
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
</ul>
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
<p>
В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").
</p>
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
<div v-show="mode == 'omnireader'">
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
<q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
<div v-show="mode == 'omnireader' || mode == 'liberama.top'">
<p>
Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>{{ bookmarkText }}</strong>
<q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon>
<br>или перетащив на панель закладок следующую ссылку:
<br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
<br><a style="margin-left: 50px" :href="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
</p>
@@ -41,14 +46,11 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../../vueComponent.js';
import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({
})
class CommonHelpPage extends Vue {
class CommonHelpPage {
created() {
}
@@ -56,6 +58,10 @@ class CommonHelpPage extends Vue {
return this.$store.state.config.mode;
}
get bookmarkText() {
return `javascript:location.href='https://${window.location.host}/?url='+location.href;`
}
async copyText(text, mes) {
const result = await copyTextToClipboard(text);
const msg = (result ? mes : 'Копирование не удалось');
@@ -65,6 +71,8 @@ class CommonHelpPage extends Vue {
this.$root.notify.error(msg);
}
}
export default vueComponent(CommonHelpPage);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,49 +1,68 @@
<template>
<div class="page">
<div class="box">
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
<p class="p">
Вы можете пожертвовать на развитие проекта любую сумму:
</p>
<div class="address">
<img class="logo" src="./assets/yandex.png">
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
<div class="para">{{ yandexAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
<img class="logo" src="./assets/yoomoney.png">
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
Пожертвовать
</q-btn><br>
<div class="para">
{{ yooAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<!--div class="address">
<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-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>
</div>
</div>
</div-->
<div class="address">
<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-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>
</div>
</div>
<div class="address">
<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-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>
</div>
</div>
<div class="address">
<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-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>
</div>
</div>
@@ -53,14 +72,12 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../../vueComponent.js';
import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({
})
class DonateHelpPage extends Vue {
yandexAddress = '410018702323056';
class DonateHelpPage {
yooAddress = '410018702323056';
paypalAddress = 'bookpauk@gmail.com';
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
@@ -69,8 +86,8 @@ class DonateHelpPage extends Vue {
created() {
}
donateYandexMoney() {
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank');
donateYooMoney() {
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
}
async copyAddress(address, prefix) {
@@ -81,6 +98,8 @@ class DonateHelpPage extends Vue {
this.$root.notify.error('Копирование не удалось');
}
}
export default vueComponent(DonateHelpPage);
//-----------------------------------------------------------------------------
</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,21 +1,27 @@
<template>
<Window @close="close">
<template slot="header">
<template #header>
Справка
</template>
<div class="col column" style="min-width: 600px">
<q-btn-toggle
v-model="selectedTab"
toggle-color="primary"
no-caps unelevated
:options="buttons"
/>
<div class="separator"></div>
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
inline-label
class="bg-grey-4 text-grey-7"
>
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
</q-tabs>
</div>
<keep-alive>
<component ref="page" class="col" :is="activePage"
></component>
<component :is="activePage" ref="page" class="col"></component>
</keep-alive>
</div>
</Window>
@@ -23,22 +29,21 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../vueComponent.js';
import Window from '../../share/Window.vue';
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
const pages = {
'CommonHelpPage': CommonHelpPage,
'HotkeysHelpPage': HotkeysHelpPage,
'MouseHelpPage': MouseHelpPage,
'VersionHistoryPage': VersionHistoryPage,
'DonateHelpPage': DonateHelpPage,
//'DonateHelpPage': DonateHelpPage,
};
const tabs = [
@@ -46,13 +51,15 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'],
//['DonateHelpPage', 'Помочь проекту'],
];
export default @Component({
const componentOptions = {
components: Object.assign({ Window }, pages),
})
class HelpPage extends Vue {
};
class HelpPage {
_options = componentOptions;
selectedTab = 'CommonHelpPage';
close() {
@@ -73,7 +80,7 @@ class HelpPage extends Vue {
}
activateDonateHelpPage() {
this.selectedTab = 'DonateHelpPage';
//this.selectedTab = 'DonateHelpPage';
}
activateVersionHistoryHelpPage() {
@@ -81,18 +88,16 @@ class HelpPage extends Vue {
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
export default vueComponent(HelpPage);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,47 @@
<template>
<div ref="main" class="column no-wrap" style="min-height: 500px">
<div class="relative-position">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
<div v-if="mode != 'liberama.top'" class="relative-position">
<GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
</div>
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
<span class="greeting"><b>{{ title }}</b></span>
<div class="q-my-sm"></div>
<span class="greeting">Добро пожаловать!</span>
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
<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>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
</div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
<template v-slot:append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
<q-input
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
>
<template #append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
</template>
</q-input>
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
<input
id="file" ref="file" type="file"
style="display: none;"
:accept="acceptFileExt"
@change="loadFile"
/>
<div class="q-my-sm"></div>
<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>
<div class="q-my-sm"></div>
<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>
<div class="q-my-md"></div>
<div v-if="mode == 'omnireader'">
<!--div v-if="mode == 'omnireader'">
<div ref="yaShare2" class="ya-share2"
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
@@ -39,42 +49,61 @@
data-url="https://omnireader.ru">
</div>
</div>
<div class="q-my-sm"></div>
<div class="q-my-sm"></div-->
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
</div>
<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="openDonate">Помочь проекту</span>
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
</div>
<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>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../vueComponent.js';
import GithubCorner from './GithubCorner/GithubCorner.vue';
import Dialog from '../../share/Dialog.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory';
import * as utils from '../../../share/utils';
export default @Component({
const componentOptions = {
components: {
GithubCorner,
Dialog,
PasteTextPage,
},
})
class LoaderPage extends Vue {
};
class LoaderPage {
_options = componentOptions;
bookUrl = null;
loadPercent = 0;
pasteTextActive = false;
findBookVisible = false;
created() {
this.commit = this.$store.commit;
@@ -82,8 +111,8 @@ class LoaderPage extends Vue {
mounted() {
this.progress = this.$refs.progress;
if (this.mode == 'omnireader')
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
/*if (this.mode == 'omnireader')
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
}
activated() {
@@ -93,6 +122,8 @@ class LoaderPage extends Vue {
get title() {
if (this.mode == 'omnireader')
return 'Omni Reader - браузерная онлайн-читалка.';
if (this.mode == 'liberama.top')
return 'Liberama Reader - браузерная онлайн-читалка.';
return 'Универсальная читалка книг и ресурсов интернета.';
}
@@ -105,14 +136,16 @@ class LoaderPage extends Vue {
return this.$store.state.config.version;
}
get acceptFileExt() {
return this.$store.state.config.acceptFileExt;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
get clientVersion() {
let v = versionHistory[0].header;
v = v.split(' ')[0];
return v;
return versionHistory[0].version;
}
submitUrl() {
@@ -134,7 +167,7 @@ class LoaderPage extends Vue {
}
loadBufferClick() {
this.pasteTextToggle();
this.showPasteText();
}
loadBuffer(opts) {
@@ -144,6 +177,10 @@ class LoaderPage extends Vue {
}
}
showPasteText() {
this.pasteTextActive = true;
}
pasteTextToggle() {
this.pasteTextActive = !this.pasteTextActive;
}
@@ -156,6 +193,10 @@ class LoaderPage extends Vue {
this.$emit('do-action', {action: 'donate'});
}
findBook() {
this.findBookVisible = true;
}
openComments() {
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
}
@@ -164,30 +205,30 @@ class LoaderPage extends Vue {
window.open('http://old.omnireader.ru', '_blank');
}
async onInputKeydown(event) {
if (event.key == 'Enter') {
await utils.sleep(100);
this.submitUrl();
}
}
keyHook(event) {
if (this.$refs.dialog1.active)
return true;
if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event);
}
//недостатки сторонних ui
const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
this.submitUrl();
const input = this.$refs.input.getNativeElement();
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
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;
}
}
export default vueComponent(LoaderPage);
//-----------------------------------------------------------------------------
</script>
<style scoped>

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
<div class="column justify-start items-center" style="height: 250px">
<q-circular-progress
show-value
@@ -17,7 +17,7 @@
<div>
<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>
@@ -25,8 +25,8 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../vueComponent.js';
import * as utils from '../../../share/utils';
const ruMessage = {
@@ -42,9 +42,7 @@ const ruMessage = {
'upload': 'отправка',
};
export default @Component({
})
class ProgressPage extends Vue {
class ProgressPage {
text = '';
totalSteps = 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);
}
}
export default vueComponent(ProgressPage);
//-----------------------------------------------------------------------------
</script>

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,158 +1,215 @@
<template>
<Window width="600px" ref="window" @close="close">
<template slot="header">
<Window ref="window" width="600px" @close="close">
<template #header>
<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>
<a ref="download" style='display: none;'></a>
<a ref="download" style="display: none;" target="_blank"></a>
<q-table
class="recent-books-table col"
:data="tableData"
:columns="columns"
row-key="key"
:pagination.sync="pagination"
separator="cell"
hide-bottom
virtual-scroll
dense
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
placeholder="Найти"
v-model="search"
@click.stop
/>
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
<div ref="header" class="scroll-header row bg-blue-2">
<q-btn class="tool-button" round @click="showSameBookClick">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Показать/скрыть версии книг
</q-tooltip>
</q-btn>
<span v-html="props.cols[2].label"></span>
</q-th>
</q-tr>
</template>
<q-btn class="tool-button" round @click="scrollToBegin">
<q-icon name="la la-arrow-up" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В начало списка
</q-tooltip>
</q-btn>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="num" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 25px">
{{ props.row.num }}
<q-btn class="tool-button" round @click="scrollToEnd">
<q-icon name="la la-arrow-down" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В конец списка
</q-tooltip>
</q-btn>
<q-btn class="tool-button" round @click="scrollToActiveBook">
<q-icon name="la la-location-arrow" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
На текущую книгу
</q-tooltip>
</q-btn>
<q-input
ref="input"
v-model="search"
class="q-ml-sm q-mt-xs"
outlined dense
style="width: 185px"
bg-color="white"
placeholder="Найти"
@click.stop
>
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
</template>
</q-input>
<q-select
ref="sortMethod"
v-model="sortMethod"
class="q-ml-sm q-mt-xs"
:options="sortMethodOptions"
style="width: 180px"
bg-color="white"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options display-value-sanitize options-sanitize
options-html display-value-html
@update:model-value="sortMethodSelected"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Метод сортировки
</q-tooltip>
<template #selected-item="scope">
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
</template>
</q-select>
</div>
<q-virtual-scroll
ref="virtualScroll"
v-slot="{ item, index }"
:items="tableData"
scroll-target="#vs-container"
virtual-scroll-item-size="80"
@virtual-scroll="onScroll"
>
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px">
<q-icon name="la la-code-branch" size="24px" style="color: green" />
</div>
<div class="row-part column justify-center items-stretch" style="width: 80px">
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
</div>
</q-td>
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
<div class="break-word" style="width: 68px">
{{ props.row.touchDate }}<br>
{{ props.row.touchTime }}
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
{{ (item.group ? item.group.length + 1 : 0) }} верси{{ wordEnding((item.group ? item.group.length + 1 : 0), 1) }}
</div>
</q-td>
</div>
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
<div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">{{ props.row.desc.author }}</div>
<div>{{ props.row.desc.title }}</div>
<div class="row-part column items-stretch clickable break-word" :style="{ 'width': (350 - 40*(+item.inGroup)) + 'px' }" style="font-size: 75%" @click="loadBook(item)">
<div class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
<div class="text-green-10" style="font-size: 105%">
{{ item.desc.author }}
</div>
<div>{{ item.desc.title }}</div>
</div>
</q-td>
<q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%">
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
<div class="row" style="font-size: 10px">
<div class="row justify-center items-center row-info-top" style="width: 60px">
{{ item.desc.textLen }}
</div>
<div class="row items-center row-info-top" :style="`width: ${(220 - 40*(+item.inGroup))}px; padding: 1px`">
<div class="read-bar" :style="`width: ${100*item.readPart}%`"></div>
</div>
<div class="row justify-center items-center row-info-top" style="width: 59px">
{{ item.desc.perc }}
</div>
<div class="row-info-top" style="width: 1px">
</div>
</div>
</q-td>
<q-td key="close" :props="props" class="td-mp" auto-width>
<div style="width: 38px">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(props.row.key)">
<q-icon class="la la-times" size="14px" style="top: -6px"/>
</q-btn>
<div class="row" style="font-size: 10px" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
<div class="row justify-center items-center row-info-bottom" style="width: 30px">
{{ item.num }}
</div>
<div class="col row">
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
Загружен: {{ item.loadTime }}
</div>
<div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
Читался: {{ item.touchTime }}
</div>
</div>
<div class="row-info-bottom" style="width: 1px">
</div>
</div>
</q-td>
<q-td key="last" :props="props" class="no-mp">
</q-td>
</q-tr>
</template>
</q-table>
</div>
<div class="row-part column justify-center" style="width: 80px; font-size: 75%">
<div>
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
</div>
</div>
<div class="row-part column justify-center">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(item.key)"
>
<q-icon class="la la-times" size="14px" />
</q-btn>
</div>
</div>
</q-virtual-scroll>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import path from 'path';
import vueComponent from '../../vueComponent.js';
import path from 'path-browserify';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import LockQueue from '../../../share/LockQueue';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
export default @Component({
const componentOptions = {
components: {
Window,
},
watch: {
search: function() {
search() {
this.updateTableData();
}
},
sortMethod() {
this.updateTableData();
},
settings() {
this.loadSettings();
},
},
})
class RecentBooksPage extends Vue {
};
class RecentBooksPage {
_options = componentOptions;
loading = false;
search = null;
search = '';
tableData = [];
columns = [];
pagination = {};
sortMethod = '';
showSameBook = false;
created() {
this.pagination = {rowsPerPage: 0};
this.commit = this.$store.commit;
this.columns = [
{
name: 'num',
label: '#',
align: 'center',
sortable: true,
field: 'num',
},
{
name: 'date',
label: 'Время<br>просм.',
align: 'left',
field: 'touchDateTime',
sortable: true,
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
},
{
name: 'desc',
label: 'Название',
align: 'left',
field: 'descString',
sortable: true,
},
{
name: 'links',
label: '',
align: 'left',
},
{
name: 'close',
label: '',
align: 'left',
},
{
name: 'last',
label: '',
align: 'left',
},
];
this.lastScrollTop1 = 0;
this.lastScrollTop2 = 0;
this.lock = new LockQueue(100);
this.loadSettings();
}
init() {
@@ -161,143 +218,223 @@ class RecentBooksPage extends Vue {
this.$nextTick(() => {
//this.$refs.input.focus();//плохо на планшетах
});
(async() => {//подгрузка списка
if (this.initing)
return;
this.initing = true;
this.inited = true;
if (!bookManager.loaded) {
await this.updateTableData(10);
//для отзывчивости
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);
}
(async() => {
this.showBar();
await this.updateTableData();
this.initing = false;
await this.scrollToActiveBook();
})();
}
async updateTableData(limit) {
while (this.updating) await utils.sleep(100);
this.updating = true;
let result = [];
this.loading = !!limit;
const sorted = bookManager.getSortedRecent();
let num = 0;
for (let i = 0; i < sorted.length; i++) {
const book = sorted[i];
if (book.deleted)
continue;
num++;
if (limit && result.length >= limit)
break;
let d = new Date();
d.setTime(book.touchTime);
const t = utils.formatDate(d).split(' ');
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
textLen = ` ${Math.round(book.textLength/1000)}k`;
}
const fb2 = (book.fb2 ? book.fb2 : {});
let title = fb2.bookTitle;
if (title)
title = `"${title}"`;
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({
num,
touchDateTime: book.touchTime,
touchDate: t[0],
touchTime: t[1],
desc: {
author,
title: `${title}${perc}${textLen}`,
},
descString: `${author}${title}${perc}${textLen}`,
url: book.url,
path: book.path,
key: book.key,
});
}
const search = this.search;
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.touchDate.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
this.tableData = result;
this.updating = false;
loadSettings() {
const settings = this.settings;
this.showSameBook = settings.recentShowSameBook;
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
}
wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
get settings() {
return this.$store.state.reader.settings;
}
async updateTableData() {
if (!this.inited)
return;
await this.lock.get();
try {
let result = [];
const sorted = bookManager.getSortedRecent();
const activeBook = bookManager.mostRecentBook();
//подготовка полей
for (const book of sorted) {
if (book.deleted)
continue;
let d = new Date();
d.setTime(book.touchTime);
const touchTime = utils.formatDate(d);
const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
d.setTime(loadTimeRaw);
const loadTime = utils.formatDate(d);
let readPart = 0;
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
readPart = p/book.textLength;
perc = `${(readPart*100).toFixed(2)}%`;
textLen = `${Math.floor(readPart*book.textLength/1000)}/${Math.floor(book.textLength/1000)}`;
}
const bt = utils.getBookTitle(book.fb2);
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
result.push({
touchTime,
loadTime,
desc: {
author,
title,
perc,
textLen,
},
readPart,
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
sameBookKey: book.sameBookKey,
active: (activeBook.key == book.key),
activeParent: false,
inGroup: false,
//для сортировки
loadTimeRaw,
touchTimeRaw: book.touchTime,
});
}
//нумерация
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
let num = 0;
for (let i = result.length - 1; i >= 0; i--) {
num++;
result[i].num = num;
}
//фильтрация
const search = this.search;
if (search) {
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.loadTime.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
}
//сортировка
switch (this.sortMethod) {
case 'loadTimeDesc':
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
break;
case 'loadTimeAsc':
result.sort((a, b) => a.loadTimeRaw - b.loadTimeRaw);
break;
case 'touchTimeDesc':
result.sort((a, b) => b.touchTimeRaw - a.touchTimeRaw);
break;
case 'touchTimeAsc':
result.sort((a, b) => a.touchTimeRaw - b.touchTimeRaw);
break;
case 'authorDesc':
result.sort((a, b) => b.desc.author.localeCompare(a.desc.author));
break;
case 'authorAsc':
result.sort((a, b) => a.desc.author.localeCompare(b.desc.author));
break;
case 'titleDesc':
result.sort((a, b) => b.desc.title.localeCompare(a.desc.title));
break;
case 'titleAsc':
result.sort((a, b) => a.desc.title.localeCompare(b.desc.title));
break;
}
//группировка
const groups = {};
const parents = {};
let newResult = [];
for (const book of result) {
if (book.sameBookKey !== undefined) {
if (!groups[book.sameBookKey]) {
groups[book.sameBookKey] = [];
parents[book.sameBookKey] = book;
book.group = groups[book.sameBookKey];
newResult.push(book);
} else {
book.inGroup = true;
if (book.active)
parents[book.sameBookKey].activeParent = true;
groups[book.sameBookKey].push(book);
}
} else {
newResult.push(book);
}
}
result = newResult;
//showSameBook
if (this.showSameBook) {
newResult = [];
for (const book of result) {
newResult.push(book);
if (book.group) {
for (const sameBook of book.group) {
newResult.push(sameBook);
}
}
}
result = newResult;
}
//другие стадии
//.....
this.tableData = result;
} finally {
this.lock.ret();
}
}
resetSearch() {
this.search = '';
this.$refs.input.focus();
}
wordEnding(num, type = 0) {
const endings = [
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
];
const deci = num % 100;
if (deci > 10 && deci < 20) {
return '';
return endings[type][0];
} else {
return endings[num % 10];
return endings[type][num % 10];
}
}
get header() {
const len = (this.tableData ? this.tableData.length : 0);
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
return `${(this.search ? 'Найдено' : 'Всего')} ${len} файл${this.wordEnding(len)}`;
}
async downloadBook(fb2path) {
async downloadBook(fb2path, fullTitle) {
try {
await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download;
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();
} catch (e) {
let errMes = e.message;
@@ -307,14 +444,6 @@ class RecentBooksPage extends Vue {
}
}
openOriginal(url) {
window.open(url, '_blank');
}
openFb2(path) {
window.open(path, '_blank');
}
async handleDel(key) {
await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
@@ -323,56 +452,167 @@ class RecentBooksPage extends Vue {
this.close();
}
loadBook(url) {
this.$emit('load-book', {url});
loadBook(row) {
this.$emit('load-book', {url: row.url, path: row.path});
this.close();
}
isUrl(url) {
if (url)
return (url.indexOf('file://') != 0);
return (url.indexOf('disk://') != 0);
else
return false;
}
showBar() {
this.lastScrollTop1 = this.$refs.vsContainer.scrollTop;
this.$refs.header.style.position = 'sticky';
this.$refs.header.style.top = 0;
}
onScroll() {
const curScrollTop = this.$refs.vsContainer.scrollTop;
if (this.lockScroll) {
this.lastScrollTop1 = curScrollTop;
return;
}
if (curScrollTop - this.lastScrollTop1 > 100) {
this.$refs.header.style.top = `-${this.$refs.header.offsetHeight}px`;
this.$refs.header.style.transition = 'top 0.2s ease 0s';
this.lastScrollTop1 = curScrollTop;
} else if (curScrollTop - this.lastScrollTop2 < 0) {
this.$refs.header.style.position = 'sticky';
this.$refs.header.style.top = 0;
this.lastScrollTop1 = curScrollTop;
}
this.lastScrollTop2 = curScrollTop;
}
showSameBookClick() {
this.showSameBook = !this.showSameBook;
const newSettings = _.cloneDeep(this.settings);
newSettings.recentShowSameBook = this.showSameBook;
this.commit('reader/setSettings', newSettings);
this.updateTableData();
}
sortMethodSelected() {
const newSettings = _.cloneDeep(this.settings);
newSettings.recentSortMethod = this.sortMethod;
this.commit('reader/setSettings', newSettings);
}
async scrollToActiveBook() {
this.lockScroll = true;
try {
let activeIndex = -1;
let activeParentIndex = -1;
for (let i = 0; i < this.tableData.length; i++) {
const book = this.tableData[i];
if (book.active)
activeIndex = i;
if (book.activeParent)
activeParentIndex = i;
if (activeIndex >= 0 && activeParentIndex >= 0)
break;
}
const index = (activeIndex >= 0 ? activeIndex : activeParentIndex);
if (index >= 0) {
this.$refs.virtualScroll.scrollTo(index, 'center');
}
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
async scrollToBegin() {
this.lockScroll = true;
try {
this.$refs.virtualScroll.scrollTo(0, 'center');
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
async scrollToEnd() {
this.lockScroll = true;
try {
this.$refs.virtualScroll.scrollTo(this.tableData.length, 'center');
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
get sortMethodOptions() {
return [
{label: '<span style="font-size: 150%">&uarr;</span> Время загрузки', value: 'loadTimeDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Время загрузки', value: 'loadTimeAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Время чтения', value: 'touchTimeDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Время чтения', value: 'touchTimeAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Автор', value: 'authorDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Автор', value: 'authorAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Название', value: 'titleDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Название', value: 'titleAsc'},
];
}
close() {
this.$emit('recent-books-close');
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
export default vueComponent(RecentBooksPage);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.recent-books-table {
width: 600px;
.recent-books-scroll {
width: 573px;
overflow-y: auto;
overflow-x: hidden;
}
.scroll-header {
height: 50px;
position: sticky;
z-index: 1;
top: 0;
border-bottom: 2px solid #aaaaaa;
padding-left: 5px;
}
.table-row {
min-height: 80px;
}
.row-part {
padding: 4px 4px 4px 4px;
}
.clickable {
cursor: pointer;
}
.td-mp {
margin: 0 !important;
padding: 4px 4px 4px 4px !important;
border-bottom: 1px solid #ddd;
}
.no-mp {
margin: 0 !important;
padding: 0 !important;
border: 0;
border-left: 1px solid #ddd !important;
}
.break-word {
line-height: 180%;
overflow-wrap: break-word;
@@ -380,21 +620,55 @@ class RecentBooksPage extends Vue {
white-space: normal;
}
</style>
<style>
.recent-books-table .q-table__middle {
height: 100%;
overflow-x: hidden;
.even {
background-color: #f2f2f2;
}
.recent-books-table thead tr:first-child th {
position: sticky;
z-index: 1;
top: 0;
background-color: #c1f4cd;
.active-book {
background-color: #b0f0b0 !important;
}
.recent-books-table tr:nth-child(even) {
background-color: #f8f8f8;
.active-parent-book {
background-color: #ffbbbb !important;
}
.icon {
transition: transform 0.2s;
}
.expanded-icon {
transform: rotate(90deg);
}
.tool-button {
min-width: 30px;
width: 30px;
min-height: 30px;
height: 30px;
margin: 10px 6px 0px 3px;
background-color: white;
}
.row-info-bottom {
line-height: 110%;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
height: 12px;
}
.row-info-top {
line-height: 110%;
border: 1px solid #cccccc;
border-right: 0;
height: 12px;
}
.time-info, .row-info-top {
color: #888888;
}
.read-bar {
height: 6px;
background-color: #bbbbbb;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header">
<template #header>
{{ header }}
</template>
@@ -8,18 +8,23 @@
<span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input">
<!--input ref="input"
placeholder="что ищем"
:value="needle" @input="needle = $event.target.value"/-->
<q-input ref="input" class="col" outlined dense
placeholder="что ищем"
v-model="needle" @keydown="inputKeyDown"
<q-input
ref="input" v-model="needle"
class="col" outlined dense
placeholder="Найти"
@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>
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
<q-btn class="button" dense stretch @click="showNext">
<q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
</q-btn>
<q-btn class="button" dense stretch @click="showPrev">
<q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
</q-btn>
</q-btn-group>
</div>
</Window>
@@ -27,13 +32,12 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../vueComponent.js';
import Window from '../../share/Window.vue';
import {sleep} from '../../../share/utils';
export default @Component({
const componentOptions = {
components: {
Window,
},
@@ -49,8 +53,10 @@ export default @Component({
el.style.paddingRight = newValue.length*12 + 'px';
},
},
})
class SearchPage extends Vue {
};
class SearchPage {
_options = componentOptions;
header = null;
initStep = null;
initPercentage = 0;
@@ -100,7 +106,7 @@ class SearchPage extends Vue {
this.parsed = parsed;
}
this.header = 'Найти';
this.header = 'Поиск в тексте';
await this.$nextTick();
this.$refs.input.focus();
this.$refs.input.select();
@@ -174,12 +180,14 @@ class SearchPage extends Vue {
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
export default vueComponent(SearchPage);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,11 +1,11 @@
<template>
<div></div>
<div class="hidden"></div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../vueComponent.js';
import _ from 'lodash';
import bookManager from '../share/bookManager';
@@ -18,7 +18,7 @@ const ssCacheStore = localForage.createInstance({
name: 'ssCacheStore'
});
export default @Component({
const componentOptions = {
watch: {
serverSyncEnabled: function() {
this.serverSyncEnabledChanged();
@@ -35,41 +35,51 @@ export default @Component({
currentProfile: function() {
this.currentProfileChanged(true);
},
libs: function() {
this.debouncedSaveLibs();
},
},
})
class ServerStorage extends Vue {
};
class ServerStorage {
_options = componentOptions;
created() {
this.inited = false;
this.keyInited = false;
this.commit = this.$store.commit;
this.prevServerStorageKey = null;
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
this.debouncedSaveSettings = _.debounce(() => {
this.saveSettings();
}, 500);
this.debouncedSaveLibs = _.debounce(() => {
this.saveLibs();
}, 500);
this.debouncedNotifySuccess = _.debounce(() => {
this.success('Данные синхронизированы с сервером');
}, 1000);
this.oldProfiles = {};
this.oldSettings = {};
this.oldLibs = {};
}
async init() {
try {
this.cachedRecent = await ssCacheStore.getItem('recent');
if (!this.cachedRecent)
await this.setCachedRecent({rev: 0, data: {}});
await this.cleanCachedRecent('cachedRecent');
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
if (!this.cachedRecentPatch)
await this.setCachedRecentPatch({rev: 0, data: {}});
await this.cleanCachedRecent('cachedRecentPatch');
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
if (!this.cachedRecentMod)
await this.setCachedRecentMod({rev: 0, data: {}});
await this.cleanCachedRecent('cachedRecentMod');
if (!this.serverStorageKey) {
//генерируем новый ключ
@@ -97,6 +107,15 @@ class ServerStorage extends Vue {
this.cachedRecentMod = value;
}
async cleanCachedRecent(whatToClean) {
if (whatToClean == 'cachedRecent' || whatToClean == 'all')
await this.setCachedRecent({rev: 0, data: {}});
if (whatToClean == 'cachedRecentPatch' || whatToClean == 'all')
await this.setCachedRecentPatch({rev: 0, data: {}});
if (whatToClean == 'cachedRecentMod' || whatToClean == 'all')
await this.setCachedRecentMod({rev: 0, data: {}});
}
async generateNewServerStorageKey() {
const key = utils.toBase58(utils.randomArray(32));
this.commit('reader/setServerStorageKey', key);
@@ -124,9 +143,14 @@ class ServerStorage extends Vue {
await this.loadProfiles(force);
this.checkCurrentProfile();
await this.currentProfileChanged(force);
await this.loadLibs(force);
if (force)
await this.cleanCachedRecent('all');
const loadSuccess = await this.loadRecent();
if (loadSuccess && force)
if (loadSuccess && force) {
await this.saveRecent();
}
}
}
@@ -169,6 +193,14 @@ class ServerStorage extends Vue {
return this.settings.showServerStorageMessages;
}
get libs() {
return this.$store.state.reader.libs;
}
get libsRev() {
return this.$store.state.reader.libsRev;
}
checkCurrentProfile() {
if (!this.profiles[this.currentProfile]) {
this.commit('reader/setCurrentProfile', '');
@@ -186,8 +218,15 @@ class ServerStorage extends Vue {
}
error(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$root.notify.error(message);
if (this.showServerStorageMessages && !this.offlineModeActive) {
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) {
@@ -338,13 +377,85 @@ class ServerStorage extends Vue {
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else if (result.state == 'success') {
this.oldProfiles = _.cloneDeep(this.profiles);
this.commit('reader/setProfilesRev', this.profilesRev + 1);
this.commit('reader/setProfilesRev', this.profilesRev + 1);
}
} finally {
this.savingProfiles = false;
}
}
async loadLibs(force = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled)
return;
const oldRev = this.libsRev;
//проверим ревизию на сервере
if (!force) {
try {
const revs = await this.storageCheck({libs: {}});
if (revs.state == 'success' && revs.items.libs.rev == oldRev) {
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let libs = null;
try {
libs = await this.storageGet({libs: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (libs.state == 'success') {
libs = libs.items.libs;
if (libs.rev == 0)
libs.data = {};
this.oldLibs = _.cloneDeep(libs.data);
this.commit('reader/setLibs', libs.data);
this.commit('reader/setLibsRev', libs.rev);
if (doNotifySuccess)
this.debouncedNotifySuccess();
} else {
this.warning(`Неверный ответ сервера: ${libs.state}`);
}
}
async saveLibs() {
if (!this.keyInited || !this.serverSyncEnabled || this.savingLibs)
return;
const diff = utils.getObjDiff(this.oldLibs, this.libs);
if (utils.isEmptyObjDiff(diff))
return;
this.savingLibs = true;
try {
let result = {state: ''};
try {
result = await this.storageSet({libs: {rev: this.libsRev + 1, data: this.libs}});
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
await this.loadLibs(true, false);
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else if (result.state == 'success') {
this.oldLibs = _.cloneDeep(this.libs);
this.commit('reader/setLibsRev', this.libsRev + 1);
}
} finally {
this.savingLibs = false;
}
}
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
return;
@@ -403,12 +514,12 @@ class ServerStorage extends Vue {
const md = newRecentMod.data;
if (md.key && result[md.key])
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, true);
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
if (!bookManager.loaded) {
/*if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией');
while (!bookManager.loaded) await utils.sleep(100);
}
}*/
if (newRecent.rev != this.cachedRecent.rev)
await this.setCachedRecent(newRecent);
@@ -465,9 +576,9 @@ class ServerStorage extends Vue {
newRecentPatch.rev++;
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])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, true);
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
needSaveRecentPatch = true;
@@ -478,8 +589,8 @@ class ServerStorage extends Vue {
let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent
while (!bookManager.loaded)
await utils.sleep(100);
/*while (!bookManager.loaded)
await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
@@ -516,7 +627,7 @@ class ServerStorage extends Vue {
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) {
this.savingRecent = false;
this.saveRecent(itemKey, true);
await this.saveRecent(itemKey, true);
return;
}
} else if (result.state == 'success') {
@@ -617,13 +728,15 @@ class ServerStorage extends Vue {
const ids = id.split('.');
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
throw new Error(`decodeStorageItems: bad id - ${id}`);
items[utils.fromBase58(ids[1])] = decoded;
items[utils.fromBase58(ids[1]).toString()] = decoded;
}
}
result.items = items;
return result;
}
}
export default vueComponent(ServerStorage);
//-----------------------------------------------------------------------------
</script>

View File

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

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%">
Показывать уведомления "Что нового"<br>
при каждом выходе новой версии читалки
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
Показывать уведомление о новой версии
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Напоминать о необходимости обновления страницы<br>
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<!--div class="item row">
<div class="label-6">Уведомление</div>
<q-checkbox size="xs" v-model="showDonationDialog2020">
Показывать "Оплатим хостинг вместе"
@@ -49,27 +60,11 @@
Показывать уведомление "Оплатим хостинг вместе"
</q-tooltip>
</q-checkbox>
</div>
</div-->
<!---------------------------------------------->
<div class="part-header">Другое</div>
<div class="item row">
<div class="label-6">Обработка</div>
<div class="col row">
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Html-фильтр вырезает лишние элементы со<br>
страницы для определенных сайтов, таких как:<br>
samlib.ru<br>
www.fanfiction.net<br>
archiveofourown.org<br>
и других
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="item row">
<div class="label-6">Обработка</div>
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">

View File

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

View File

@@ -1,6 +1,6 @@
<template lang="includer">
<Window ref="window" height="95%" width="600px" @close="close">
<template slot="header">
<template>
<Window ref="window" width="600px" @close="close">
<template #header>
Настройки
</template>
@@ -8,8 +8,9 @@
<div class="full-height">
<q-tabs
ref="tabs"
class="bg-grey-3 text-black"
v-model="selectedTab"
class="bg-grey-3 text-black"
left-icon="la la-caret-up"
right-icon="la la-caret-down"
active-color="white"
@@ -20,70 +21,115 @@
stretch
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="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="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="reset" icon="la la-broom" label="Сброс" />
<div v-show="tabsScrollable" class="q-pt-lg"/>
<div v-show="tabsScrollable" class="q-pt-lg" />
</q-tabs>
</div>
<div class="col fit">
<!-- Профили --------------------------------------------------------------------->
<div v-if="selectedTab == 'profiles'" class="fit tab-panel">
@@include('./include/ProfilesTab.inc');
@@include('./ProfilesTab.inc');
</div>
<!-- Вид ------------------------------------------------------------------------->
<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 v-if="selectedTab == 'buttons'" class="fit tab-panel">
@@include('./include/ButtonsTab.inc');
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ToolBarTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column">
@@include('./include/KeysTab.inc');
@@include('./KeysTab.inc');
</div>
<!-- Листание -------------------------------------------------------------------->
<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 v-if="selectedTab == 'others'" class="fit tab-panel">
@@include('./include/OthersTab.inc');
@@include('./OthersTab.inc');
</div>
<!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./include/ResetTab.inc');
@@include('./ResetTab.inc');
</div>
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import { ref, watch } from 'vue';
import vueComponent from '../../vueComponent.js';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
export default @Component({
const componentOptions = {
components: {
Window,
NumInput,
@@ -97,8 +143,9 @@ export default @Component({
this.settingsChanged();
},
form: function(newValue) {
if (this.inited)
this.commit('reader/setSettings', newValue);
if (this.inited) {
this.commit('reader/setSettings', _.cloneDeep(newValue));
}
},
fontBold: function(newValue) {
this.fontWeight = (newValue ? 'bold' : '');
@@ -108,8 +155,10 @@ export default @Component({
},
vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName);
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
if (this.fontShifts[font] != newValue || this.fontVertShift != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
}
},
fontName: function(newValue) {
const font = (this.webFontName ? this.webFontName : newValue);
@@ -123,6 +172,10 @@ export default @Component({
if (newValue != '' && this.pageChangeAnimation == 'flip')
this.pageChangeAnimation = '';
},
dualPageMode(newValue) {
if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
this.pageChangeAnimation = '';
},
textColor: function(newValue) {
this.textColorFiltered = newValue;
},
@@ -137,19 +190,35 @@ export default @Component({
if (hex.test(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';
selectedViewTab = 'color';
selectedViewTab = 'mode';
selectedKeysTab = 'mouse';
form = {};
fontBold = false;
fontItalic = false;
vertShift = 0;
tabsScrollable = false;
textColorFiltered = '';
bgColorFiltered = '';
dualDivColorFiltered = '';
webFonts = [];
fonts = [];
@@ -158,6 +227,19 @@ class SettingsPage extends Vue {
toolButtons = [];
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() {
this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
@@ -172,7 +254,7 @@ class SettingsPage extends Vue {
this.$watch(
'$refs.tabs.scrollable',
(newValue) => {
this.tabsScrollable = newValue && !this.$isMobileDevice;
this.tabsScrollable = newValue && !this.$root.isMobileDevice;
}
);
}
@@ -185,12 +267,10 @@ class SettingsPage extends Vue {
settingsChanged() {
if (_.isEqual(this.form, this.settings))
return;
this.form = Object.assign({}, this.settings);
for (let prop in rstore.settingDefaults) {
this[prop] = this.form[prop];
this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue});
});
for (const prop in rstore.settingDefaults) {
this[prop] = _.cloneDeep(this.form[prop]);
}
this.fontBold = (this.fontWeight == 'bold');
@@ -202,12 +282,18 @@ class SettingsPage extends Vue {
this.vertShift = this.fontShifts[font] || 0;
this.textColorFiltered = this.textColor;
this.bgColorFiltered = this.backgroundColor;
this.dualDivColorFiltered = this.dualDivColor;
this.statusBarColorFiltered = this.statusBarColor;
}
get mode() {
return this.$store.state.config.mode;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
get settings() {
return this.$store.state.reader.settings;
}
@@ -237,9 +323,19 @@ class SettingsPage extends Vue {
get wallpaperOptions() {
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}`});
}
return result;
}
@@ -263,13 +359,15 @@ class SettingsPage extends Vue {
let result = [
{label: 'Нет', value: ''},
{label: 'Вверх-вниз', value: 'downShift'},
{label: 'Вправо-влево', value: 'rightShift'},
(!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
{label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'},
];
if (this.wallpaper == '')
result.push({label: 'Листание', value: 'flip'});
(this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
];
result = result.filter(v => v);
return result;
}
@@ -332,6 +430,12 @@ class SettingsPage extends Vue {
case 'bg':
result += `background-color: ${this.backgroundColor};`
break;
case 'div':
result += `background-color: ${this.dualDivColor};`
break;
case 'statusbar':
result += `background-color: ${this.statusBarColor};`
break;
}
return result;
}
@@ -361,10 +465,6 @@ class SettingsPage extends Vue {
}
}
changeShowToolButton(buttonName) {
this.showToolButton = Object.assign({}, this.showToolButton, {[buttonName]: !this.showToolButton[buttonName]});
}
async addProfile() {
try {
if (Object.keys(this.profiles).length >= 100) {
@@ -397,7 +497,7 @@ class SettingsPage extends Vue {
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.currentProfile}' необратимо.` +
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
@@ -490,7 +590,8 @@ class SettingsPage extends Vue {
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.$root.$emit('generateNewServerStorageKey');
if (this.$root.generateNewServerStorageKey)
this.$root.generateNewServerStorageKey();
}
} catch (e) {
//
@@ -498,13 +599,80 @@ 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) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
export default vueComponent(SettingsPage);
//-----------------------------------------------------------------------------
</script>
@@ -534,11 +702,11 @@ class SettingsPage extends Vue {
margin-bottom: 5px;
}
.label-1 {
.label-1, .label-3, .label-7 {
width: 75px;
}
.label-2, .label-3, .label-4, .label-5 {
.label-2, .label-4, .label-5 {
width: 110px;
}
@@ -546,7 +714,7 @@ class SettingsPage extends Vue {
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;
flex-direction: column;
justify-content: center;

View File

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

View File

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

View File

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

View File

@@ -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 {
fontBySize(size) {
return `${size}px ${this.fontName}`;
return `${size}px '${this.fontName}'`;
}
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
@@ -19,6 +19,109 @@ export default class DrawHelper {
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) {
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
return '';
@@ -26,140 +129,78 @@ export default class DrawHelper {
const font = this.fontByStyle({});
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}` +
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
let imageDrawn = new Set();
let imageDrawn1 = new Set();
let imageDrawn2 = new Set();
let len = lines.length;
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
len = (len > lineCount ? lineCount : len);
for (let i = 0; i < len; i++) {
const line = lines[i];
/* 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 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 sel = new Set();
if (len > 0 && this.searching) {
const line = lines[0];
let pureText = '';
for (const part of line.parts) {
pureText += part.text;
}
let lineText = '';
let center = false;
let space = 0;
pureText = pureText.toLowerCase();
let j = 0;
//формируем строку
for (const part of line.parts) {
let tOpen = (part.style.bold ? '<b>' : '');
tOpen += (part.style.italic ? '<i>' : '');
let tClose = (part.style.italic ? '</i>' : '');
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++;
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
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) + (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 {
//
}
}
break;
j++;
}
}
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 (!this.dualPageMode) {
out += `<div class="fit">`;
for (let i = 0; i < len; i++) {
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>';
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 fh = h - 2*pad;
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 read = (bookPos + 1)/textLength;
const t2 = `${(read*100).toFixed(2)}%`;
@@ -172,8 +213,8 @@ export default class DrawHelper {
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
const barWidth = w - w1 - w2 - fh2;
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarColor);
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, 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.statusBarRgbaColor);
}
if (w1 <= w)
@@ -182,16 +223,16 @@ export default class DrawHelper {
return out;
}
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
let out = `<div class="layout" style="` +
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
`color: ${this.statusBarColor}">`;
`color: ${this.statusBarRgbaColor}">`;
const fontSize = statusBarHeight*0.75;
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 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.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>';
return out;
@@ -267,7 +308,7 @@ export default class DrawHelper {
}
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
const s = this.w + this.fontSize;
const s = this.boxW + this.fontSize;
if (isDown) {
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,33 +1,43 @@
<template>
<div ref="main" class="main">
<div class="layout back" @wheel.prevent.stop="onMouseWheel">
<div v-html="background"></div>
<!-- img -->
<div class="absolute" v-html="background"></div>
<div class="absolute" v-html="pageDivider"></div>
</div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div v-html="page1"></div>
<div @copy.prevent="copyText" v-html="page1"></div>
</div>
</div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div v-html="page2"></div>
<div @copy.prevent="copyText" v-html="page2"></div>
</div>
</div>
<div v-show="showStatusBar" ref="statusBar" class="layout">
<div v-html="statusBar"></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"
@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
@click.prevent.stop="onStatusBarClick"></div>
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
>
<div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div>
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick">
<div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
>
</div>
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
</div>
@@ -35,12 +45,16 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../../vueComponent.js';
import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import he from 'he';
import './TextPage.css';
import * as utils from '../../../share/utils';
import {sleep} from '../../../share/utils';
import bookManager from '../share/bookManager';
import DrawHelper from './DrawHelper';
import rstore from '../../../store/modules/reader';
@@ -48,11 +62,18 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100;
export default @Component({
const componentOptions = {
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
this.draw();
if (this.userBookPosChange) {
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
this.prevBookPos = this.bookPos;
this.userBookPosChange = false;
}
},
bookPosSeen: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -67,13 +88,16 @@ export default @Component({
this.updateLayout();
},
},
})
class TextPage extends Vue {
};
class TextPage {
_options = componentOptions;
toggleLayout = false;
showStatusBar = false;
clickControl = true;
background = null;
pageDivider = null;
page1 = null;
page2 = null;
statusBar = null;
@@ -82,6 +106,8 @@ class TextPage extends Vue {
lastBook = null;
bookPos = 0;
bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null;
fontSize = null;
@@ -110,7 +136,11 @@ class TextPage extends Vue {
this.debouncedDrawStatusBar = _.throttle(() => {
this.drawStatusBar();
}, 60);
}, 60);
this.debouncedDrawPageDividerAndOrnament = _.throttle(() => {
this.drawPageDividerAndOrnament();
}, 65);
this.debouncedLoadSettings = _.debounce(() => {
this.loadSettings();
@@ -132,9 +162,9 @@ class TextPage extends Vue {
await this.doPageAnimation();
}, 10);
this.$root.$on('resize', async() => {
this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize);
await sleep(500);
await utils.sleep(200);
this.$nextTick(this.onResize);
});
}
@@ -152,7 +182,7 @@ class TextPage extends Vue {
const wideLetter = 'Щ';
//preloaded fonts
this.fontList = [`12px ${this.fontName}`];
this.fontList = [`12px '${this.fontName}'`];
//widths
this.realWidth = this.$refs.main.clientWidth;
@@ -161,14 +191,16 @@ class TextPage extends Vue {
this.$refs.layoutEvents.style.width = this.realWidth + '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.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.$refs.scrollingPage2.style.width = this.w + 'px';
this.lineHeight = this.fontSize + this.lineInterval;
this.pageRowsCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
this.pageLineCount = (this.dualPageMode ? this.pageRowsCount*2 : this.pageRowsCount)
//stuff
this.currentAnimation = '';
@@ -180,7 +212,10 @@ class TextPage extends Vue {
this.$refs.statusBar.style.left = '0px';
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
this.drawHelper.realWidth = this.realWidth;
@@ -188,10 +223,20 @@ class TextPage extends Vue {
this.drawHelper.lastBook = this.lastBook;
this.drawHelper.book = this.book;
this.drawHelper.parsed = this.parsed;
this.drawHelper.pageRowsCount = this.pageRowsCount;
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.statusBarColor = this.statusBarColor;
this.drawHelper.statusBarRgbaColor = this.statusBarRgbaColor;
this.drawHelper.fontStyle = this.fontStyle;
this.drawHelper.fontWeight = this.fontWeight;
this.drawHelper.fontSize = this.fontSize;
@@ -200,6 +245,7 @@ class TextPage extends Vue {
this.drawHelper.textColor = this.textColor;
this.drawHelper.textShift = this.textShift;
this.drawHelper.p = this.p;
this.drawHelper.boxW = this.boxW;
this.drawHelper.w = this.w;
this.drawHelper.h = this.h;
this.drawHelper.indentLR = this.indentLR;
@@ -228,32 +274,33 @@ class TextPage extends Vue {
//parsed
if (this.parsed) {
this.parsed.p = this.p;
this.parsed.w = this.w;// px, ширина текста
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, {}))
let wideLine = wideLetter;
if (!this.drawHelper.measureText(wideLine, {}))
throw new Error('Ошибка measureText');
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
this.parsed.maxWordLength = t.length - 1;
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
this.parsed.lineHeight = this.lineHeight;
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;
while (this.drawHelper.measureText(wideLine, {}) < this.w) wideLine += wideLetter;
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
this.parsed.setSettings({
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
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
const pageSpace = this.scrollHeight - this.pageRowsCount*this.lineHeight;
let top = pageSpace/2;
if (this.showStatusBar)
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
@@ -262,14 +309,14 @@ class TextPage extends Vue {
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.top = page2.top = top + 'px';
page1.left = page2.left = this.indentLR + 'px';
page1 = this.$refs.scrollingPage1.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';
}
@@ -285,7 +332,7 @@ class TextPage extends Vue {
let close = null;
(async() => {
await sleep(500);
await utils.sleep(500);
if (this.fontsLoading)
close = this.$root.notify.info('Загрузка шрифта &nbsp;<i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
})();
@@ -333,20 +380,36 @@ class TextPage extends Vue {
if (!omitLoadFonts)
await this.loadFonts();
this.draw();
if (omitLoadFonts) {
this.draw();
} else {
// ширина шрифта некоторое время выдается неверно,
// не удалось событийно отловить этот момент, поэтому костыль
while (this.checkingFont) {
this.stopCheckingFont = true;
await utils.sleep(100);
}
// ширина шрифта некоторое время выдается неверно, поэтому
if (!omitLoadFonts) {
const parsed = this.parsed;
this.checkingFont = true;
this.stopCheckingFont = false;
try {
const parsed = this.parsed;
let i = 0;
const t = this.parsed.testText;
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
await sleep(100);
if (this.parsed === parsed) {
this.parsed.testWidth = this.drawHelper.measureText(t, {});
this.draw();
let i = 0;
const t = 'Это тестовый текст. Его ширина выдается системой неправильно некоторое время.';
let twprev = 0;
//5 секунд проверяем изменения шрифта
while (!this.stopCheckingFont && i++ < 50 && this.parsed === parsed) {
const tw = this.drawHelper.measureText(t, {});
if (tw !== twprev) {
this.parsed.setSettings({testWidth: tw});
this.draw();
twprev = tw;
}
await utils.sleep(100);
}
} finally {
this.checkingFont = false;
}
}
}
@@ -366,7 +429,6 @@ class TextPage extends Vue {
this.updateLayout();
this.book = null;
this.meta = null;
this.fb2 = null;
this.parsed = null;
this.linesUp = null;
@@ -383,7 +445,7 @@ class TextPage extends Vue {
try {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await sleep(10);
while (this.doingLazyParse) await utils.sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
@@ -392,23 +454,11 @@ class TextPage extends Vue {
this.book = await bookManager.getBook(this.lastBook);
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
const bt = utils.getBookTitle(this.meta.fb2);
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = bt.fullTitle;
this.title = _.compact([
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
this.$root.$emit('set-app-title', this.title);
this.$root.setAppTitle(this.title);
this.parsed = this.book.parsed;
@@ -443,17 +493,40 @@ class TextPage extends Vue {
}
setBackground() {
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` background-color: ${this.backgroundColor}"></div>`;
if (this.wallpaperIgnoreStatusBar) {
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() {
if (this.resizing)
return;
this.resizing = true;
try {
const scrolled = this.doingScrolling;
if (scrolled)
await this.stopTextScrolling();
this.calcDrawProps();
this.setBackground();
this.draw();
if (scrolled)
this.startTextScrolling();
} catch (e) {
//
} finally {
this.resizing = false;
}
}
@@ -462,7 +535,7 @@ class TextPage extends Vue {
}
get font() {
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontName}`;
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
}
onPage1TransitionEnd() {
@@ -493,7 +566,7 @@ class TextPage extends Vue {
let wait = (timeout + 201)/100;
while (wait > 0 && !this[stopPropertyName]) {
wait--;
await sleep(100);
await utils.sleep(100);
}
resolve();
})().catch(reject); });
@@ -503,13 +576,13 @@ class TextPage extends Vue {
async startTextScrolling() {
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
this.linesDown.length <= this.pageLineCount) {
this.linesDown.length <= this.pageLineCount || this.dualPageMode) {
this.doStopScrolling();
return;
}
//ждем анимацию
while (this.inAnimation) await sleep(10);
while (this.inAnimation) await utils.sleep(10);
this.stopScrolling = false;
this.doingScrolling = true;
@@ -520,7 +593,7 @@ class TextPage extends Vue {
this.page1 = this.page2;
this.toggleLayout = true;
await this.$nextTick();
await sleep(50);
await utils.sleep(50);
this.cachedPos = -1;
this.draw();
@@ -557,7 +630,7 @@ class TextPage extends Vue {
page.style.transform = 'none';
page.offsetHeight;
while (this.doingScrolling) await sleep(10);
while (this.doingScrolling) await utils.sleep(10);
}
draw() {
@@ -601,7 +674,7 @@ class TextPage extends Vue {
}
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
@@ -621,9 +694,10 @@ class TextPage extends Vue {
if (!this.pageChangeAnimation)
this.debouncedPrepareNextPage();
this.debouncedDrawStatusBar();
this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
}
@@ -735,8 +809,24 @@ class TextPage extends Vue {
message = this.statusBarMessage;
if (!message)
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,
lines[i].end, this.parsed.textLength, message);
lines[i].end, this.parsed.textLength, message, imageNum, this.parsed.images.length);
this.bookPosSeen = lines[i].end;
}
} else {
@@ -744,6 +834,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) {
if (state === 'finish') {
this.statusBarMessage = '';
@@ -766,7 +875,7 @@ class TextPage extends Vue {
for (let i = 0; i < this.parsed.para.length; i++) {
j++;
if (j > 1) {
await sleep(1);
await utils.sleep(1);
j = 0;
}
if (this.stopLazyParse)
@@ -788,7 +897,7 @@ class TextPage extends Vue {
async refreshTime() {
if (!this.timeRefreshing) {
this.timeRefreshing = true;
await sleep(60*1000);
await utils.sleep(60*1000);
if (this.book && this.parsed.textLength) {
this.debouncedDrawStatusBar();
@@ -824,12 +933,14 @@ class TextPage extends Vue {
doDown() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesDown[1].begin;
}
}
doUp() {
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesUp[1].begin;
}
}
@@ -839,9 +950,10 @@ class TextPage extends Vue {
let i = this.pageLineCount;
if (this.keepLastToFirst)
i--;
if (i >= 0 && this.linesDown.length >= 2*i) {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = this.linesDown[i].begin;
} else
this.doEnd();
@@ -857,6 +969,7 @@ class TextPage extends Vue {
if (i >= 0 && this.linesUp.length > i) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = this.linesUp[i].begin;
}
}
@@ -865,10 +978,11 @@ class TextPage extends Vue {
doHome() {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = 0;
}
doEnd(noAni) {
doEnd(noAni, isUser = true) {
if (this.parsed.para.length && this.pageLineCount > 0) {
let i = this.parsed.para.length - 1;
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
@@ -879,6 +993,7 @@ class TextPage extends Vue {
if (!noAni)
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = isUser;
this.bookPos = lines[i].begin;
}
}
@@ -904,9 +1019,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
this.commit('reader/setSettings', newSettings);
await sleep(50);
this.commit('reader/setSettings', {fontSize: newSize});
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -915,9 +1029,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
this.commit('reader/setSettings', newSettings);
await sleep(50);
this.commit('reader/setSettings', {fontSize: newSize});
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -926,9 +1039,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
this.commit('reader/setSettings', newSettings);
await sleep(50);
this.commit('reader/setSettings', {scrollingDelay: newDelay});
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -937,9 +1049,8 @@ class TextPage extends Vue {
if (!this.settingsChanging) {
this.settingsChanging = true;
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
const newSettings = Object.assign({}, this.settings, {scrollingDelay: newDelay});
this.commit('reader/setSettings', newSettings);
await sleep(50);
this.commit('reader/setSettings', {scrollingDelay: newDelay});
await utils.sleep(50);
this.settingsChanging = false;
}
}
@@ -953,7 +1064,7 @@ class TextPage extends Vue {
let delay = 400;
while (this.repDoing) {
this.handleClick(pointX, pointY);
await sleep(delay);
await utils.sleep(delay);
if (delay > 15)
delay *= 0.8;
}
@@ -966,7 +1077,7 @@ class TextPage extends Vue {
}
onTouchStart(event) {
if (!this.$isMobileDevice)
if (!this.$root.isMobileDevice)
return;
this.endClickRepeat();
@@ -994,7 +1105,7 @@ class TextPage extends Vue {
}
onTouchEnd(event) {
if (!this.$isMobileDevice)
if (!this.$root.isMobileDevice)
return;
this.endClickRepeat();
@@ -1030,13 +1141,13 @@ class TextPage extends Vue {
}
onTouchCancel() {
if (!this.$isMobileDevice)
if (!this.$root.isMobileDevice)
return;
this.endClickRepeat();
}
onMouseDown(event) {
if (this.$isMobileDevice)
if (this.$root.isMobileDevice)
return;
this.endClickRepeat();
if (event.button == 0) {
@@ -1053,13 +1164,13 @@ class TextPage extends Vue {
}
onMouseUp() {
if (this.$isMobileDevice)
if (this.$root.isMobileDevice)
return;
this.endClickRepeat();
}
onMouseWheel(event) {
if (this.$isMobileDevice)
if (this.$root.isMobileDevice)
return;
if (event.deltaY > 0) {
this.doDown();
@@ -1070,7 +1181,7 @@ class TextPage extends Vue {
onStatusBarClick() {
const url = this.meta.url;
if (url && url.indexOf('file://') != 0) {
if (url && url.indexOf('disk://') != 0) {
window.open(url, '_blank');
} else {
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
@@ -1122,9 +1233,57 @@ class TextPage extends Vue {
}
return action;
}
}
copyText(event) {
//все это для того, чтобы правильно расставить переносы \n при копировании текста
//прямо с текущей страницы
//подготовка, вытаскиваем весь текст страницы
const lines = this.getLines(this.bookPos);
const decodedLines = [];
for (const line of lines.linesDown) {
let lineText = '';
for (const part of line.parts) {
lineText += part.text;
}
decodedLines.push({text: he.decode(lineText), first: line.first});
}
let i = 0;
const findDecoded = (line) => {
for (let j = i; j < decodedLines.length; j++) {
const decoded = decodedLines[j];
if (decoded.text.indexOf(line) >= 0) {
i = j;
return decoded;
}
}
return;
}
const selection = document.getSelection();
const splitted = selection.toString().split(/[\n\r]/);
let filtered = '';
//формируем filtered, учитывая переносы из decodedLines
for (const line of splitted) {
const found = findDecoded(line);
if (found && found.first) {
filtered += (filtered ? '\n' : '') + line;
} else {
filtered += (filtered ? '\r ' : '') + line;
}
}
//маленькие хитрости, убираем переносы по слогам
filtered = filtered.replace(/-\r /g, '').replace(/\r /g, ' ');
event.clipboardData.setData('text/plain', filtered);
}
}
export default vueComponent(TextPage);
//-----------------------------------------------------------------------------
</script>
<style scoped>
@@ -1162,60 +1321,3 @@ class TextPage extends Vue {
}
</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,26 +1,59 @@
import he from 'he';
import sax from '../../../../server/core/sax';
import {sleep} from '../../../share/utils';
import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
const maxParaTextLength = 10000;
// 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 {
constructor(settings) {
if (settings) {
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
}
constructor(settings = {}) {
this.sets = {};
// defaults
this.p = 30;// px, отступ параграфа
this.w = 300;// px, ширина страницы
this.wordWrap = false;// перенос по слогам
//заглушка
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
};
this.setSettings(defaultSettings);
this.setSettings(settings);
}
setSettings(settings = {}) {
this.sets = Object.assign({}, this.sets, settings);
this.measureText = this.sets.measureText;
}
async parse(data, callback) {
if (!callback)
callback = () => {};
@@ -46,11 +79,23 @@ export default class BookParser {
let isFirstSection = true;
let isFirstTitlePara = false;
//изображения
this.binary = {};
let binaryId = '';
let binaryType = '';
let dimPromises = [];
//оглавление
this.contents = [];
this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
let imageNum = 0;
let paraIndex = -1;
let paraOffset = 0;
let para = []; /*array of
@@ -59,12 +104,12 @@ export default class BookParser {
offset: Number, //сумма всех length до этого параграфа
length: Number, //длина text без тегов
text: String, //текст параграфа с вложенными тегами
cut: Boolean, //параграф - кандидат на сокрытие (cutEmptyParagraphs)
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
}
*/
const getImageDimensions = (binaryId, binaryType, data) => {
return new Promise ((resolve, reject) => { (async() => {
data = data.replace(/[\n\r\s]/g, '');
const i = new Image();
let resolved = false;
i.onload = () => {
@@ -81,7 +126,7 @@ export default class BookParser {
i.onerror = reject;
i.src = `data:${binaryType};base64,${data}`;
await sleep(30*1000);
await utils.sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
})().catch(reject); });
@@ -103,63 +148,114 @@ export default class BookParser {
i.onerror = reject;
i.src = src;
await sleep(30*1000);
await utils.sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
})().catch(reject); });
};
const newParagraph = (text, len, addIndex) => {
paraIndex++;
let p = {
index: paraIndex,
offset: paraOffset,
length: len,
text: text,
cut: (!addIndex && (len == 1 && text[0] == ' ')),
addIndex: (addIndex ? addIndex : 0),
};
para[paraIndex] = p;
paraOffset += p.length;
};
const growParagraph = (text, len) => {
if (paraIndex < 0) {
newParagraph(' ', 1);
growParagraph(text, len);
return;
}
let p = para[paraIndex];
//добавление пустых (addEmptyParagraphs) параграфов
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
paraIndex--;
const correctCurrentPara = () => {
//коррекция текущего параграфа
if (paraIndex >= 0) {
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
for (let i = 0; i < 2; i++) {
newParagraph(' ', 1, i + 1);
//уберем пробелы с концов параграфа, минимум 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;
}
paraIndex++;
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
paraOffset += p.length;
}
};
paraOffset -= p.length;
//параграф оказался непустой
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
p.length = 0;
p.text = p.text.substr(1);
p.cut = (len == 1 && text[0] == ' ');
const newParagraph = (text = '', len = 0) => {
correctCurrentPara();
//новый параграф
paraIndex++;
let p = {
index: paraIndex,
offset: paraOffset,
length: len,//длина текста внутри параграфа без учета длины разметки
text: text,
addIndex: 0,
};
if (inSubtitle) {
curSubtitle.title += '<p>';
} else if (inTitle) {
curTitle.title += '<p>';
}
para[paraIndex] = p;
paraOffset += len;
};
const growParagraph = (text, len, textRaw) => {
if (paraIndex < 0) {
newParagraph();
growParagraph(text, len);
return;
}
//ограничение на размер куска текста в параграфе
if (textRaw && textRaw.length > maxParaTextLength) {
while (textRaw.length > 0) {
const textPart = textRaw.substring(0, maxParaTextLength);
textRaw = textRaw.substring(maxParaTextLength);
newParagraph();
growParagraph(textPart, textPart.length);
}
return;
}
if (inSubtitle) {
curSubtitle.title += text;
} else if (inTitle) {
curTitle.title += text;
}
const p = para[paraIndex];
p.length += len;
p.text += text;
para[paraIndex] = p;
paraOffset += p.length;
paraOffset += len;
};
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
@@ -167,12 +263,13 @@ export default class BookParser {
return;
tag = elemName;
path += '/' + elemName;
path += '/' + tag;
if (tag == 'binary') {
let attrs = sax.getAttrsSync(tail);
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 : '');
}
@@ -180,53 +277,111 @@ export default class BookParser {
let attrs = sax.getAttrsSync(tail);
if (attrs.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 (inPara && !this.showInlineImagesInCenter && !center)
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
imageNum++;
if (inPara && !this.sets.showInlineImagesInCenter && !center)
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
else
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
if (inPara && this.showInlineImagesInCenter)
newParagraph(' ', 1);
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
this.images.push({paraIndex, num: imageNum, id, local, alt});
if (inPara && this.sets.showInlineImagesInCenter)
newParagraph();
} else {//external
dimPromises.push(getExternalImageDimensions(href));
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
imageNum++;
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 (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author)
fb2.author = [];
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 (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)
newParagraph(' ', 1);
newParagraph();
isFirstBody = false;
bodyIndex++;
}
if (tag == 'title') {
newParagraph(' ', 1);
newParagraph();
isFirstTitlePara = true;
bold = true;
center = true;
inTitle = true;
curTitle = {paraIndex, title: '', inset: sectionLevel, bodyIndex, subtitles: []};
this.contents.push(curTitle);
}
if (tag == 'section') {
if (!isFirstSection)
newParagraph(' ', 1);
newParagraph();
isFirstSection = false;
sectionLevel++;
}
if (tag == 'emphasis' || tag == 'strong') {
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`<${tag}>`, 0);
}
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
if (!(tag == 'p' && isFirstTitlePara))
newParagraph(' ', 1);
newParagraph();
if (tag == 'p') {
inPara = true;
isFirstTitlePara = false;
@@ -234,23 +389,33 @@ export default class BookParser {
}
if (tag == 'subtitle') {
newParagraph(' ', 1);
newParagraph();
isFirstTitlePara = true;
bold = true;
center = true;
if (curTitle.paraIndex < 0) {
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
this.contents.push(curTitle);
}
inSubtitle = true;
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
curTitle.subtitles.push(curSubtitle);
}
if (tag == 'epigraph') {
if (tag == 'epigraph' || tag == 'annotation') {
italic = true;
space += 1;
}
if (tag == 'poem') {
newParagraph(' ', 1);
newParagraph();
}
if (tag == 'text-author') {
newParagraph(' ', 1);
newParagraph();
bold = true;
space += 1;
}
}
@@ -267,9 +432,14 @@ export default class BookParser {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
if (tag == 'emphasis' || tag == 'strong') {
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0);
}
@@ -281,18 +451,21 @@ export default class BookParser {
isFirstTitlePara = false;
bold = false;
center = false;
inSubtitle = false;
}
if (tag == 'epigraph') {
if (tag == 'epigraph' || tag == 'annotation') {
italic = false;
space -= 1;
newParagraph();
}
if (tag == 'stanza') {
newParagraph(' ', 1);
newParagraph();
}
if (tag == 'text-author') {
bold = false;
space -= 1;
}
}
@@ -309,17 +482,14 @@ export default class BookParser {
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
text = he.decode(text);
text = text.replace(/>/g, '&gt;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/[\t\n\r\xa0]/g, ' ');
if (text && text.trim() == '')
text = (text.indexOf(' ') >= 0 ? ' ' : '');
text = ' ';
if (!text)
return;
text = text.replace(/[\t\n\r\xa0]/g, ' ');
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
switch (path) {
case '/fictionbook/description/title-info/author/first-name':
@@ -357,35 +527,43 @@ export default class BookParser {
fb2.annotation += text;
}
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 (!this.sets.isTesting) {
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
} else {
dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
}
}
if (path.indexOf('/fictionbook/body/title') == 0 ||
path.indexOf('/fictionbook/body/section') == 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) {
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
if (text != ' ')
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
}
};
const onProgress = async(prog) => {
await sleep(1);
await utils.sleep(1);
callback(prog);
};
await sax.parse(data, {
onStartNode, onEndNode, onTextNode, onProgress
});
correctCurrentPara();
if (dimPromises.length) {
try {
@@ -401,11 +579,20 @@ export default class BookParser {
this.textLength = paraOffset;
callback(100);
await sleep(10);
await utils.sleep(10);
return {fb2};
}
imageHrefToId(id) {
let local = false;
if (id[0] == '#') {
id = id.substr(1);
local = true;
}
return {id, local};
}
findParaIndex(bookPos) {
let result = undefined;
//дихотомия
@@ -430,16 +617,26 @@ export default class BookParser {
splitToStyle(s) {
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},
text: String,
}*/
let style = {};
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
result.push({
style: Object.assign({}, style),
style: copyStyle(style),
image,
text
});
@@ -453,6 +650,12 @@ export default class BookParser {
case 'emphasis':
style.italic = true;
break;
case 'sup':
style.sup = true;
break;
case 'sub':
style.sub = true;
break;
case 'center':
style.center = true;
break;
@@ -465,28 +668,21 @@ export default class BookParser {
case 'image': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
let id = attrs.href.value;
let local = false;
if (id[0] == '#') {
id = id.substr(1);
local = true;
}
image = {local, inline: false, id};
image = this.imageHrefToId(attrs.href.value);
image.inline = false;
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
}
break;
}
case 'image-inline': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
let id = attrs.href.value;
let local = false;
if (id[0] == '#') {
id = id.substr(1);
local = true;
}
const img = this.imageHrefToId(attrs.href.value);
img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({
style: Object.assign({}, style),
image: {local, inline: true, id},
style: copyStyle(style),
image: img,
text: ''
});
}
@@ -503,6 +699,12 @@ export default class BookParser {
case 'emphasis':
style.italic = false;
break;
case 'sup':
style.sup = false;
break;
case 'sub':
style.sub = false;
break;
case 'center':
style.center = false;
break;
@@ -522,7 +724,7 @@ export default class BookParser {
});
//длинные слова (или белиберду без пробелов) тоже разобьем
const maxWordLength = this.maxWordLength;
const maxWordLength = this.sets.maxWordLength;
const parts = result;
result = [];
for (const part of parts) {
@@ -535,7 +737,7 @@ export default class BookParser {
spaceIndex = i;
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)});
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
spaceIndex = -1;
@@ -553,90 +755,88 @@ export default class BookParser {
splitToSlogi(word) {
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;
word += ' ';
for (let i = 0; i < len; i++) {
slog += word[i];
if (alpha.has(word[i]))
slogLen++;
if (len > 3) {
let slog = '';
let slogLen = 0;
word += ' ';
for (let i = 0; i < len; i++) {
slog += word[i];
if (alpha.has(word[i]))
slogLen++;
if (slogLen > 1 && i < len - 2 && (
//гласная, а следом не 2 согласные буквы
(glas.has(word[i]) && !(soglas.has(word[i + 1]) &&
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) ||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) &&
soglas.has(word[i]) && soglas.has(word[i + 1]) &&
(glas.has(word[i + 2]) || soglas.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])) ||
(word[i] == '-')
) &&
//нельзя оставлять окончания на ь, ъ, й
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
if (slogLen > 1 && i < len - 2 && (
//гласная, а следом не 2 согласные буквы
(glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) ||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
( glas.has(word[i + 2]) || soglas.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])) ||
(word[i] == '-')
) &&
//нельзя оставлять окончания на ь, ъ, й
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
) {
result.push(slog);
slog = '';
slogLen = 0;
) {
result.push(slog);
slog = '';
slogLen = 0;
}
}
if (slog)
result.push(slog);
} else {
result.push(word);
}
if (slog)
result.push(slog);
return result;
}
parsePara(paraIndex) {
const para = this.para[paraIndex];
const s = this.sets;
//перераспарсиваем только при изменении одного из параметров
if (!this.force &&
para.parsed &&
para.parsed.testWidth === this.testWidth &&
para.parsed.w === this.w &&
para.parsed.p === this.p &&
para.parsed.wordWrap === this.wordWrap &&
para.parsed.maxWordLength === this.maxWordLength &&
para.parsed.font === this.font &&
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs &&
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
para.parsed.showImages === this.showImages &&
para.parsed.imageHeightLines === this.imageHeightLines &&
para.parsed.imageFitWidth === this.imageFitWidth &&
para.parsed.compactTextPerc === this.compactTextPerc
para.parsed.p === s.p &&
para.parsed.w === s.w &&
para.parsed.font === s.font &&
para.parsed.fontSize === s.fontSize &&
para.parsed.wordWrap === s.wordWrap &&
para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
para.parsed.maxWordLength === s.maxWordLength &&
para.parsed.lineHeight === s.lineHeight &&
para.parsed.showImages === s.showImages &&
para.parsed.imageHeightLines === s.imageHeightLines &&
para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
para.parsed.compactTextPerc === s.compactTextPerc &&
para.parsed.testWidth === s.testWidth
)
return para.parsed;
const parsed = {
testWidth: this.testWidth,
w: this.w,
p: this.p,
wordWrap: this.wordWrap,
maxWordLength: this.maxWordLength,
font: this.font,
cutEmptyParagraphs: this.cutEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs,
showImages: this.showImages,
imageHeightLines: this.imageHeightLines,
imageFitWidth: this.imageFitWidth,
compactTextPerc: this.compactTextPerc,
visible: !(
(this.cutEmptyParagraphs && para.cut) ||
(para.addIndex > this.addEmptyParagraphs)
)
p: s.p,
w: s.w,
font: s.font,
fontSize: s.fontSize,
wordWrap: s.wordWrap,
cutEmptyParagraphs: s.cutEmptyParagraphs,
addEmptyParagraphs: s.addEmptyParagraphs,
maxWordLength: s.maxWordLength,
lineHeight: s.lineHeight,
showImages: s.showImages,
imageHeightLines: s.imageHeightLines,
imageFitWidth: (s.imageFitWidth || s.dualPageMode),
compactTextPerc: s.compactTextPerc,
testWidth: s.testWidth,
visible: true, //вычисляется позже
};
@@ -652,9 +852,12 @@ export default class BookParser {
text: String,
}
}*/
let parts = this.splitToStyle(para.text);
//инициализация парсера
let line = {begin: para.offset, parts: []};
let paragraphText = '';//текст параграфа
let partText = '';//накапливаемый кусок со стилем
let str = '';//измеряемая строка
@@ -663,26 +866,28 @@ export default class BookParser {
let style = {};
let ofs = 0;//смещение от начала параграфа para.offset
let imgW = 0;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
let imageInPara = false;
const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) {
style = part.style;
paragraphText += part.text;
//изображения
if (part.image.id && !part.image.inline) {
parsed.visible = this.showImages;
imageInPara = true;
let bin = this.binary[part.image.id];
if (!bin)
bin = {h: 1, w: 1};
let lineCount = this.imageHeightLines;
let c = Math.ceil(bin.h/this.lineHeight);
let lineCount = parsed.imageHeightLines;
let c = Math.ceil(bin.h/parsed.lineHeight);
const maxH = lineCount*this.lineHeight;
const maxH = lineCount*parsed.lineHeight;
let maxH2 = maxH;
if (this.imageFitWidth && bin.w > this.w) {
maxH2 = bin.h*this.w/bin.w;
c = Math.ceil(maxH2/this.lineHeight);
if (parsed.imageFitWidth && bin.w > parsed.w) {
maxH2 = bin.h*parsed.w/bin.w;
c = Math.ceil(maxH2/parsed.lineHeight);
}
lineCount = (c < lineCount ? c : lineCount);
@@ -705,6 +910,7 @@ export default class BookParser {
paraIndex,
w: imageWidth,
h: imageHeight,
num: part.image.num
}});
lines.push(line);
line = {begin: line.end + 1, parts: []};
@@ -715,19 +921,19 @@ export default class BookParser {
line.last = true;
line.parts.push({style, text: ' ',
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;
}
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];
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;
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}});
}
}
@@ -837,6 +1043,16 @@ export default class BookParser {
}
}
//parsed.visible
if (imageInPara) {
parsed.visible = parsed.showImages;
} else {
parsed.visible = !(
(para.addIndex > parsed.addEmptyParagraphs) ||
(para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
);
}
parsed.lines = lines;
para.parsed = parsed;

View File

@@ -1,21 +1,26 @@
import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
const maxDataSize = 300*1024*1024;//compressed bytes
const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
name: 'bmMetaStore'
});
//локальный кэш самих книг, ограничение maxDataSize
const bmDataStore = localForage.createInstance({
name: 'bmDataStore'
});
const bmRecentStore = localForage.createInstance({
name: 'bmRecentStore'
//список недавно открытых книг
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
class BookManager {
@@ -25,15 +30,41 @@ class BookManager {
this.eventListeners = [];
this.books = {};
this.recent = {};
this.recentLast = await bmRecentStore.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
this.recent = {};
this.saveRecent = _.debounce(() => {
bmRecentStoreNew.setItem('recent', this.recent);
}, 300, {maxWait: 800});
this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < maxRecentLength ? 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;
//конвертируем в новые ключи
await this.convertRecent();
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;
@@ -41,9 +72,41 @@ class BookManager {
this.loadStored();//no await
}
//Долгая асинхронная загрузка из хранилища.
//Хранение в отдельных записях дает относительно
//нормальное поведение при нескольких вкладках с читалкой в браузере.
//TODO: убрать в 2025г
async convertRecent() {
const converted = await bmRecentStoreNew.getItem('recent-converted');
if (converted)
return;
const newRecent = {};
for (const book of Object.values(this.recent)) {
if (!book.path) {
continue;
}
const newKey = this.keyFromPath(book.path);
newRecent[newKey] = _.cloneDeep(book);
newRecent[newKey].key = newKey;
if (!newRecent[newKey].loadTime)
newRecent[newKey].loadTime = newRecent[newKey].addTime;
}
this.recent = newRecent;
//console.log(converted);
(async() => {
await utils.sleep(3000);
this.saveRecent();
this.emit('recent-changed');
this.emit('set-recent');
await bmRecentStoreNew.setItem('recent-converted', true);
})();
}
//Ленивая асинхронная загрузка bmMetaStore
async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
await utils.sleep(2000);
@@ -70,32 +133,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.cleanRecentBooks();
this.recentChanged = true;
this.loaded = true;
this.emit('load-stored-finish');
}
@@ -125,7 +163,7 @@ class BookManager {
}
async deflateWithProgress(data, callback) {
const chunkSize = 128*1024;
const chunkSize = 512*1024;
const deflator = new utils.pako.Deflate({level: 5});
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
@@ -159,7 +197,7 @@ class BookManager {
}
async inflateWithProgress(data, callback) {
const chunkSize = 64*1024;
const chunkSize = 512*1024;
const inflator = new utils.pako.Inflate({to: 'string'});
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
@@ -194,8 +232,8 @@ class BookManager {
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now();
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => {
const p = Math.round(30*perc/100);
@@ -230,15 +268,15 @@ class BookManager {
async hasBookParsed(meta) {
if (!this.books)
return false;
if (!meta.url)
if (!meta.path)
return false;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
let book = this.books[meta.key];
if (!book && !this.loaded) {
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (book)
this.books[meta.key] = book;
}
@@ -248,22 +286,21 @@ class BookManager {
async getBook(meta, callback) {
let result = undefined;
if (!meta.path)
return;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
result = this.books[meta.key];
if (!result) {
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (result)
this.books[meta.key] = result;
}
//Если файл на сервере изменился, считаем, что в кеше его нету
if (meta.path && result && meta.path != result.path) {
return;
}
if (result && !result.parsed) {
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5);
@@ -323,95 +360,127 @@ class BookManager {
return result;
}
keyFromUrl(url) {
/*keyFromUrl(url) {
return utils.stringToHex(url);
}*/
keyFromPath(bookPath) {
return path.basename(bookPath);
}
keysEqual(bookPath1, bookPath2) {
if (bookPath1 === undefined || bookPath2 === undefined)
return false;
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
}
//-- recent --------------------------------------------------------------
async setRecentBook(value) {
const result = this.metaOnly(value);
result.touchTime = Date.now();
result.deleted = 0;
if (this.recent[result.key] && this.recent[result.key].deleted) {
//восстановим из небытия пользовательские данные
if (!result.bookPos)
result.bookPos = this.recent[result.key].bookPos;
if (!result.bookPosSeen)
result.bookPosSeen = this.recent[result.key].bookPosSeen;
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;
}
this.recent[result.key] = result;
const prevKey = (this.recentItem ? this.recentItem.key : '');
if (item) {
this.recent[item.key] = item;
this.recentItem = item;
} else {
this.recentItem = null;
}
await bmRecentStore.setItem(result.key, result);
this.saveRecentItem();
this.recentLast = result;
await bmRecentStore.setItem('recent-last', this.recentLast);
if (!item || prevKey != item.key) {
this.saveRecent();
}
this.recentChanged = true;
this.emit('recent-changed', result.key);
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) {
let result = this.metaOnly(value);
result.touchTime = Date.now();//время последнего чтения
if (!result.loadTime)
result.loadTime = Date.now();//время загрузки файла
result.deleted = 0;
if (this.recent[result.key]) {
result = Object.assign({}, this.recent[result.key], result);
}
await this.recentSetLastKey(result.key);
await this.recentSetItem(result);
return result;
}
async getRecentBook(value) {
let result = this.recent[value.key];
if (!result) {
result = await bmRecentStore.getItem(value.key);
if (result)
this.recent[value.key] = result;
}
return result;
return this.recent[value.key];
}
async delRecentBook(value) {
this.recent[value.key].deleted = 1;
await bmRecentStore.setItem(value.key, this.recent[value.key]);
const item = this.recent[value.key];
item.deleted = 1;
if (this.recentLast.key == value.key) {
this.recentLast = null;
await bmRecentStore.setItem('recent-last', this.recentLast);
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
}
await this.recentSetItem(item);
this.emit('recent-deleted', value.key);
this.emit('recent-changed', value.key);
}
async cleanRecentBooks() {
const sorted = this.getSortedRecent();
let isDel = false;
for (let i = 1000; i < sorted.length; i++) {
await bmRecentStore.removeItem(sorted[i].key);
for (let i = maxRecentLength; i < sorted.length; i++) {
delete this.recent[sorted[i].key];
await bmRecentStore.removeItem(sorted[i].key);
isDel = true;
}
this.sortedRecentCached = null;
if (isDel)
this.emit('recent-changed');
await this.recentSetItem();
return isDel;
}
mostRecentBook() {
if (this.recentLast) {
return this.recentLast;
if (this.recentLastKey) {
return this.recent[this.recentLastKey];
}
const oldRecentLast = this.recentLast;
const oldKey = this.recentLastKey;
let max = 0;
let result = null;
for (let key in this.recent) {
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.touchTime > max) {
max = book.touchTime;
result = book;
}
}
this.recentLast = result;
bmRecentStore.setItem('recent-last', this.recentLast);//no await
const newRecentLastKey = (result ? result.key : null);
this.recentSetLastKey(newRecentLastKey);//no await
if (this.recentLast !== oldRecentLast)
if (newRecentLastKey !== oldKey)
this.emit('recent-changed');
return result;
@@ -431,6 +500,43 @@ class BookManager {
return result;
}
findRecentByUrlAndPath(url, bookPath) {
if (bookPath) {
const key = this.keyFromPath(bookPath);
const book = this.recent[key];
if (book && !book.deleted)
return book;
}
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.url == url && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
findRecentBySameBookKey(sameKey) {
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent);
@@ -442,24 +548,12 @@ class BookManager {
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.recentLast = null;
await bmRecentStore.setItem('recent-last', this.recentLast);
await this.recentSetLastKey(null);
await this.recentSetItem(null, true);
this.recentChanged = true;
this.emit('set-recent');
this.emit('recent-changed');
}
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,126 +1,418 @@
export const versionHistory = [
{
version: '0.11.7',
releaseDate: '2022-07-12',
showUntil: '2022-07-19',
content:
`
<ul>
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
<li>изменения в окне загруженных книг:</li>
<ul>
<li>добавлена группировка по версиям файла одной и той же книги</li>
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
<li>добавлены различные методы сортировки списка загруженных книг</li>
<li>нумерация всегда осуществляется по времени загрузки</li>
</ul>
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.6',
releaseDate: '2022-07-02',
showUntil: '2022-07-01',
content:
`
<ul>
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
<li>актуализация используемых пакетов</li>
</ul>
`
},
{
version: '0.11.5',
releaseDate: '2022-04-15',
showUntil: '2022-04-14',
content:
`
<ul>
<li>небольшие дополнения интерфейса</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.1',
releaseDate: '2021-12-03',
showUntil: '2021-12-02',
content:
`
<ul>
<li>переход на JembaDb вместо SQLite</li>
</ul>
`
},
{
version: '0.11.0',
releaseDate: '2021-11-18',
showUntil: '2021-11-17',
content:
`
<ul>
<li>переход на Vue 3</li>
</ul>
`
},
{
version: '0.10.3',
releaseDate: '2021-10-24',
showUntil: '2021-10-23',
content:
`
<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',
content:
`
<ul>
<li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.5',
releaseDate: '2020-11-01',
showUntil: '2020-10-31',
content:
`
<ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.4',
releaseDate: '2020-10-29',
showUntil: '2020-10-28',
content:
`
<ul>
<li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.3',
releaseDate: '2020-05-21',
showUntil: '2020-05-20',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.2',
releaseDate: '2020-03-15',
showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content:
`
<ul>
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.9.1',
releaseDate: '2020-03-03',
showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content:
`
<ul>
<li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
version: '0.9.0',
releaseDate: '2020-02-26',
showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',
content:
`
<ul>
<li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
version: '0.8.4',
releaseDate: '2020-02-06',
showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content:
`
<ul>
<li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.8.3',
releaseDate: '2020-01-28',
showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content:
`
<ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li>
</ul>
`
},
{
version: '0.8.2',
releaseDate: '2020-01-20',
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{
version: '0.8.1',
releaseDate: '2020-01-07',
showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content:
`
<ul>
<li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li>
</ul>
`
},
{
version: '0.8.0',
releaseDate: '2020-01-02',
showUntil: '2020-01-05',
header: '0.8.0 (2020-01-02)',
content:
`
<ul>
<li>окончательный переход на https</li>
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
</ul>
`
},
{
version: '0.7.9',
releaseDate: '2019-11-27',
showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content:
`
<ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.8',
releaseDate: '2019-11-25',
showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)',
content:
`
<ul>
<li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.7',
releaseDate: '2019-11-06',
showUntil: '2019-11-10',
header: '0.7.7 (2019-11-06)',
content:
`
<ul>
@@ -132,34 +424,40 @@ export const versionHistory = [
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
</ul>
</ul>
`
},
{
version: '0.7.6',
releaseDate: '2019-10-30',
showUntil: '2019-10-29',
header: '0.7.6 (2019-10-30)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.5',
releaseDate: '2019-10-22',
showUntil: '2019-10-21',
header: '0.7.5 (2019-10-22)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.3',
releaseDate: '2019-10-18',
showUntil: '2019-10-17',
header: '0.7.3 (2019-10-18)',
content:
`
<ul>
@@ -168,12 +466,14 @@ export const versionHistory = [
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.7.1',
releaseDate: '2019-09-20',
showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content:
`
<ul>
@@ -181,12 +481,14 @@ export const versionHistory = [
<li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li>
</ul>
`
},
{
version: '0.7.0',
releaseDate: '2019-09-07',
showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content:
`
<ul>
@@ -197,23 +499,27 @@ export const versionHistory = [
<li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul>
`
},
{
version: '0.6.10',
releaseDate: '2019-07-21',
showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.6.9',
releaseDate: '2019-06-23',
showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content:
`
<ul>
@@ -224,12 +530,14 @@ export const versionHistory = [
<li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li>
</ul>
`
},
{
version: '0.6.7',
releaseDate: '2019-05-30',
showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content:
`
<ul>
@@ -242,36 +550,42 @@ export const versionHistory = [
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li>
</ul>
`
},
{
version: '0.6.6',
releaseDate: '2019-03-29',
showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content:
`
<ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul>
`
},
{
version: '0.6.4',
releaseDate: '2019-03-24',
showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content:
`
<ul>
<li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li>
</ul>
`
},
{
version: '0.5.4',
releaseDate: '2019-03-04',
showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content:
`
<ul>
@@ -280,12 +594,14 @@ export const versionHistory = [
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul>
`
},
{
version: '0.3.0',
releaseDate: '2019-02-17',
showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content:
`
<ul>
@@ -293,12 +609,14 @@ export const versionHistory = [
<li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul>
`
},
{
version: '0.1.7',
releaseDate: '2019-02-14',
showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content:
`
<ul>
@@ -308,17 +626,20 @@ export const versionHistory = [
<li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul>
`
},
{
version: '0.1.0',
releaseDate: '2019-02-12',
showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content:
`
<ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul>
`
},

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<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="header row">
<div class="caption col row items-center q-ml-md">
<slot name="header"></slot>
</div>
<div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup>
<q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
@@ -25,26 +25,42 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../vueComponent.js';
import * as utils from '../../share/utils';
const DialogProps = Vue.extend({
props: {
value: Boolean,
}
})
class Dialog {
_props = {
modelValue: Boolean,
};
shown = false;
export default @Component({
})
class Dialog extends DialogProps {
get active() {
return this.value;
return this.modelValue;
}
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>

View File

@@ -4,12 +4,9 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../vueComponent.js';
export default @Component({
})
class Notify extends Vue {
class Notify {
notify(opts) {
let {
caption = null,
@@ -19,11 +16,12 @@ class Notify extends Vue {
iconColor = 'white',
message = '',
messageColor = 'black',
position = 'top-right',
} = opts;
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
return this.$q.notify({
position: 'top-right',
position,
color,
textColor: iconColor,
icon,
@@ -38,21 +36,23 @@ class Notify extends Vue {
});
}
success(message, caption) {
this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
success(message, caption, options) {
this.notify(Object.assign({color: 'positive', icon: 'la la-check-circle', message, caption}, options));
}
warning(message, caption) {
this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
warning(message, caption, options) {
this.notify(Object.assign({color: 'warning', icon: 'la la-exclamation-circle', message, caption}, options));
}
error(message, caption) {
this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
error(message, caption, options) {
this.notify(Object.assign({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption}, options));
}
info(message, caption) {
this.notify({color: 'info', icon: 'la la-bell', message, caption});
info(message, caption, options) {
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
}
}
export default vueComponent(Notify);
//-----------------------------------------------------------------------------
</script>

View File

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

View File

@@ -1,16 +1,16 @@
<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>
<!--------------------------------------------------->
<div v-show="type == 'alert'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<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-btn>
</div>
@@ -21,7 +21,9 @@
</div>
<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>
@@ -29,11 +31,11 @@
<div v-show="type == 'confirm'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<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-btn>
</div>
@@ -44,8 +46,40 @@
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Отмена
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
OK
</q-btn>
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Нет
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
Да
</q-btn>
</div>
</div>
@@ -53,11 +87,11 @@
<div v-show="type == 'prompt'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<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-btn>
</div>
@@ -65,13 +99,19 @@
<div class="q-mx-md">
<div v-html="message"></div>
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
<q-input ref="input" v-model="inputValue" class="q-mt-xs" outlined dense />
<div class="error">
<span v-show="error != ''">{{ error }}</span>
</div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Отмена
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
OK
</q-btn>
</div>
</div>
@@ -79,11 +119,11 @@
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup>
<q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
@@ -92,14 +132,20 @@
<div class="q-mx-md">
<div v-html="message"></div>
<div class="q-my-md text-center">
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
<div v-show="hotKeyCode == ''" class="text-grey-5">
Нет
</div>
<div>{{ hotKeyCode }}</div>
</div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Отмена
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps :disabled="hotKeyCode == ''" @click="okClick">
OK
</q-btn>
</div>
</div>
</q-dialog>
@@ -107,19 +153,18 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../vueComponent.js';
import * as utils from '../../share/utils';
export default @Component({
const componentOptions = {
watch: {
inputValue: function(newValue) {
this.validate(newValue);
},
}
})
class StdDialog extends Vue {
};
class StdDialog {
_options = componentOptions;
caption = '';
message = '';
active = false;
@@ -127,11 +172,12 @@ class StdDialog extends Vue {
inputValue = '';
error = '';
iconColor = '';
iconName = '';
hotKeyCode = '';
created() {
if (this.$root.addKeyHook) {
this.$root.addKeyHook(this.keyHook);
if (this.$root.addEventHook) {
this.$root.addEventHook('key', this.keyHook);
}
}
@@ -144,12 +190,18 @@ class StdDialog extends Vue {
this.inputValidator = null;
this.inputValue = '';
this.error = '';
this.showed = false;
this.iconColor = 'text-warning';
if (opts && opts.color) {
this.iconColor = `text-${opts.color}`;
}
this.iconName = 'las la-exclamation-circle';
if (opts && opts.iconName) {
this.iconName = opts.iconName;
}
this.hotKeyCode = '';
if (opts && opts.hotKeyCode) {
this.hotKeyCode = opts.hotKeyCode;
@@ -161,6 +213,7 @@ class StdDialog extends Vue {
this.hideTrigger();
this.hideTrigger = null;
}
this.showed = false;
}
onShow() {
@@ -170,6 +223,7 @@ class StdDialog extends Vue {
this.validate(this.inputValue);
this.$refs.input.focus();
}
this.showed = true;
}
validate(value) {
@@ -236,6 +290,23 @@ class StdDialog extends Vue {
});
}
askYesNo(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'askYesNo';
this.active = true;
});
}
prompt(message, caption, opts) {
return new Promise((resolve) => {
this.enableValidator = false;
@@ -276,25 +347,36 @@ class StdDialog extends Vue {
}
keyHook(event) {
if (this.active) {
if (this.active && this.showed) {
let handled = false;
if (this.type == 'hotKey') {
if (event.type == 'keydown') {
this.hotKeyCode = utils.keyEventToCode(event);
handled = true;
}
} else {
if (event.code == 'Enter')
if (event.key == 'Enter') {
this.okClick();
if (event.code == 'Escape') {
handled = true;
}
if (event.key == 'Escape') {
this.$nextTick(() => {
this.$refs.dialog.hide();
});
handled = true;
}
}
event.stopPropagation();
event.preventDefault();
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
}
}
export default vueComponent(StdDialog);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,11 +1,18 @@
<template>
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
<div class="window flexfit column no-wrap">
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
<div ref="window" class="window flexfit column no-wrap">
<div
ref="header"
class="header row justify-end"
@mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart"
@touchend.stop="onTouchEnd"
@touchmove.stop="onTouchMove"
>
<span class="header-text col"><slot name="header"></slot></span>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
<slot name="buttons"></slot>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
</div>
<slot></slot>
@@ -16,20 +23,22 @@
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import vueComponent from '../vueComponent.js';
export default @Component({
props: {
class Window {
_props = {
height: { type: String, default: '100%' },
width: { type: String, default: '100%' },
maxWidth: { type: String, default: '' },
topShift: { type: Number, default: 0 },
}
})
class Window extends Vue {
margin: '',
};
init() {
this.$nextTick(() => {
this.$refs.main.style.top = 0;
this.$refs.main.style.left = 0;
this.$refs.windowBox.style.height = this.height;
this.$refs.windowBox.style.width = this.width;
if (this.maxWidth)
@@ -39,11 +48,14 @@ class Window extends Vue {
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
if (this.margin)
this.$refs.window.style.margin = this.margin;
});
}
onMouseDown(event) {
if (this.$isMobileDevice)
if (this.$root.isMobileDevice)
return;
if (event.button == 0) {
this.$refs.header.style.cursor = 'move';
@@ -73,7 +85,7 @@ class Window extends Vue {
}
onTouchStart(event) {
if (!this.$isMobileDevice)
if (!this.$root.isMobileDevice)
return;
if (event.touches.length == 1) {
const touch = event.touches[0];
@@ -85,7 +97,7 @@ class Window extends Vue {
}
onTouchMove(event) {
if (!this.$isMobileDevice)
if (!this.$root.isMobileDevice)
return;
if (event.touches.length == 1 && this.moving) {
const touch = event.touches[0];
@@ -100,7 +112,7 @@ class Window extends Vue {
}
onTouchEnd() {
if (!this.$isMobileDevice)
if (!this.$root.isMobileDevice)
return;
this.$refs.header.style.cursor = 'default';
this.moving = false;
@@ -112,6 +124,8 @@ class Window extends Vue {
this.$emit('close');
}
}
export default vueComponent(Window);
//-----------------------------------------------------------------------------
</script>
@@ -139,7 +153,7 @@ class Window extends Vue {
}
.header {
background: linear-gradient(to bottom right, green, #59B04F);
background: linear-gradient(to bottom right, #007000, #59B04F);
align-items: center;
height: 30px;
}
@@ -147,8 +161,8 @@ class Window extends Vue {
.header-text {
margin-left: 10px;
margin-right: 10px;
color: yellow;
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
color: #FFFFA0;
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
overflow: hidden;
white-space: nowrap;
}
@@ -160,7 +174,8 @@ class Window extends Vue {
}
.close-button:hover {
background-color: #69C05F;
color: white;
background-color: #FF3030;
}
</style>
</style>

View File

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

View File

@@ -10,6 +10,5 @@
</head>
<body>
<div id="app"></div>
<script src="https://yastatic.net/share2/share.js" async="async"></script>
</body>
</html>

View File

@@ -1,15 +1,16 @@
import Vue from 'vue';
import { createApp } from 'vue';
import router from './router';
import store from './store';
import './quasar';
import q from './quasar';
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({
router,
store,
render: h => h(App),
}).$mount('#app');
const app = createApp(App);
app.use(router);
app.use(store);
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 from 'quasar/src/vue-plugin.js'
//import Quasar from 'quasar/dist/quasar.umd.prod.js';
import Quasar from 'quasar/src/vue-plugin.js';
//config
const config = {};
@@ -21,7 +20,8 @@ import {QSlider} from 'quasar/src/components/slider';
import {QTabs, QTab} from 'quasar/src/components/tabs';
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
import {QSeparator} from 'quasar/src/components/separator';
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
//import {QList} from 'quasar/src/components/item';
import {QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
import {QTooltip} from 'quasar/src/components/tooltip';
import {QSpinner} from 'quasar/src/components/spinner';
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
@@ -31,6 +31,10 @@ import {QColor} from 'quasar/src/components/color';
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = {
//QLayout,
@@ -47,7 +51,8 @@ const components = {
QTabs, QTab,
//QTabPanels, QTabPanel,
QSeparator,
QList, QItem, QItemSection, QItemLabel,
//QList,
QItem, QItemSection, QItemLabel,
QTooltip,
QSpinner,
QTable, QTh, QTr, QTd,
@@ -57,6 +62,9 @@ const components = {
QPopupProxy,
QDialog,
QChip,
QTree,
//QExpansionItem,
QVirtualScroll,
};
//directives
@@ -74,16 +82,17 @@ const plugins = {
Notify,
};
//use
Vue.use(Quasar, { config, components, directives, plugins });
//icons
//import '@quasar/extras/material-icons/material-icons.css';
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import '@quasar/extras/line-awesome/line-awesome.css';
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
import lineAwesome from 'quasar/icon-set/line-awesome.js'
Quasar.iconSet.set(lineAwesome);
export default {
quasar: Quasar,
options: { config, components, directives, plugins },
init: () => {
Quasar.iconSet.set(lineAwesome);
}
};

View File

@@ -1,43 +1,41 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router';
import _ from 'lodash';
//немедленная загрузка
import CardIndex from './components/CardIndex/CardIndex.vue';
//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
const Search = () => import('./components/CardIndex/Search/Search.vue');
const Card = () => import('./components/CardIndex/Card/Card.vue');
const Book = () => import('./components/CardIndex/Book/Book.vue');
const History = () => import('./components/CardIndex/History/History.vue');
//немедленная загрузка
//const Reader = () => import('./components/Reader/Reader.vue');
import Reader from './components/Reader/Reader.vue';
//import Reader from './components/Reader/Reader.vue';
const Reader = () => import('./components/Reader/Reader.vue');
const ExternalLibs = () => import('./components/ExternalLibs/ExternalLibs.vue');
//const Forum = () => import('./components/Forum/Forum.vue');
const Income = () => import('./components/Income/Income.vue');
const Sources = () => import('./components/Sources/Sources.vue');
const Settings = () => import('./components/Settings/Settings.vue');
const Help = () => import('./components/Help/Help.vue');
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
const myRoutes = [
['/', null, null, '/cardindex'],
['/cardindex', CardIndex ],
['/cardindex~search', Search ],
['/cardindex~card', Card ],
['/cardindex~card/:authorId', Card ],
['/cardindex~book', Book ],
['/cardindex~book/:bookId', Book ],
['/cardindex~history', History ],
['/cardindex', CardIndex],
['/cardindex~search', Search],
['/cardindex~card', Card],
['/cardindex~card/:authorId', Card],
['/cardindex~book', Book],
['/cardindex~book/:bookId', Book],
['/cardindex~history', History],
['/reader', Reader ],
['/income', Income ],
['/sources', Sources ],
['/settings', Settings ],
['/help', Help ],
['*', null, null, '/cardindex' ],
['/reader', Reader],
['/external-libs', ExternalLibs],
['/income', Income],
['/sources', Sources],
['/settings', Settings],
['/help', Help],
['/404', NotFound404],
['/:pathMatch(.*)*', null, null, '/cardindex'],
];
let routes = {};
@@ -64,8 +62,7 @@ for (let route of myRoutes) {
}
routes = routes.children;
Vue.use(VueRouter);
export default new VueRouter({
export default createRouter({
history: createWebHashHistory(),
routes
});

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

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

View File

@@ -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));
}
export function toHex(buf) {
return Buffer.from(buf).toString('hex');
}
export function stringToHex(str) {
return Buffer.from(str).toString('hex');
}
@@ -86,7 +90,7 @@ export function toBase58(data) {
}
export function fromBase58(data) {
return bs58.decode(data);
return Buffer.from(bs58.decode(data));
}
//base-x слишком тормозит, используем sjcl
@@ -103,69 +107,140 @@ export function fromBase64(data) {
));
}
export function getObjDiff(oldObj, newObj) {
const result = {__isDiff: true, change: {}, add: {}, del: []};
export function hasProp(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
for (const key of Object.keys(oldObj)) {
if (newObj.hasOwnProperty(key)) {
if (!_.isEqual(oldObj[key], newObj[key])) {
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
} else {
result.change[key] = _.cloneDeep(newObj[key]);
export function getObjDiff(oldObj, newObj, opts = {}) {
const {
exclude = [],
excludeAdd = [],
excludeDel = [],
} = opts;
const ex = new Set(exclude);
const exAdd = new Set(excludeAdd);
const exDel = new Set(excludeDel);
const makeObjDiff = (oldObj, newObj, keyPath) => {
const result = {__isDiff: true, change: {}, add: {}, del: []};
keyPath = `${keyPath}${keyPath ? '/' : ''}`;
for (const key of Object.keys(oldObj)) {
const kp = `${keyPath}${key}`;
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
if (ex.has(kp))
continue;
if (!_.isEqual(oldObj[key], newObj[key])) {
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
result.change[key] = makeObjDiff(oldObj[key], newObj[key], kp);
} else {
result.change[key] = _.cloneDeep(newObj[key]);
}
}
} else {
if (exDel.has(kp))
continue;
result.del.push(key);
}
} else {
result.del.push(key);
}
for (const key of Object.keys(newObj)) {
const kp = `${keyPath}${key}`;
if (exAdd.has(kp))
continue;
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
result.add[key] = _.cloneDeep(newObj[key]);
}
}
return result;
}
for (const key of Object.keys(newObj)) {
if (!oldObj.hasOwnProperty(key)) {
result.add[key] = _.cloneDeep(newObj[key]);
}
}
return result;
return makeObjDiff(oldObj, newObj, '');
}
export function isObjDiff(diff) {
return (_.isObject(diff) && diff.__isDiff);
return (_.isObject(diff) && diff.__isDiff && diff.change && diff.add && diff.del);
}
export function isEmptyObjDiff(diff) {
return (!_.isObject(diff) || !diff.__isDiff ||
(!Object.keys(diff.change).length &&
!Object.keys(diff.add).length &&
!diff.del.length
return (!isObjDiff(diff) ||
!(Object.keys(diff.change).length ||
Object.keys(diff.add).length ||
diff.del.length
)
);
}
export function applyObjDiff(obj, diff, isAddChanged) {
const result = _.cloneDeep(obj);
export function isEmptyObjDiffDeep(diff, opts = {}) {
if (!isObjDiff(diff))
return true;
const {
isApplyChange = true,
isApplyAdd = true,
isApplyDel = true,
} = opts;
let notEmptyDeep = false;
const change = diff.change;
for (const key of Object.keys(change)) {
if (_.isObject(change[key]))
notEmptyDeep |= !isEmptyObjDiffDeep(change[key], opts);
else if (isApplyChange)
notEmptyDeep = true;
}
return !(
notEmptyDeep ||
(isApplyAdd && Object.keys(diff.add).length) ||
(isApplyDel && diff.del.length)
);
}
export function applyObjDiff(obj, diff, opts = {}) {
const {
isAddChanged = false,
isApplyChange = true,
isApplyAdd = true,
isApplyDel = true,
} = opts;
let result = _.cloneDeep(obj);
if (!diff.__isDiff)
return result;
const change = diff.change;
for (const key of Object.keys(change)) {
if (result.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
if (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], isAddChanged);
result[key] = applyObjDiff(result[key], change[key], opts);
} else {
result[key] = _.cloneDeep(change[key]);
if (isApplyChange)
result[key] = _.cloneDeep(change[key]);
}
} else if (isAddChanged) {
result[key] = _.cloneDeep(change[key]);
}
}
for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[key]);
if (isApplyAdd) {
for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[key]);
}
}
for (const key of diff.del) {
delete result[key];
if (isApplyDel && diff.del.length) {
for (const key of diff.del) {
delete result[key];
}
if (_.isArray(result))
result = result.filter(v => v);
}
return result;
@@ -237,3 +312,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
}
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 Vuex from 'vuex';
import { createStore } from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import root from './root.js';
@@ -7,11 +6,9 @@ import uistate from './modules/uistate';
import config from './modules/config';
import reader from './modules/reader';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store(Object.assign({}, root, {
export default createStore(Object.assign({}, root, {
modules: {
uistate,
config,

View File

@@ -10,18 +10,7 @@ const state = {
const getters = {};
// 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 });
}
},
};
const actions = {};
// 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,6 +1,11 @@
import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json';
const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки',
'loadFile': 'Загрузить файл с диска',
'loadBuffer': 'Загрузить из буфера обмена',
'help': 'Вызвать cправку',
'settings': 'Настроить',
'undoAction': 'Действие назад',
'redoAction': 'Действие вперед',
@@ -10,9 +15,13 @@ const readerActions = {
'setPosition': 'Установить позицию',
'search': 'Найти в тексте',
'copyText': 'Скопировать текст со страницы',
'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу',
'clickControl': 'Управление кликом',
'offlineMode': 'Автономный режим (без интернета)',
'recentBooks': 'Открыть недавние',
'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Показать загруженные',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
'bookBegin': 'В начало книги',
@@ -29,6 +38,9 @@ const readerActions = {
//readerActions[name]
const toolButtons = [
{name: 'loadFile', show: true},
{name: 'loadBuffer', show: true},
{name: 'help', show: true},
{name: 'undoAction', show: true},
{name: 'redoAction', show: true},
{name: 'fullScreen', show: true},
@@ -36,15 +48,21 @@ const toolButtons = [
{name: 'setPosition', show: true},
{name: 'search', show: true},
{name: 'copyText', show: false},
{name: 'convOptions', show: true},
{name: 'refresh', show: true},
{name: 'offlineMode', show: false},
{name: 'contents', show: true},
{name: 'libs', show: true},
{name: 'recentBooks', show: true},
{name: 'clickControl', show: false},
{name: 'offlineMode', show: false},
];
//readerActions[name]
const hotKeys = [
{name: 'help', codes: ['F1', 'H']},
{name: 'loader', codes: ['Escape']},
{name: 'loadFile', codes: ['F3']},
{name: 'loadBuffer', codes: ['F4']},
{name: 'help', codes: ['F1', 'H']},
{name: 'settings', codes: ['S']},
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
@@ -52,10 +70,14 @@ const hotKeys = [
{name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'copyText', codes: ['Ctrl+Space']},
{name: 'convOptions', codes: ['Ctrl+M']},
{name: 'refresh', codes: ['R']},
{name: 'offlineMode', codes: ['O']},
{name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']},
{name: 'clickControl', codes: ['Ctrl+B']},
{name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']},
{name: 'bookBegin', codes: ['Home']},
@@ -80,125 +102,22 @@ const fonts = [
{name: 'Rubik', fontVertShift: 0},
];
const webFonts = [
{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Alegreya+Sans', name: 'Alegreya Sans', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Alegreya+SC', name: 'Alegreya SC', fontVertShift: -5},
{css: 'https://fonts.googleapis.com/css?family=Alice', name: 'Alice', fontVertShift: 5},
{css: 'https://fonts.googleapis.com/css?family=Amatic+SC', name: 'Amatic SC', fontVertShift: 0},
{css: 'https://fonts.googleapis.com/css?family=Andika', name: 'Andika', fontVertShift: -35},
{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},
];
//webFonts: [{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: 0}, ...],
const webFonts = [];
for (const family of googleFonts) {
webFonts.push({
css: `https://fonts.googleapis.com/css?family=${family.replace(/\s/g, '+')}`,
name: family,
fontVertShift: 0,
});
}
//----------------------------------------------------------------------------------------------------------
const settingDefaults = {
textColor: '#000000',
backgroundColor: '#EBE2C9',
backgroundColor: '#ebe2c9',
wallpaper: '',
wallpaperIgnoreStatusBar: false,
fontStyle: '',// 'italic'
fontWeight: '',// 'bold'
fontSize: 20,// px
@@ -215,9 +134,22 @@ const settingDefaults = {
wordWrap: true,//перенос по слогам
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,
statusBarTop: false,// top, bottom
statusBarHeight: 19,// px
statusBarColorAsText: true,//цвет как у текста
statusBarColor: '#000000',
statusBarColorAlpha: 0.4,
statusBarClickOpen: true,
@@ -240,14 +172,25 @@ const settingDefaults = {
compactTextPerc: 0,
imageHeightLines: 100,
imageFitWidth: true,
enableSitesFilter: true,
splitToPara: false,
djvuQuality: 20,
pdfAsText: true,
pdfQuality: 20,
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
enableSitesFilter: true,
showNeedUpdateNotify: true,
fontShifts: {},
showToolButton: {},
toolBarHideOnScroll: true,
userHotKeys: {},
userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
};
for (const font of fonts)
@@ -259,6 +202,45 @@ for (const button of toolButtons)
for (const hotKey of hotKeys)
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
const diffExclude = [];
for (const hotKey of hotKeys)
diffExclude.push(`userHotKeys/${hotKey.name}`);
diffExclude.push('userWallpapers');
function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
}
return false;
}
const libsDefaults = {
startLink: 'http://flibusta.is',
comment: 'Флибуста | Книжное братство',
closeAfterSubmit: false,
openInFrameOnEnter: false,
openInFrameOnAdd: false,
groups: [
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
]},
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},
]
};
// initial state
const state = {
toolBarActive: true,
@@ -272,6 +254,8 @@ const state = {
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settingsRev: {},
libs: Object.assign({}, libsDefaults),
libsRev: 0,
};
// getters
@@ -310,11 +294,23 @@ const mutations = {
state.currentProfile = value;
},
setSettings(state, value) {
state.settings = Object.assign({}, state.settings, value);
const newSettings = Object.assign({}, state.settings, value);
const added = addDefaultsToSettings(newSettings);
if (added) {
state.settings = added;
} else {
state.settings = newSettings;
}
},
setSettingsRev(state, value) {
state.settingsRev = Object.assign({}, state.settingsRev, value);
},
setLibs(state, value) {
state.libs = value;
},
setLibsRev(state, value) {
state.libsRev = value;
},
};
export default {
@@ -324,6 +320,8 @@ export default {
fonts,
webFonts,
settingDefaults,
addDefaultsToSettings,
libsDefaults,
namespaced: true,
state,

View File

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

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

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

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