Compare commits

...

1165 Commits

Author SHA1 Message Date
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
Book Pauk
34c7a33576 Merge branch 'release/0.9.2' 2020-04-15 15:37:21 +07:00
Book Pauk
23ecfeeb4f Версия 0.9.2 2020-04-15 15:36:42 +07:00
Book Pauk
9703f83eb3 Мелкая поправка 2020-04-15 15:32:32 +07:00
Book Pauk
0f3cc03d00 Мелкие поправки 2020-03-19 19:44:06 +07:00
Book Pauk
6f7ba1f9fc Окончание работы над хоткеями 2020-03-19 19:38:40 +07:00
Book Pauk
e1b85e4a1b Рефакторинг 2020-03-19 19:09:39 +07:00
Book Pauk
b308dd58cc Рефакторинг 2020-03-19 17:12:12 +07:00
Book Pauk
9f4c0479ce Рефакторинг 2020-03-19 17:05:29 +07:00
Book Pauk
2c57817dde Рефакторинг 2020-03-19 16:38:31 +07:00
Book Pauk
ba85c54d7c Рефакторинг 2020-03-19 15:57:56 +07:00
Book Pauk
a80e5c3a65 Поправки текста 2020-03-19 15:46:32 +07:00
Book Pauk
22e2c34da8 Работа над хоткеями 2020-03-18 20:20:06 +07:00
Book Pauk
00a8e4c2c5 Работа над хоткеями 2020-03-18 20:04:44 +07:00
Book Pauk
10d0a4079c Работа над хоткеями 2020-03-18 19:00:02 +07:00
Book Pauk
589f7f3c22 Работа над хоткеями 2020-03-18 18:20:57 +07:00
Book Pauk
d1126a7eb0 Работа над хоткеями, промежуточный коммит 2020-03-18 15:37:19 +07:00
Book Pauk
9f4e72a0e1 Работа над хоткеями, промежуточный коммит 2020-03-18 15:06:29 +07:00
Book Pauk
a024295379 Мелкий рефакторинг 2020-03-15 21:48:53 +07:00
Book Pauk
dc2b2ec488 Мелкая доработка 2020-03-15 21:46:35 +07:00
Book Pauk
0c5f5975aa Работа над хоткеями 2020-03-15 21:44:26 +07:00
Book Pauk
dc3f682d2d Работа над хоткеями 2020-03-15 21:29:58 +07:00
Book Pauk
2db8876c66 Рефакторинг, работа над хоткеями 2020-03-15 20:58:06 +07:00
Book Pauk
8f6201b0f7 Работа над хоткеями 2020-03-15 16:12:55 +07:00
Book Pauk
4b146c70ad Мелкий рефакторинг 2020-03-08 18:53:40 +07:00
Book Pauk
0118034b4b Запись в историю 2020-03-06 18:10:22 +07:00
Book Pauk
39217053ca Исправление багов 2020-03-06 18:00:10 +07:00
Book Pauk
fba190c826 Переход на ServiceWorker вместо appcache 2020-03-06 15:20:56 +07:00
Book Pauk
5e9d528e16 Поправка конфига nginx 2020-03-05 14:35:10 +07:00
Book Pauk
c5921d88fc Добавлена настройка certbot 2020-03-05 14:31:48 +07:00
Book Pauk
eb980b0ea1 Удалил ненужное 2020-03-05 14:23:16 +07:00
Book Pauk
de5b4216f7 Добавлены конфиги для среды beta 2020-03-05 13:46:52 +07:00
Book Pauk
495ff57b19 Добавлен пакет sw-precache-webpack-plugin 2020-03-05 13:46:21 +07:00
Book Pauk
57948cf6e3 Мелкий рефакторинг 2020-03-04 15:45:13 +07:00
Book Pauk
1aebbbcabd Мелкий рефакторинг 2020-03-04 15:32:05 +07:00
Book Pauk
25b4cb072d Обновление пакетов 2020-03-04 15:15:53 +07:00
Book Pauk
1cdacc3a08 Merge tag '0.9.1' into develop
0.9.1
2020-03-03 21:48:07 +07:00
Book Pauk
34d9466d09 Merge branch 'release/0.9.1' 2020-03-03 21:48:01 +07:00
Book Pauk
c182c4ce66 Версия 0.9.1 2020-03-03 21:47:13 +07:00
Book Pauk
dbb9bd1282 Добавлено сохранение валидных uploaded-файлов в удаленном хранилище 2020-03-03 18:06:46 +07:00
Book Pauk
8019d2d6cc Мелкая поправка 2020-03-03 16:44:21 +07:00
Book Pauk
459cdb2e0b Мелкие поправки 2020-03-03 16:41:38 +07:00
Book Pauk
a230cd9513 Добавлена настройка "Открывать оригинал по клику" 2020-03-03 12:51:40 +07:00
Book Pauk
0c44a25e85 Перенос чекбокса в другой раздел 2020-03-03 12:30:31 +07:00
Book Pauk
34f3d04370 Апдейт пакетов 2020-03-02 19:08:17 +07:00
Book Pauk
1f3e6b7e16 Удалены более ненужные пакеты 2020-03-02 17:24:39 +07:00
Book Pauk
47d49a200a Merge tag '0.9.0-1' into develop
0.9.0-1
2020-02-26 18:10:56 +07:00
Book Pauk
e1767d6e52 Merge branch 'release/0.9.0-1' 2020-02-26 18:10:48 +07:00
Book Pauk
0f8e343cd2 Поправлен баг 2020-02-26 18:10:02 +07:00
Book Pauk
23ab487baf Merge tag '0.9.0' into develop
0.9.0
2020-02-26 17:48:45 +07:00
Book Pauk
22e5d38ef5 Merge branch 'release/0.9.0' 2020-02-26 17:48:37 +07:00
Book Pauk
5819ccb528 Версия 0.9.0 2020-02-26 17:47:26 +07:00
Book Pauk
42a2fd77cf Удаление element-ui 2020-02-26 17:44:03 +07:00
Book Pauk
ab93a8b0b3 Merge branch 'feature/quasar' into develop 2020-02-26 17:31:50 +07:00
Book Pauk
84437eafa6 Поправки для смартфонов 2020-02-26 17:31:08 +07:00
Book Pauk
0107d848e0 Мелкая поправка 2020-02-26 16:58:32 +07:00
Book Pauk
5eeac96a0d Поправки цветов 2020-02-26 16:52:38 +07:00
Book Pauk
9351c115be Переход на quasar 2020-02-26 16:44:01 +07:00
Book Pauk
f95a11096c Переход на quasar 2020-02-26 16:40:26 +07:00
Book Pauk
4203d179e6 Переход на quasar 2020-02-26 14:34:30 +07:00
Book Pauk
78dfc9cb1c Переход на quasar 2020-02-26 14:32:47 +07:00
Book Pauk
0bef307d77 Переход на quasar 2020-02-26 14:16:17 +07:00
Book Pauk
b0da806f7a Переход на quasar 2020-02-26 13:45:03 +07:00
Book Pauk
badecd1d81 Переход на quasar 2020-02-26 13:42:22 +07:00
Book Pauk
6418e8ee30 Переход на quasar 2020-02-26 13:40:06 +07:00
Book Pauk
09115c9658 Переход на quasar 2020-02-26 13:26:15 +07:00
Book Pauk
74e3866bd7 Поправки цветов таба 2020-02-26 13:08:25 +07:00
Book Pauk
408de78c13 Переход на quasar 2020-02-25 22:02:18 +07:00
Book Pauk
c0451c18b3 Переход на quasar 2020-02-25 21:59:22 +07:00
Book Pauk
f303d26c1e Переход на quasar 2020-02-25 21:52:55 +07:00
Book Pauk
1b58a34859 Переход на quasar 2020-02-25 21:51:11 +07:00
Book Pauk
82ea416e67 Переход на quasar 2020-02-25 21:46:30 +07:00
Book Pauk
efd4fbad70 Переход на quasar 2020-02-25 21:07:12 +07:00
Book Pauk
01bd15121b Мелкий рефакторинг 2020-02-25 20:46:58 +07:00
Book Pauk
a9c2495349 Переход на quasar 2020-02-25 20:42:34 +07:00
Book Pauk
e7c50b50ed Переход на quasar 2020-02-25 17:37:41 +07:00
Book Pauk
6e25b289d2 Переход на quasar 2020-02-24 13:09:10 +07:00
Book Pauk
157267eaf7 Мелкая поправка 2020-02-24 12:56:03 +07:00
Book Pauk
a317f9137a Переход на quasar 2020-02-24 12:32:58 +07:00
Book Pauk
5dad3d22ea Переход на quasar 2020-02-24 12:29:47 +07:00
Book Pauk
be85df456b Переход на quasar 2020-02-24 12:21:52 +07:00
Book Pauk
2e172a08c7 Доработка NumInput 2020-02-20 21:06:21 +07:00
Book Pauk
bb1069ca60 Переход на quasar 2020-02-20 18:45:31 +07:00
Book Pauk
d8141a1628 Переход на quasar 2020-02-20 18:30:40 +07:00
Book Pauk
de9f7c4baf Переход на quasar 2020-02-20 18:24:00 +07:00
Book Pauk
fa9b3116f1 Переход на quasar 2020-02-20 18:20:59 +07:00
Book Pauk
dcf9d52961 Переход на quasar 2020-02-20 18:00:03 +07:00
Book Pauk
1da93e2cc7 Доработка NumInput 2020-02-20 16:46:58 +07:00
Book Pauk
1d1bab988e Переход на quasar 2020-02-20 16:44:28 +07:00
Book Pauk
dcc6ad3af3 Доработка NumInput 2020-02-19 15:11:54 +07:00
Book Pauk
d57f266789 Доработка NumInput 2020-02-19 15:11:15 +07:00
Book Pauk
c3395e1eff Переход на quasar 2020-02-19 11:43:29 +07:00
Book Pauk
ca59ec2dbe Переход на quasar 2020-02-18 21:06:44 +07:00
Book Pauk
79788125f3 Убрал дебаг 2020-02-18 20:52:42 +07:00
Book Pauk
2154f20fa4 Мелкая поправка 2020-02-18 20:52:12 +07:00
Book Pauk
afe40b6a89 Доработка NumInput 2020-02-18 20:52:00 +07:00
Book Pauk
ba4b3bd6b8 Доработки NumInput 2020-02-18 13:49:55 +07:00
Book Pauk
e423b5d745 Переход на quasar 2020-02-18 13:29:15 +07:00
Book Pauk
6de8eca7ea Переход на quasar 2020-02-18 12:46:18 +07:00
Book Pauk
9d68cfcaf0 Улучшена загрузка внешних шрифтов 2020-02-17 11:53:03 +07:00
Book Pauk
225de11e6a Переход на quasar 2020-02-17 11:26:05 +07:00
Book Pauk
916581bbd0 Поправлен баг 2020-02-16 11:30:48 +07:00
Book Pauk
1cbb35840f Переход на quasar 2020-02-15 20:58:17 +07:00
Book Pauk
7a1d769e39 Мелкие поправки 2020-02-15 20:58:07 +07:00
Book Pauk
8254bf934c Переход на quasar 2020-02-14 21:24:05 +07:00
Book Pauk
5e2f20542f Переход на quasar 2020-02-14 21:09:49 +07:00
Book Pauk
551a707ee4 Переход на quasar 2020-02-14 19:32:00 +07:00
Book Pauk
024b15b4f9 Мелкие поправки 2020-02-14 12:53:15 +07:00
Book Pauk
1935df4143 Merge branch 'develop' into feature/quasar 2020-02-14 11:51:50 +07:00
Book Pauk
3f99f90076 Merge tag '0.8.4-1' into develop
0.8.4-1
2020-02-14 11:46:37 +07:00
Book Pauk
53cb445dde Merge branch 'release/0.8.4-1' 2020-02-14 11:46:28 +07:00
Book Pauk
6e46947220 Исправлен баг 2020-02-14 11:44:39 +07:00
Book Pauk
9b65e1671b Переход на quasar 2020-02-14 11:35:35 +07:00
Book Pauk
d5c741db35 Переход на quasar 2020-02-12 16:05:31 +07:00
Book Pauk
11e0780b6e Переход на quasar 2020-02-12 16:03:42 +07:00
Book Pauk
f153541570 Переход на quasar 2020-02-11 15:17:47 +07:00
Book Pauk
f066af88e7 Поправка формата заголовка 2020-02-11 15:05:43 +07:00
Book Pauk
97e1eef799 Поправлено дефолтное значение для pageChangeAnimation 2020-02-11 15:00:10 +07:00
Book Pauk
1bcd902817 Закрыта дыра безопасности 2020-02-11 13:02:43 +07:00
Book Pauk
2484568b21 Доработка общения по вебсокету 2020-02-11 11:16:32 +07:00
Book Pauk
085cc47ea5 Переход на quasar 2020-02-10 17:13:12 +07:00
Book Pauk
aac36a88f3 Переход на quasar 2020-02-10 16:39:14 +07:00
Book Pauk
1f2ebc82b7 Поправлен мелкий баг 2020-02-10 13:56:11 +07:00
Book Pauk
9781949064 Переход на quasar 2020-02-09 22:06:57 +07:00
Book Pauk
b06ef3781a Переход на quasar 2020-02-09 21:22:18 +07:00
Book Pauk
b32213cb7b Небольшие доработки 2020-02-09 17:11:39 +07:00
Book Pauk
ac4c7d2421 Переход на quasar 2020-02-09 17:11:11 +07:00
Book Pauk
824a49b80f Переход на quasar 2020-02-09 16:49:08 +07:00
Book Pauk
13efd50d80 Добавлен универсальный includer для включения файлов в исходник 2020-02-09 16:43:36 +07:00
Book Pauk
6fb091d20f Мелкие доработки 2020-02-09 11:13:35 +07:00
Book Pauk
518ab85cae Небольшие доработки 2020-02-08 20:50:47 +07:00
Book Pauk
f5124ad8b5 Мелкие поправки 2020-02-08 20:24:15 +07:00
Book Pauk
6f80900aa8 Добавлена анимация слеша на страницу 2020-02-08 20:12:55 +07:00
Book Pauk
06b80e9281 Поправлены размеры иконок на кнопках 2020-02-08 11:59:59 +07:00
Book Pauk
51b39d9365 Мелкие поправки 2020-02-07 19:59:47 +07:00
Book Pauk
f7d2d8fc95 Мелкие поправки 2020-02-07 19:57:53 +07:00
Book Pauk
f34fb94c1a Убрал лишнее 2020-02-07 19:52:42 +07:00
Book Pauk
3107224e50 Переход на quasar 2020-02-07 19:51:23 +07:00
Book Pauk
e1c481c534 Поправлена сортировка 2020-02-07 19:26:00 +07:00
Book Pauk
945a2dd3eb Переход на quasar 2020-02-07 18:56:51 +07:00
Book Pauk
e318945eb1 Переход на quasar 2020-02-07 16:18:32 +07:00
Book Pauk
926709568d Merge branch 'develop' into feature/quasar 2020-02-06 22:04:31 +07:00
Book Pauk
da040e799c Merge tag '0.8.4' into develop
0.8.4
2020-02-06 21:55:56 +07:00
Book Pauk
694976cb6e Merge branch 'release/0.8.4' 2020-02-06 21:55:48 +07:00
Book Pauk
3f7bd1846a Версия 0.8.4 2020-02-06 21:50:58 +07:00
Book Pauk
714898b4c3 Добавлен paypal-адрес для пожертвований 2020-02-06 21:49:20 +07:00
Book Pauk
4efc9b6990 Merge tag '0.8.3-4' into develop
0.8.3-4
2020-02-06 21:07:35 +07:00
Book Pauk
73c3beaff1 Merge branch 'release/0.8.3-4' 2020-02-06 21:07:28 +07:00
Book Pauk
a6bdccd4ef Доработки конвертирования из буфера обмена 2020-02-06 21:04:40 +07:00
Book Pauk
8007991e7d Merge tag '0.8.3-3' into develop
0.8.3-3
2020-02-06 20:27:29 +07:00
Book Pauk
0e5d1ed1c3 Merge branch 'release/0.8.3-3' 2020-02-06 20:27:20 +07:00
Book Pauk
91dc2f4f71 Поправки логирования 2020-02-06 20:25:49 +07:00
Book Pauk
950bab3023 Добавлено декодирование имен файлов при распаковке Zip-архива в случае,
если кодировка имени не дает создать файл на диске
2020-02-06 20:20:29 +07:00
Book Pauk
29082a10e6 Рефакторинг 2020-02-06 20:13:33 +07:00
Book Pauk
65c1227d88 Удален node-stream-zip, т.к. в него внесены ручные правки 2020-02-06 20:12:01 +07:00
Book Pauk
5d121a68cf Поправки скриптов деплоя и запуска, добавлен авторестарт при падении сервера 2020-02-06 16:40:13 +07:00
Book Pauk
ad07d2b8b1 Переход на quasar 2020-02-05 21:16:05 +07:00
Book Pauk
c5aef78085 Поправки иконки 2020-02-05 19:46:48 +07:00
Book Pauk
522ebc8aa2 К предыдущему 2020-02-05 15:36:24 +07:00
Book Pauk
199b3761b5 Мелкая поправка 2020-02-05 15:34:57 +07:00
Book Pauk
daf7b45e45 Поправка иконки 2020-02-05 15:31:24 +07:00
Book Pauk
fc71b953c7 update quasar 2020-02-05 15:28:09 +07:00
Book Pauk
74ccd4a001 Переход на иконки line-awesome 2020-02-05 15:27:26 +07:00
Book Pauk
3c09f6ca55 Переход на quasar 2020-02-05 14:21:50 +07:00
Book Pauk
c7dbe8599d Поправки вида слайдера 2020-02-05 14:20:46 +07:00
Book Pauk
ca036b6676 Поправки копирования в буфер обмена, поправки текста 2020-02-04 19:51:19 +07:00
Book Pauk
5ae87c8e03 Рефакторинг 2020-02-04 19:45:06 +07:00
Book Pauk
9774fc4f65 Небольшие доработки 2020-02-04 19:17:43 +07:00
Book Pauk
d0891fb652 Поправлен баг вычисления rootRoute 2020-02-04 16:29:10 +07:00
Book Pauk
e388e2a1c7 Переименование 2020-02-04 16:16:31 +07:00
Book Pauk
d9ab354338 Небольшая поправка 2020-02-04 14:36:45 +07:00
Book Pauk
9ea0a0e214 К предыдущему 2020-02-04 13:39:46 +07:00
Book Pauk
131ddf0355 Поправил цвет 2020-02-04 13:37:52 +07:00
Book Pauk
8abe71a0fe Поправлен баг загрузки шрифтов 2020-02-04 13:30:48 +07:00
Book Pauk
43e27a7e68 Переход на quasar 2020-02-04 13:08:49 +07:00
Book Pauk
b784d277e4 Небольшие поправки 2020-02-04 12:29:54 +07:00
Book Pauk
cb443157da Переход на quasar 2020-02-04 12:20:12 +07:00
Book Pauk
c886015d92 Поправлен tooltip delay 2020-02-03 16:57:54 +07:00
Book Pauk
3161247da9 Рефакторинг 2020-02-03 16:57:39 +07:00
Book Pauk
743a250131 Поправлена иконка 2020-02-03 16:16:13 +07:00
Book Pauk
4fb4b21a9e Поправил цвет кнопки 2020-02-03 16:07:25 +07:00
Book Pauk
e1a7d3ebc5 Мелкие поправки 2020-02-03 16:05:36 +07:00
Book Pauk
72b8b156ac Переход на quasar 2020-02-03 15:49:27 +07:00
Book Pauk
134dafb608 Поправлен баг 2020-02-03 15:39:09 +07:00
Book Pauk
d5102b6422 Мелкая поправка 2020-02-03 15:38:20 +07:00
Book Pauk
a2cfb9d423 Переход на quasar 2020-02-03 15:33:54 +07:00
Book Pauk
bef70f94ab Переход на quasar 2020-02-03 15:12:28 +07:00
Book Pauk
4233fffe74 Переход на quasar 2020-02-03 14:23:07 +07:00
Book Pauk
81c214748d Закомментировал пока неиспользуемые компоненты 2020-02-03 02:22:16 +07:00
Book Pauk
c6a61dc8c8 Переход на quasar 2020-02-03 02:20:46 +07:00
Book Pauk
483092d40d Небольшие поправки 2020-02-03 01:57:04 +07:00
Book Pauk
88cb02f6bc Мелкие поправки 2020-02-03 01:39:52 +07:00
Book Pauk
9628188730 Переход на quasar 2020-02-03 01:25:57 +07:00
Book Pauk
2e66134bf8 Переход на quasar 2020-02-03 01:10:58 +07:00
Book Pauk
424fe4d1e9 Переход на quasar 2020-02-03 01:01:10 +07:00
Book Pauk
2b6f9568de Мелкий рефакторинг 2020-02-02 23:25:28 +07:00
Book Pauk
4b270bce8b Переход на quasar 2020-02-02 23:15:59 +07:00
Book Pauk
6b077e67db Мелкие поправки 2020-02-02 20:17:31 +07:00
Book Pauk
4c79ea0679 Мелкая поправка 2020-02-02 18:20:25 +07:00
Book Pauk
8c4c4c25aa Переход на quasar 2020-02-02 18:17:30 +07:00
Book Pauk
a37dbe2c06 Переход на quasar 2020-02-02 16:49:12 +07:00
Book Pauk
5e10cb2d16 Переход на quasar 2020-02-02 15:55:04 +07:00
Book Pauk
58316c5c1d Переход на quasar 2020-02-02 15:37:36 +07:00
Book Pauk
55f092f161 Переход на quasar 2020-02-02 15:14:22 +07:00
Book Pauk
ab5049127a Переход на quasar 2020-02-02 15:03:50 +07:00
Book Pauk
5f99067e56 Переход на quasar 2020-02-02 14:24:43 +07:00
Book Pauk
3a89e61bd8 Небольшие поправки 2020-02-02 12:58:13 +07:00
Book Pauk
06edfa2fee Поправки цветов заголовка 2020-02-02 12:48:07 +07:00
Book Pauk
77bfd72458 Переход на quasar 2020-01-31 21:14:22 +07:00
Book Pauk
5ddf19be4d Удалил мусор 2020-01-31 21:14:09 +07:00
Book Pauk
6657b47746 Небольшие поправки 2020-01-31 20:41:17 +07:00
Book Pauk
5690efb07a Переход на quasar 2020-01-31 20:21:25 +07:00
Book Pauk
05600cba08 Переход на quasar 2020-01-31 19:51:53 +07:00
Book Pauk
e3b4120b2c Переход на quasar 2020-01-31 18:35:30 +07:00
Book Pauk
1059245fd9 Мелкая поправка 2020-01-31 18:17:39 +07:00
Book Pauk
87c8d310b3 Переход на quasar, добавлены иконки 2020-01-31 18:14:38 +07:00
Book Pauk
fdc4999556 Merge branch 'develop' into feature/quasar 2020-01-31 17:03:46 +07:00
Book Pauk
d28a8db4ff Добавлен альтернативный метод вычисления ширины строки в пикселях 2020-01-31 16:59:34 +07:00
Book Pauk
ab9e7d10dd Добавлен отлов ошибок при инициализации, добавлена генерация ошибки measureText 2020-01-31 16:08:37 +07:00
Book Pauk
3ff72b26b9 0.8.3 2020-01-31 14:52:49 +07:00
Book Pauk
107ae70651 Переход на quasar 2020-01-31 14:49:59 +07:00
Book Pauk
04de19033e Переход на quasar 2020-01-31 14:49:44 +07:00
Book Pauk
089ac70cd3 Используем css-классы quasar 2020-01-30 18:53:01 +07:00
Book Pauk
ae40a9ead9 К предыдущему 2020-01-30 18:03:10 +07:00
Book Pauk
152806b7f6 Рефакторинг 2020-01-30 18:01:02 +07:00
Book Pauk
06beb8e704 Рефакторинг 2020-01-30 17:55:24 +07:00
Book Pauk
64f2b94685 0.8.3 2020-01-30 16:44:34 +07:00
Book Pauk
5a42eb98ab Merge branch 'develop' into feature/quasar 2020-01-30 16:36:17 +07:00
Book Pauk
404b87d78d Небольшие поправки 2020-01-30 16:34:05 +07:00
Book Pauk
dcb8fbdbf4 Merge tag '0.8.3-2' into develop
0.8.3-2
2020-01-29 01:03:20 +07:00
Book Pauk
0fe513d7f5 Merge branch 'release/0.8.3-2' 2020-01-29 01:02:56 +07:00
Book Pauk
0be05325e4 Исправлен баг 2020-01-29 01:02:05 +07:00
Book Pauk
75b39308cd Merge tag '0.8.3-1' into develop
0.8.3-1
2020-01-28 21:32:25 +07:00
Book Pauk
35ded81713 Merge branch 'release/0.8.3-1' 2020-01-28 21:32:02 +07:00
Book Pauk
07c85280cd Исправлены таймауты для конвертера calibre, добавлен флаг запуска -vv, соответственно поправлено вычисление прогресса 2020-01-28 21:27:54 +07:00
Book Pauk
43f1d86be0 Merge tag '0.8.3' into develop
0.8.3
2020-01-28 20:21:40 +07:00
Book Pauk
82f5ed4c44 Merge branch 'release/0.8.3' 2020-01-28 20:21:31 +07:00
Book Pauk
0b53ad4b4d Версия 0.8.3 2020-01-28 20:20:10 +07:00
Book Pauk
56ad41d10c Поправки текста объявления 2020-01-28 20:18:02 +07:00
Book Pauk
249a4564e0 Добавлено уведомление "Оплатим хостинг вместе" 2020-01-28 19:46:34 +07:00
Book Pauk
efb2413720 Небольшое изменение содержимого страницы 2020-01-28 19:44:52 +07:00
Book Pauk
1226acefd6 Небольшие исправления, queue теперь в одном экземпляре на класс 2020-01-28 14:51:09 +07:00
Book Pauk
76f7d7bc90 Мелкая поправка 2020-01-27 19:52:56 +07:00
Book Pauk
a5cb2641fd Мелкая поправка 2020-01-27 19:42:30 +07:00
Book Pauk
57fc64af79 Добавлен abort конвертеров при истечении времени ожидания подвижек очереди 2020-01-27 19:34:10 +07:00
Book Pauk
f8b7b8b698 Исправления LimitedQueue, исправления багов, добавлена проверка флага abort 2020-01-27 18:57:42 +07:00
Book Pauk
3da6befe10 Добавлен класс LimitedQueue для организации очередей 2020-01-26 18:38:09 +07:00
Book Pauk
a50d61c3ce Добавлена очередь скачивания и конвертирования 2020-01-26 18:37:14 +07:00
Book Pauk
b7568975e7 Добавлена обработка state = 'queue' 2020-01-26 18:31:31 +07:00
Book Pauk
4b9475310f Убрал ненужный this.taken 2020-01-26 16:23:20 +07:00
Book Pauk
639f726c83 Добавлен лимит на размер файла при распаковке 2020-01-26 15:17:45 +07:00
Book Pauk
7997c486cf Мелкий рефакторинг 2020-01-26 15:07:14 +07:00
Book Pauk
2569d00bd0 Мелкие поправки 2020-01-26 13:47:25 +07:00
Book Pauk
2cd80d8fa1 Merge tag '0.8.2-5' into develop
0.8.2-5
2020-01-23 17:12:46 +07:00
Book Pauk
eedca4db9b Merge branch 'release/0.8.2-5' 2020-01-23 17:12:37 +07:00
Book Pauk
1d352a76ce Поправка опечаток 2020-01-23 17:00:17 +07:00
Book Pauk
17670aabf9 WebSocket: добавлен метод reader-storage, поправки багов 2020-01-23 16:59:08 +07:00
Book Pauk
3456b3d90e WebSocket: добавлен метод worker-get-state-finish, небольшой рефакторинг 2020-01-23 16:25:06 +07:00
Book Pauk
f3da5a9026 Поправил комментарий 2020-01-23 15:56:26 +07:00
Book Pauk
00cc63b7cd WebSocket: добавлен метод get-config 2020-01-23 15:54:46 +07:00
Book Pauk
8df80ce738 Мелкая поправка 2020-01-23 15:16:49 +07:00
Book Pauk
12e7a783b0 Небольшие изменения блокирования кнопок панели 2020-01-22 22:06:12 +07:00
Book Pauk
be86a15351 Добавил настройку proxy_read_timeout 2020-01-22 21:37:28 +07:00
Book Pauk
2c5022e7b4 Merge tag '0.8.2-4' into develop
0.8.2-4
2020-01-22 21:17:58 +07:00
Book Pauk
f4a996fcb9 Merge branch 'release/0.8.2-4' 2020-01-22 21:17:52 +07:00
Book Pauk
fdbf508bbf Используем протокол WSS при необходимости 2020-01-22 21:17:10 +07:00
Book Pauk
500fafa5b2 Merge tag '0.8.2-3' into develop
0.8.2-3
2020-01-22 21:05:36 +07:00
Book Pauk
bfa315c68b Merge branch 'release/0.8.2-3' 2020-01-22 21:05:27 +07:00
Book Pauk
4972f085a3 Мелкая поправка 2020-01-22 20:59:52 +07:00
Book Pauk
9c13261929 Добавлена настройка для вебсокетов, добавлен конфиг nginx omnireader_http 2020-01-22 20:58:57 +07:00
Book Pauk
e36dc4a913 Небольшие поправки 2020-01-22 20:28:46 +07:00
Book Pauk
4cccb56ee3 Поправил комментарий 2020-01-22 20:15:33 +07:00
Book Pauk
3199af570d Добавлен WebSocketServer и контроллер для него 2020-01-22 20:06:51 +07:00
Book Pauk
7dad47b3c8 Добавлено использование WebSocketConnection 2020-01-22 20:02:42 +07:00
Book Pauk
fbd50bad1d Исправления багов 2020-01-22 20:02:05 +07:00
Book Pauk
10469bae7b Мелкая поправка 2020-01-22 20:01:21 +07:00
Book Pauk
b6a000a001 Добавлен пакет ws 2020-01-22 20:00:52 +07:00
Book Pauk
59539e7e90 Добавлен класс WebSocketConnection 2020-01-22 19:32:11 +07:00
Book Pauk
a2c41bc5ec Merge tag '0.8.2-2' into develop
0.8.2-2
2020-01-21 16:56:20 +07:00
Book Pauk
c4a06858fb Merge branch 'release/0.8.2-2' 2020-01-21 16:56:12 +07:00
Book Pauk
15b0f05a05 Добавил комментарий 2020-01-21 16:55:41 +07:00
Book Pauk
67feee9aa1 Поправлен баг 2020-01-21 16:53:34 +07:00
Book Pauk
185fb57b8c Удален нерабочий код 2020-01-21 16:25:30 +07:00
Book Pauk
e9039f8208 Merge tag '0.8.2-1' into develop
0.8.2-1
2020-01-21 16:14:21 +07:00
Book Pauk
440d1b3ba0 Merge branch 'release/0.8.2-1' 2020-01-21 16:14:15 +07:00
Book Pauk
9c7a6c64b0 Небольшие поправки 2020-01-21 16:13:38 +07:00
Book Pauk
7cc63fe849 Добавлена автоматическая отправка загруженной книги удаленное хранилище 2020-01-21 15:53:23 +07:00
Book Pauk
5647e8219d Мелкий рефакторинг 2020-01-21 14:58:42 +07:00
Book Pauk
81629fab7a Замена webdav-fs на webdav 2020-01-21 13:54:21 +07:00
Book Pauk
992d2033f3 Merge tag '0.8.2' into develop
0.8.2
2020-01-20 21:49:08 +07:00
Book Pauk
d52d4a1278 Merge branch 'release/0.8.2' 2020-01-20 21:49:00 +07:00
Book Pauk
57a44c5952 Версия 0.8.2 2020-01-20 21:48:31 +07:00
Book Pauk
a04161ac7c Добавил принудительную загрузку книги в обход кэша, если указан URL 2020-01-20 21:44:09 +07:00
Book Pauk
47e46f13c3 Добавлен работа с RemoteWebDavStorage, в т.ч. через api 2020-01-20 21:39:55 +07:00
Book Pauk
5535bd91c8 В конфиг добавлена опция remoteWebDavStorage 2020-01-20 21:37:31 +07:00
Book Pauk
8747a00de6 Поправлен баг 2020-01-20 21:36:44 +07:00
Book Pauk
c926b86926 Добавлен пакет webdav-fs 2020-01-20 21:22:27 +07:00
Book Pauk
010ac9aa7c Доработка api, восстановление кэшированного файла из хранилища 2020-01-20 21:21:13 +07:00
Book Pauk
4ab0c337f1 Рефакторинг 2020-01-15 16:20:46 +07:00
Book Pauk
f814c42fdd Поправлен баг в getStateFinish 2020-01-15 16:06:28 +07:00
Book Pauk
02aee3e625 Добавлена переупаковка файла книги по максимуму через 5 сек после загрузки и конвертирования 2020-01-15 15:49:45 +07:00
Book Pauk
52a32cfdd1 Добавлена обработка ошибок JSON.parse 2020-01-12 20:06:50 +07:00
Book Pauk
6faa7b2efe Уменьшение запросов get-state к api, добавлен метод get-state-finish 2020-01-12 18:51:12 +07:00
Book Pauk
f8481413c9 Мелкий рефакторинг 2020-01-12 17:03:34 +07:00
Book Pauk
7d4baa7046 Поправил цвет github corner 2020-01-11 01:01:18 +07:00
Book Pauk
0951d01383 Merge tag '0.8.1-1' into develop
0.8.1-1
2020-01-10 21:47:58 +07:00
Book Pauk
da34472a6f Merge branch 'release/0.8.1-1' 2020-01-10 21:47:50 +07:00
Book Pauk
a24eaaed50 К предыдущему 2020-01-10 20:39:04 +07:00
Book Pauk
26813c582f Небольшие поправки 2020-01-10 20:37:18 +07:00
Book Pauk
6067ac73e2 Линкуем только необходимые компоненты 2020-01-09 22:00:41 +07:00
Book Pauk
b1d94b67f4 Мелкая поправка 2020-01-09 22:00:22 +07:00
Book Pauk
452f4e69fd Начало перехода от Element-UI на Quasar 2020-01-09 21:07:59 +07:00
Book Pauk
e89b6e3ea0 Добавлен компонент GithubCorner на LoaderPage 2020-01-09 20:15:32 +07:00
Book Pauk
977bab4745 0.8.1 2020-01-09 20:14:49 +07:00
Book Pauk
26c73109fe Небольшая поправка 2020-01-08 13:46:24 +07:00
Book Pauk
65f911ad51 Поправил CRLF => LF 2020-01-07 23:12:06 +07:00
Book Pauk
f8ed5ebd6a Merge tag '0.8.1' into develop
0.8.1
2020-01-07 22:57:38 +07:00
Book Pauk
e4cb61bebe Merge branch 'release/0.8.1' 2020-01-07 22:57:30 +07:00
Book Pauk
7d5310af42 Версия 0.8.1 2020-01-07 22:57:06 +07:00
Book Pauk
f68c610c0d Добавлена поддержка формата FB3 "для галочки" 2020-01-07 22:52:28 +07:00
Book Pauk
ccfb6a6d73 Исправление бага "Request path contains unescaped characters" 2020-01-07 20:31:52 +07:00
Book Pauk
da55996e22 Удалил неиспользуемые пакеты 2020-01-05 15:28:35 +07:00
Book Pauk
ecd8400a34 Удалил неиспользуемые пакеты 2020-01-05 15:15:55 +07:00
Book Pauk
03914883bc Поправил readme 2020-01-05 13:37:09 +07:00
Book Pauk
9981e1f3bd Мелкие поправки 2020-01-03 17:06:03 +07:00
Book Pauk
4d1df66025 Небольшой рефакторинг 2020-01-03 16:58:37 +07:00
Book Pauk
a0f64e188b Поправил readme 2020-01-03 15:59:00 +07:00
Book Pauk
08407a1094 Merge tag '0.8.0-1' into develop
0.8.0-1
2020-01-02 23:45:55 +07:00
Book Pauk
445ea3bb2e Merge branch 'release/0.8.0-1' 2020-01-02 23:45:46 +07:00
Book Pauk
0e0aab98b1 Поправил историю версий 2020-01-02 23:45:16 +07:00
Book Pauk
721d5eb0c1 Поправил readme 2020-01-02 23:45:03 +07:00
Book Pauk
6d99dbc3a7 Мелкая поправка 2020-01-02 22:49:31 +07:00
Book Pauk
2be31f649b Merge tag '0.8.0' into develop
0.8.0
2020-01-02 22:26:15 +07:00
Book Pauk
828ac27c03 Merge branch 'release/0.8.0' 2020-01-02 22:26:06 +07:00
Book Pauk
b3d614002f Поправил readme 2020-01-02 22:25:41 +07:00
Book Pauk
2b2000ca10 Поправил readme 2020-01-02 22:12:43 +07:00
Book Pauk
8d7428d099 Избавление от предупреждений npm 2020-01-02 20:03:20 +07:00
Book Pauk
57f8322f31 Поправил описание 2020-01-02 19:47:07 +07:00
Book Pauk
bee7bc4294 0.8.0 2020-01-02 19:45:13 +07:00
Book Pauk
28702065bc Добавил repository 2020-01-02 19:43:49 +07:00
Book Pauk
c248057081 Добавлен файл лицензии 2020-01-02 19:33:15 +07:00
Book Pauk
6186f5e138 Версия 0.8.0 2020-01-02 19:32:56 +07:00
Book Pauk
2201d8176d Удалена yandex-метрика 2020-01-01 14:55:29 +07:00
Book Pauk
2ba6819876 Окончательный переход на https 2020-01-01 14:48:32 +07:00
Book Pauk
a393b2a370 Окончательный переход на https 2020-01-01 14:34:54 +07:00
Book Pauk
59fe713df2 Немного улучшил загрузку веб-шрифтов 2019-12-25 01:29:32 +07:00
Book Pauk
4b8efaca9a Merge tag '0.7.9-3' into develop
0.7.9-3
2019-12-24 23:47:32 +07:00
Book Pauk
a26100a8d0 Merge branch 'release/0.7.9-3' 2019-12-24 23:47:21 +07:00
Book Pauk
8c52f4718c Добавил require('tls').DEFAULT_MIN_VERSION = 'TLSv1' 2019-12-24 23:45:04 +07:00
Book Pauk
85b5c3c4ec Merge tag '0.7.9-2' into develop
0.7.9-2
2019-12-23 22:25:47 +07:00
Book Pauk
4fd559e4c7 Merge branch 'release/0.7.9-2' 2019-12-23 22:25:40 +07:00
Book Pauk
a337d0ddc7 Попытка обновить pkg, версия node 10.4.1 глючит с setTimeout 2019-12-23 22:24:36 +07:00
Book Pauk
9e4cb7071e Merge tag '0.7.9-1' into develop
0.7.9-1
2019-12-23 21:02:11 +07:00
Book Pauk
c3f1707343 Merge branch 'release/0.7.9-1' 2019-12-23 21:02:02 +07:00
Book Pauk
1ed058a553 Мелкие поправки 2019-12-23 20:57:44 +07:00
Book Pauk
0500a8178d Merge tag '0.7.9' into develop
0.7.9
2019-11-27 18:16:52 +07:00
Book Pauk
7d0059f573 Merge branch 'release/0.7.9' 2019-11-27 18:16:43 +07:00
Book Pauk
4e3b882362 Версия 0.7.9 2019-11-27 18:15:28 +07:00
Book Pauk
13cf47873e Добавлен неубираемый баннер о переходе на httpS 2019-11-27 18:11:34 +07:00
Book Pauk
7ee23ec38f Удален устаревший код 2019-11-27 17:33:30 +07:00
Book Pauk
eebf17c42c Добавлена проверка наличия файла на сервере перед скачиванием fb2 2019-11-27 17:12:07 +07:00
Book Pauk
f84536788b Merge tag '0.7.8b' into develop
0.7.8b
2019-11-25 16:00:10 +07:00
Book Pauk
4bbfdc2cb2 Merge branch 'release/0.7.8b' 2019-11-25 16:00:00 +07:00
Book Pauk
211fec35e3 Исправлен баг 2019-11-25 15:56:29 +07:00
Book Pauk
b8214a46ae Merge tag '0.7.8' into develop
0.7.8
2019-11-25 15:39:35 +07:00
Book Pauk
549ef91c81 Merge branch 'release/0.7.8' 2019-11-25 15:39:23 +07:00
Book Pauk
cede65313b Версия 0.7.8 2019-11-25 15:38:52 +07:00
Book Pauk
d897a7400f Улучшение парсера fb2 2019-11-25 15:36:34 +07:00
Book Pauk
47f059213f Добавлен конвертер для flibusta 2019-11-25 15:21:33 +07:00
Book Pauk
8af51bbf08 Улучшение фильтра html 2019-11-25 15:15:06 +07:00
Book Pauk
53d9f5ddc6 Улучшение конвертирования html->fb2 2019-11-24 15:36:11 +07:00
Book Pauk
06fffdccc8 Merge tag '0.7.7d' into develop
0.7.7d
2019-11-18 20:04:03 +07:00
Book Pauk
aa13dc68fc Merge branch 'release/0.7.7d' 2019-11-18 20:03:46 +07:00
Book Pauk
813876dd90 Поправлены мета-теги 2019-11-18 20:03:09 +07:00
Book Pauk
596c7d65c5 Merge tag '0.7.7c' into develop
0.7.7c
2019-11-16 18:05:56 +07:00
Book Pauk
ce8dcb75bf Merge branch 'release/0.7.7c' 2019-11-16 18:05:40 +07:00
Book Pauk
1bd51b5565 Поправил robots.txt 2019-11-16 18:04:48 +07:00
Book Pauk
1f9ec305b4 Merge tag '0.7.7b' into develop
0.7.7b
2019-11-13 19:42:30 +07:00
Book Pauk
be0f6e57d7 Merge branch 'release/0.7.7b' 2019-11-13 19:42:21 +07:00
Book Pauk
b268e9ee74 Улучшение парсинга html 2019-11-13 19:41:20 +07:00
Book Pauk
e97774435b Merge tag '0.7.7a' into develop
0.7.7a
2019-11-08 17:21:17 +07:00
Book Pauk
93586bc5bb Merge branch 'release/0.7.7a' 2019-11-08 17:21:03 +07:00
Book Pauk
fe23089714 Небольшие поправки верстки 2019-11-08 17:20:31 +07:00
Book Pauk
e743986f38 Merge tag '0.7.7' into develop
0.7.7
2019-11-06 20:18:02 +07:00
Book Pauk
a6c9b700ed Merge branch 'release/0.7.7' 2019-11-06 20:17:42 +07:00
Book Pauk
afa3fcb524 Merge branch 'feature/lss' into develop 2019-11-06 20:17:25 +07:00
Book Pauk
b9aeb648d6 Версия 0.7.7 2019-11-06 20:16:56 +07:00
Book Pauk
5f5df1e5b7 Добавлены жесты для тачскрина 2019-11-06 20:02:21 +07:00
Book Pauk
ad885679e4 Merge branch 'develop' into feature/lss 2019-11-03 18:06:19 +07:00
Book Pauk
e002bebfbe Merge tag '0.7.6c' into develop
0.7.6c
2019-11-03 18:05:52 +07:00
Book Pauk
a8a41e2b3d Merge branch 'release/0.7.6c' 2019-11-03 18:05:42 +07:00
Book Pauk
31940caa84 Поправки логирования 2019-11-03 18:05:02 +07:00
Book Pauk
880334054e Merge tag '0.7.6b' into develop
0.7.6b
2019-11-03 17:56:44 +07:00
Book Pauk
5f03ad5597 Merge branch 'release/0.7.6b' 2019-11-03 17:56:33 +07:00
Book Pauk
1efa3f055d Merge branch 'feature/lss' into develop 2019-11-03 17:56:10 +07:00
Book Pauk
8ccf11278b Поправки логирования 2019-11-03 17:55:43 +07:00
Book Pauk
8a9e7ab4c3 Merge branch 'develop' into feature/lss 2019-11-03 17:06:04 +07:00
Book Pauk
c0fa7c0c51 Merge tag '0.7.6a' into develop
0.7.6a
2019-11-03 17:04:50 +07:00
Book Pauk
022dfd4709 Merge branch 'release/0.7.6a' 2019-11-03 17:04:39 +07:00
Book Pauk
71e08aacc3 Поправки багов 2019-11-03 17:03:58 +07:00
Book Pauk
337eca87f2 Merge tag '0.7.6' into develop
0.7.6
2019-10-30 17:37:19 +07:00
Book Pauk
074aceff8f Merge branch 'release/0.7.6' 2019-10-30 17:37:10 +07:00
Book Pauk
cdc6cf229a Версия 0.7.6 2019-10-30 17:36:46 +07:00
Book Pauk
1f33513dc9 Merge branch 'feature/lss' into develop 2019-10-30 17:35:18 +07:00
Book Pauk
b095b91ff2 Поправки багов 2019-10-30 17:33:13 +07:00
Book Pauk
454a62dbb9 Работа над MegaStorage 2019-10-29 21:03:56 +07:00
Book Pauk
5f7cc12157 Поправка путей 2019-10-29 19:07:34 +07:00
Book Pauk
97ef1ee201 Рефакторинг 2019-10-29 19:05:21 +07:00
Book Pauk
a318568b72 Рефакторинг конфига 2019-10-29 18:58:40 +07:00
Book Pauk
5bb9949440 Изменение путей к модулям 2019-10-29 14:45:55 +07:00
Book Pauk
c33e91d5d0 Рефакторинг, преобразование классов в синглтоны 2019-10-29 14:37:05 +07:00
Book Pauk
ca65ef3cb7 ReaderWorker теперь синглтон 2019-10-28 23:33:10 +07:00
Book Pauk
9ebdbc81d0 Рефакторинг 2019-10-28 23:30:02 +07:00
Book Pauk
b64985349e Рефакторинг 2019-10-28 22:49:02 +07:00
Book Pauk
625fd9d1a4 Поправки 2019-10-28 21:17:52 +07:00
Book Pauk
eac5fdcec0 Рефакторинг, поправки 2019-10-28 21:14:50 +07:00
Book Pauk
970b4d5d97 Мелкий рефакторинг 2019-10-28 20:52:04 +07:00
Book Pauk
f741bc818d Добавлен метод readFiles 2019-10-28 20:31:06 +07:00
Book Pauk
5f04c24187 Убрал adm-zip 2019-10-28 16:04:09 +07:00
Book Pauk
a382bef336 Замена AdmZip на ZipStreamer 2019-10-28 16:02:44 +07:00
Book Pauk
4ddf28f344 Добавлен метод unpack 2019-10-28 15:55:26 +07:00
Book Pauk
0dc650305a Поправки потенциальных багов, мелкий рефакторинг 2019-10-27 19:36:50 +07:00
Book Pauk
697093d1c9 Поправка бага 2019-10-27 19:27:26 +07:00
Book Pauk
622f7a4479 Исправление бага 2019-10-27 19:24:18 +07:00
Book Pauk
c4b607804b Заготовка LibSharedStorage 2019-10-27 19:19:20 +07:00
Book Pauk
864f008679 Работа над MegaStorage 2019-10-27 19:18:37 +07:00
Book Pauk
25f309bcb0 Добавлены модули node-stream-zip, zip-stream 2019-10-27 17:57:21 +07:00
Book Pauk
1354361ad9 Убрал дебаг 2019-10-27 17:55:54 +07:00
Book Pauk
8136c7b072 Начало работы над LibSharedStorage 2019-10-25 21:18:09 +07:00
Book Pauk
c9243e7249 Поправка конфигов 2019-10-25 21:17:50 +07:00
Book Pauk
1a487da3d9 Небольшой рефакторинг 2019-10-25 21:16:22 +07:00
Book Pauk
b52395751c Поправил размер кнопок 2019-10-25 13:37:20 +07:00
Book Pauk
cfa6cc9a83 Merge tag '0.7.5a' into develop
0.7.5a
2019-10-23 23:58:03 +07:00
Book Pauk
f203384b00 Merge branch 'release/0.7.5a' 2019-10-23 23:57:51 +07:00
Book Pauk
9ac3be455c Поправки багов 2019-10-23 23:53:37 +07:00
Book Pauk
20b74a9dcd Небольшие поправки 2019-10-23 20:42:14 +07:00
Book Pauk
3b848a5a86 Исправления конфига nginx 2019-10-23 20:07:38 +07:00
Book Pauk
a9b5e865a5 Merge tag '0.7.5b' into develop
0.7.5b
2019-10-22 18:24:20 +07:00
Book Pauk
ab46a1b99d Merge branch 'release/0.7.5b' 2019-10-22 18:24:06 +07:00
Book Pauk
4a08465f5b Поправка бага 2019-10-22 18:23:17 +07:00
Book Pauk
a7960d6cd6 Merge tag '0.7.5' into develop
0.7.5
2019-10-22 17:59:15 +07:00
Book Pauk
3caea77dde Merge branch 'release/0.7.5' 2019-10-22 17:59:05 +07:00
Book Pauk
fdaa3b7f93 Версия 0.7.5 2019-10-22 17:58:20 +07:00
Book Pauk
4f433b4456 Поправил описание 2019-10-22 17:56:17 +07:00
Book Pauk
309a9ad4fb Улучшение синхронизации 2019-10-22 15:41:14 +07:00
Book Pauk
b0e7431e72 Поправки синхронизации 2019-10-21 22:03:00 +07:00
Book Pauk
158118d183 Мелкая поправка 2019-10-21 21:36:43 +07:00
Book Pauk
382e37fc5a Merge tag '0.7.4b' into develop
0.7.4b
2019-10-21 20:13:21 +07:00
Book Pauk
3390676847 Merge branch 'release/0.7.4b' 2019-10-21 20:13:10 +07:00
Book Pauk
544a995312 Поправки багов синхронизации 2019-10-21 20:07:42 +07:00
Book Pauk
f209d49bb5 Поправил настройки кеширования 2019-10-21 13:07:16 +07:00
Book Pauk
42ed691fdc Merge tag '0.7.4' into develop
0.7.4
2019-10-21 12:10:45 +07:00
Book Pauk
98d2e9d266 Merge branch 'release/0.7.4' 2019-10-21 12:10:35 +07:00
Book Pauk
6111158896 Версия 0.7.4 2019-10-21 12:10:13 +07:00
Book Pauk
3267fc653c Поправил тему 2019-10-21 12:08:15 +07:00
Book Pauk
7250608767 Временно переключил Vue на ветку github:paulkamer/vue#fix_palemoon_clickhandlers_dist
до выхода нового релиза Vue с исправлениями
2019-10-21 12:03:11 +07:00
Book Pauk
e82063e435 Изменение темы, css подключается целиком 2019-10-21 11:31:41 +07:00
Book Pauk
6d4c44bc25 Обновлена тема element-ui 2019-10-21 11:27:04 +07:00
Book Pauk
2bc94d8792 Улучшил сообщение о версиях продукта 2019-10-21 09:23:49 +07:00
Book Pauk
4ca3edd789 Поправил текст 2019-10-21 09:23:29 +07:00
Book Pauk
d6859efde2 Добавлено использование reader-api от новой читалки для конвертации книг 2019-10-21 08:38:56 +07:00
Book Pauk
3f8cbfa259 Поправка бага 2019-10-20 18:22:21 +07:00
Book Pauk
5d18c9371d Мелкая поправка 2019-10-20 18:09:24 +07:00
Book Pauk
631990e3bb Merge tag '0.7.3b' into develop
0.7.3b
2019-10-19 19:28:51 +07:00
Book Pauk
4ae7338f94 Merge branch 'release/0.7.3b' 2019-10-19 19:28:36 +07:00
Book Pauk
0d1e51cb21 Поправка бага 2019-10-19 19:27:18 +07:00
Book Pauk
475fb833ea Merge tag '0.7.3' into develop
0.7.3
2019-10-18 19:30:51 +07:00
Book Pauk
580b030ee4 Merge branch 'release/0.7.3' 2019-10-18 19:30:42 +07:00
Book Pauk
6a7cbc70d6 0.7.3 2019-10-18 19:30:21 +07:00
Book Pauk
d76f60639c Добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках 2019-10-18 19:13:17 +07:00
Book Pauk
e2bea407ee 0.7.3 2019-10-18 17:12:30 +07:00
Book Pauk
558fed31aa Добавлен фильтр для некоторых сайтов 2019-10-18 17:07:28 +07:00
Book Pauk
f6513d40c8 Небольшая поправка 2019-10-18 15:32:05 +07:00
Book Pauk
259f9baa59 Merge branch 'feature/ss_fix2' into develop 2019-10-18 15:25:57 +07:00
Book Pauk
db5650e276 Мелкая поправка 2019-10-18 15:20:32 +07:00
Book Pauk
51ebbbc569 Закончена очередная переработка loadRecent & saveRecent 2019-10-18 15:19:53 +07:00
Book Pauk
5184661652 Мелкая поправка 2019-10-18 14:00:52 +07:00
Book Pauk
7853a14ce6 Начало очередной переделки ServerStorage 2019-10-18 13:53:44 +07:00
Book Pauk
a01e78ace9 Merge tag '0.7.2' into develop
0.7.2
2019-10-18 09:17:58 +07:00
Book Pauk
f7eb576d0d Merge branch 'release/0.7.2' 2019-10-18 09:17:45 +07:00
Book Pauk
34f1ad8fae Версия 0.7.2 2019-10-18 09:16:52 +07:00
Book Pauk
c60f0991df Правка багов 2019-10-18 09:06:04 +07:00
Book Pauk
d505fd0795 Мелкая поправка 2019-10-17 21:38:13 +07:00
Book Pauk
93cf506535 Поправка бага 2019-10-17 20:38:52 +07:00
Book Pauk
bfb37e55d4 Поправка отображения прогрессбара загрузки книги 2019-10-17 19:18:56 +07:00
Book Pauk
92afc5cb33 Поправки багов 2019-10-17 19:09:05 +07:00
Book Pauk
75cb611701 Поправки багов 2019-10-17 17:20:43 +07:00
Book Pauk
2ec1dd58a5 Добавлена ссылка "Старая версия" 2019-10-17 13:49:24 +07:00
Book Pauk
7d59af54de Улучшил определение кодировки 2019-10-16 21:48:41 +07:00
Book Pauk
2b5f47b3de Замена decompress-zip на adm-zip 2019-10-16 21:08:34 +07:00
Book Pauk
16eebfb9a4 Поправил текст ссылки 2019-10-16 16:41:26 +07:00
Book Pauk
9025218671 Мелкая поправка 2019-10-16 11:19:40 +07:00
Book Pauk
6bccb546bb Merge tag '0.7.1e' into develop
0.7.1e
2019-09-21 11:29:57 +07:00
Book Pauk
29d49046a0 Merge branch 'release/0.7.1e' 2019-09-21 11:29:46 +07:00
Book Pauk
717af9ffaf Изменил местоположение кнопки "Автономный режим" 2019-09-21 11:28:31 +07:00
Book Pauk
00060c9f43 Merge tag '0.7.1d' into develop
0.7.1d
2019-09-21 00:25:17 +07:00
Book Pauk
759ff46c92 Merge branch 'release/0.7.1d' 2019-09-21 00:25:08 +07:00
Book Pauk
41957cdceb Актуализирована справка, доделки 2019-09-21 00:24:27 +07:00
Book Pauk
d418e3a1c9 Merge tag '0.7.1c' into develop
0.7.1c
2019-09-20 23:54:50 +07:00
Book Pauk
f650124428 Merge branch 'release/0.7.1c' 2019-09-20 23:54:38 +07:00
Book Pauk
795d109c76 Поправил описание 2019-09-20 23:54:02 +07:00
Book Pauk
6868b3effc Добавлена кнопка offlineMode 2019-09-20 23:52:45 +07:00
Book Pauk
26747b7013 Небольшие поправки 2019-09-20 22:44:36 +07:00
Book Pauk
5198f8aa60 Merge tag '0.7.1b' into develop
0.7.1b
2019-09-20 22:18:09 +07:00
Book Pauk
552da48a32 Merge branch 'release/0.7.1b' 2019-09-20 22:17:58 +07:00
Book Pauk
db8a688620 Манипуляции с appcache 2019-09-20 22:17:28 +07:00
Book Pauk
3088028d05 Поправки багов 2019-09-20 21:45:29 +07:00
Book Pauk
fd62ef865d Merge tag '0.7.1a' into develop
0.7.1a
2019-09-20 20:37:33 +07:00
Book Pauk
ed74ed00ed Merge branch 'release/0.7.1a' 2019-09-20 20:37:23 +07:00
Book Pauk
741317aaaf К предыдущему 2019-09-20 20:36:31 +07:00
Book Pauk
9b6ecd4e6b Убрал вычисление диффа 2019-09-20 20:35:12 +07:00
Book Pauk
7863b3358e Убрал appcache 2019-09-20 20:34:42 +07:00
Book Pauk
e1be68ec3d Поправка бага 2019-09-20 20:20:11 +07:00
Book Pauk
a054186d4b Merge tag '0.7.1' into develop
Версия 0.7.1
2019-09-20 19:58:36 +07:00
Book Pauk
2d5c549c83 Merge branch 'release/0.7.1' 2019-09-20 19:58:22 +07:00
Book Pauk
9f6072dfe1 Версия 0.7.1 2019-09-20 19:54:59 +07:00
Book Pauk
69c44fe1ab Откатил новые версии pkg и sqlite, новый pkg глючит 2019-09-20 19:53:55 +07:00
Book Pauk
4fa7b2443e Добавил дебаг-лог в periodicCleanDir 2019-09-20 19:35:22 +07:00
Book Pauk
25a69592bb Правка багов 2019-09-20 19:14:14 +07:00
Book Pauk
44e0b26990 Поправка бага 2019-09-20 18:58:36 +07:00
Book Pauk
c4496f8dc8 Улучшение механизма синхронизации 2019-09-20 18:53:21 +07:00
Book Pauk
9e296231d9 Поправки багов 2019-09-20 16:54:03 +07:00
Book Pauk
49b3f05d65 Поправки багов 2019-09-20 16:38:33 +07:00
Book Pauk
f124b9c050 Переделки синхронизации, замена diff на delta 2019-09-20 16:29:19 +07:00
Book Pauk
63a86f7c06 Версия 0.7.1 2019-09-20 14:04:17 +07:00
Book Pauk
fd0f523c64 Улучшение синхронизации 2019-09-19 20:39:01 +07:00
Book Pauk
487e605520 Поправлен баг 2019-09-19 18:19:14 +07:00
Book Pauk
9e169e1f4b Улучшение синхронизации 2019-09-19 17:51:04 +07:00
Book Pauk
9612e7ebcd Merge tag '0.7.0' into develop
0.7.0
2019-09-07 22:15:32 +07:00
Book Pauk
f66162efe7 Merge branch 'release/0.7.0' 2019-09-07 22:15:14 +07:00
Book Pauk
656642697b Версия 0.7.0 2019-09-07 22:13:13 +07:00
Book Pauk
feb70f85f8 Merge branch 'feature/ss_fix' into develop 2019-09-07 22:00:35 +07:00
Book Pauk
ab1981559b Поправка бага 2019-09-07 21:59:00 +07:00
Book Pauk
c8852d9a8e Небольшая доработка 2019-09-07 20:40:48 +07:00
Book Pauk
9ac8dc7fd1 Доработки отображения диалогов на смартфонах 2019-09-07 17:39:00 +07:00
Book Pauk
c9419d99e6 К предыдущему 2019-09-07 16:44:00 +07:00
Book Pauk
a1f4a83e72 Работа над saveRecent 2019-09-07 16:39:29 +07:00
Book Pauk
a8abd5d427 Эталонный работающий вариант ServerStorage без оптимизации, с дебагом 2019-09-07 12:25:14 +07:00
Book Pauk
629d1b0630 Поправка рассылки сообщений 2019-09-07 12:17:08 +07:00
Book Pauk
97c368f63a Поправки уведомления 2019-09-07 11:02:52 +07:00
Book Pauk
3266a444d0 Доработка 2019-09-06 22:36:48 +07:00
Book Pauk
1c246f71f8 Доделки ServerStorage 2019-09-06 22:07:15 +07:00
Book Pauk
96945dfc4a Начало переделки ServerStorage 2019-09-06 18:47:07 +07:00
Book Pauk
30eb3001ef Переход на https 2019-09-06 15:10:40 +07:00
Book Pauk
bdd8636390 Переход на https-версию, небольшой рефакторинг, улучшения 2019-09-06 15:06:58 +07:00
Book Pauk
f762d2a271 Сделан npm update, поправлены ошибки 2019-09-03 22:45:00 +07:00
Book Pauk
cf2efc2b92 Добавлены уведомления о выходе новой httpS версии сайта 2019-09-03 19:59:43 +07:00
Book Pauk
7670da4cba Мелкие поправки 2019-08-30 02:35:56 +07:00
Book Pauk
d87f9f2a21 Добавил конфиг для https с помощью certbot 2019-08-30 02:14:25 +07:00
Book Pauk
6e690f3fea Добавил cache.manifest 2019-08-29 16:30:24 +07:00
Book Pauk
6321002617 Коррекция размеров окна 2019-08-29 15:57:47 +07:00
Book Pauk
15ec362428 Поправлен баг - не распознавались картинки, если в fb2 указан binaryType == 'application/octet-stream' 2019-08-29 15:25:50 +07:00
Book Pauk
454004e705 Поправил дефолт 2019-08-28 18:45:13 +07:00
Book Pauk
e14b414fc1 Поправил баг 2019-08-28 18:41:29 +07:00
Book Pauk
c4b47a5915 Обновил element-ui 2019-08-28 18:34:51 +07:00
Book Pauk
957c252cd7 Отключил пока ServerStorage 2019-08-28 18:33:26 +07:00
Book Pauk
d6a6c21762 К предыдущему 2019-08-28 18:15:12 +07:00
Book Pauk
834580cfdf Поправил описание 2019-08-28 17:36:31 +07:00
Book Pauk
de13cfb555 К предыдущему 2019-08-28 17:30:29 +07:00
Book Pauk
4f87508834 К предыдущему 2019-08-28 17:14:58 +07:00
Book Pauk
682a044f32 Добавлена возможность двигать окна, небольшое облагораживание отображения 2019-08-28 16:48:16 +07:00
Book Pauk
bdb5d90b1d Переименование HistoryPage -> RecentBooksPage 2019-08-28 11:03:09 +07:00
Book Pauk
01880f4456 Добавил описание 0.7.0 2019-08-28 10:48:04 +07:00
Book Pauk
39f78ce7e8 Добавлен параметр compactTextPerc 2019-08-23 19:48:55 +07:00
Book Pauk
755c6b92da Мелкие поправки 2019-08-23 18:55:14 +07:00
Book Pauk
2eab9c2837 Улучшение анимации листания 2019-08-23 15:38:12 +07:00
Book Pauk
63861789de Мелкая поправка 2019-08-23 13:38:23 +07:00
Book Pauk
086c353eff Поправки багов 2019-08-23 13:32:18 +07:00
Book Pauk
4fe5b44655 Мелкие поправки 2019-08-23 12:48:32 +07:00
Book Pauk
036547e260 Мелкая поправка 2019-08-22 23:53:16 +07:00
Book Pauk
696f434c90 Улучшение отображения загрузки списка недавних 2019-08-22 23:37:55 +07:00
Book Pauk
0c654d9346 К предыдущему 2019-08-22 20:01:48 +07:00
Book Pauk
a2c393b06b Рефакторинг, упрощение, начало переделки ServerStorage 2019-08-22 15:37:15 +07:00
Book Pauk
eae2c2b102 Merge tag '0.6.10' into develop
0.6.10
2019-07-21 14:44:57 +07:00
Book Pauk
d9e49e3484 Merge branch 'release/0.6.10' 2019-07-21 14:44:43 +07:00
Book Pauk
a28d4c2f1c Версия 0.6.10 2019-07-21 14:42:54 +07:00
Book Pauk
9af055ec54 Поправки порядка загрузки компонентов и сопутствующих багов 2019-07-21 14:39:06 +07:00
Book Pauk
0d41171e9d Улучшение отзывчивости прогрессбаров 2019-07-21 12:15:33 +07:00
Book Pauk
08af826ae9 Merge tag '0.6.9s' into develop
0.6.9s
2019-06-26 20:04:45 +07:00
Book Pauk
4fd577d7c5 Merge branch 'release/0.6.9s' 2019-06-26 20:04:25 +07:00
Book Pauk
2c8efebe98 Поправлен баг клика в статус баре 2019-06-26 20:03:19 +07:00
Book Pauk
93c9fb53ac Merge tag '0.6.9' into develop
Версия 0.6.9
2019-06-23 18:51:11 +07:00
Book Pauk
5a4d249cf9 Merge branch 'release/0.6.9' 2019-06-23 18:50:57 +07:00
Book Pauk
4cc7bdee37 Версия 0.6.9 2019-06-23 18:50:28 +07:00
Book Pauk
a6af568411 Ускорил сжатие книги при сохранении в BookStore 2019-06-23 18:49:41 +07:00
Book Pauk
576a6a094a Merge tag '0.6.8' into develop
Версия 0.6.8
2019-06-23 17:20:02 +07:00
Book Pauk
e671e4b6f5 Merge branch 'release/0.6.8' 2019-06-23 17:19:48 +07:00
Book Pauk
a66b2a4c70 Версия 0.6.8 2019-06-23 17:19:30 +07:00
Book Pauk
f1ae409535 На страницу загрузки добавлен блок "Поделиться" 2019-06-23 17:18:04 +07:00
Book Pauk
a4b56b477d Исправление автоформирования заголовка при вставке из буфера обмена 2019-06-23 16:29:36 +07:00
Book Pauk
d9c389812a Добавлен новый вариант анимации перелистывания - листание 2019-06-23 15:51:55 +07:00
Book Pauk
074ef3645f Добавлен вариант перелистывания - rotate 2019-06-23 14:13:59 +07:00
Book Pauk
cc3aa413e8 Исправил сообщение о загрузке шрифтов 2019-06-09 18:23:04 +07:00
Book Pauk
7f90c09227 Улучшение прогрессбара загрузки/сохранения книги 2019-06-09 16:44:11 +07:00
Book Pauk
f6f4d8ccc9 Исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8 2019-06-09 15:03:04 +07:00
Book Pauk
31afce8304 Исправление бага - падение сервера при распаковке битых архивов 2019-06-04 17:35:32 +07:00
Book Pauk
2c4ff856cd Merge tag '0.6.7' into develop
Версия 0.6.7
2019-05-30 16:16:19 +07:00
Book Pauk
f59974e310 Merge branch 'release/0.6.7' 2019-05-30 16:16:08 +07:00
Book Pauk
70e2c12a6b Версия 0.6.7 2019-05-30 16:15:46 +07:00
Book Pauk
11f3c6ce6f Мелкие поправки 2019-05-30 16:14:41 +07:00
Book Pauk
e213c4640b Добавлен GET-параметр вида "reader?__pp=50.5&url=..." для указания позиции в книге в процентах 2019-05-30 16:00:47 +07:00
Book Pauk
959c5eaa59 Добавлен GET-параметр вида "reader?__refresh=1&url=..." для принудительного обновления загружаемого текста 2019-05-30 14:54:55 +07:00
Book Pauk
66fa510b26 Добавлена возможность указать название текста 2019-05-28 16:32:54 +07:00
Book Pauk
f26a3b31ac На страницу загрузки добавлена возможность загрузки книги из буфера обмена 2019-05-27 16:25:51 +07:00
Book Pauk
724fbf579e Мелкое форматирование 2019-05-27 15:10:40 +07:00
Book Pauk
f192f8e3cd Мелкий рефакторинг 2019-05-27 15:09:55 +07:00
Book Pauk
f13c3d19fb - добавлена возможность настройки отображаемых кнопок на панели управления
- некоторые кнопки на панели управления были скрыты по-умолчанию
2019-05-26 16:16:20 +07:00
Book Pauk
b51a09efcc В справку добавлена история версий проекта 2019-05-26 14:01:56 +07:00
Book Pauk
6004043782 Мелкие поправки 2019-05-23 13:47:36 +07:00
Book Pauk
f9fd0dc2c3 Поправлен баг 2019-05-23 13:47:12 +07:00
Book Pauk
eb5411cd20 К предыдущему 2019-04-27 19:13:56 +07:00
Book Pauk
da3c7a02f0 Небольшие поправки отображения загрузки шрифта 2019-04-27 18:34:23 +07:00
Book Pauk
e67d05007f Поправлен баг 2019-04-27 17:21:49 +07:00
Book Pauk
b0a9a6a08e Добавлена настройка showWhatsNewDialog 2019-04-27 17:04:34 +07:00
Book Pauk
d848ea35f4 Поправки верстки 2019-04-27 16:58:04 +07:00
Book Pauk
350f20effe Добавлена история версий 2019-04-27 16:40:48 +07:00
Book Pauk
b6dc8f98fe Добавлен диалог whatsNew 2019-04-27 15:40:11 +07:00
Book Pauk
1b762ee48d Merge tag '0.6.6' into develop
0.6.6
2019-03-28 14:48:17 +07:00
Book Pauk
cc3d7f1eac Merge branch 'release/0.6.6' 2019-03-28 14:47:52 +07:00
Book Pauk
4107282fbf Версия 0.6.6 2019-03-28 14:47:28 +07:00
Book Pauk
c29ffc3fcd Поправки багов 2019-03-28 14:45:42 +07:00
Book Pauk
f648bcda13 Доработки, оптимизация сохранения recentLast 2019-03-28 14:05:13 +07:00
Book Pauk
aa0044eed2 package-lock.json 2019-03-28 13:15:29 +07:00
Book Pauk
2312a721ae Поправлен текст помощи для автономной загрузки читалки 2019-03-28 13:14:57 +07:00
Book Pauk
b93fc39b00 Мелкая поправка 2019-03-28 12:44:27 +07:00
Book Pauk
2dc2cd700f Merge tag '0.6.5' into develop
0.6.5
2019-03-25 14:04:51 +07:00
Book Pauk
d69e534f8b Merge branch 'release/0.6.5' 2019-03-25 14:04:43 +07:00
Book Pauk
1de9ddd394 Версия 0.6.5 2019-03-25 14:04:16 +07:00
Book Pauk
77c68d4e11 Небольшие поправки 2019-03-25 14:03:50 +07:00
Book Pauk
2a0d1dcfce Поправка бага 2019-03-25 13:06:48 +07:00
Book Pauk
5a19cca407 Поправка текста 2019-03-25 12:53:50 +07:00
Book Pauk
4e8773ecde Мелкая поправка 2019-03-25 12:51:01 +07:00
Book Pauk
4c7dada809 Merge tag '0.6.4' into develop
0.6.4
2019-03-24 14:33:19 +07:00
Book Pauk
65690b15da Merge branch 'release/0.6.4' 2019-03-24 14:33:09 +07:00
Book Pauk
8ba07812ce Оптимизация 2019-03-24 14:32:08 +07:00
Book Pauk
2dd8f35001 Версия 0.6.4 2019-03-24 14:04:46 +07:00
Book Pauk
2d15aa88d4 Исправления багов 2019-03-24 14:04:21 +07:00
Book Pauk
e4257e50f0 Merge tag '0.6.3' into develop
0.6.3
2019-03-24 12:52:10 +07:00
Book Pauk
33ebc07915 Merge branch 'release/0.6.3' 2019-03-24 12:51:55 +07:00
Book Pauk
bc07299626 Версия 0.6.3 2019-03-24 12:51:27 +07:00
Book Pauk
25e8aeef53 Merge tag '0.6.2' into develop
0.6.2
2019-03-24 12:28:43 +07:00
Book Pauk
a2ed34abf3 Merge branch 'release/0.6.2' 2019-03-24 12:28:28 +07:00
Book Pauk
36a7b7b91a Версия 0.6.2, поправка мелкого бага 2019-03-24 12:27:43 +07:00
Book Pauk
b4e8b7375f Merge tag '0.6.1' into develop
0.6.1
2019-03-24 12:15:16 +07:00
Book Pauk
153b635bdb Merge branch 'release/0.6.1' 2019-03-24 12:15:05 +07:00
Book Pauk
80af72465e Версия 0.6.1, поправлен баг 2019-03-24 12:14:24 +07:00
Book Pauk
a91a8f9993 Merge tag '0.6.0' into develop
0.6.0
2019-03-24 12:07:41 +07:00
Book Pauk
a0ccc7fe07 Merge branch 'release/0.6.0' 2019-03-24 12:07:32 +07:00
Book Pauk
c162c9ae0e Версия 0.6.0 2019-03-24 12:07:10 +07:00
Book Pauk
25542cdff3 Исправления багов 2019-03-24 11:44:15 +07:00
Book Pauk
16d0ae60c1 Добавлен бэкап БД при запуске 2019-03-24 11:35:43 +07:00
Book Pauk
b1937eb8c0 Мелкий рефакторинг 2019-03-24 11:24:54 +07:00
Book Pauk
3f6b468021 Поправки багов 2019-03-22 20:14:33 +07:00
Book Pauk
92d929b704 Доделки сохранения recentLast 2019-03-22 17:03:06 +07:00
Book Pauk
737ae75c28 Синхронизация recent, пока не оптимизировано 2019-03-22 14:14:47 +07:00
Book Pauk
79ced4eca4 Работа над ServerStorage - saveRecent 2019-03-22 13:20:43 +07:00
Book Pauk
329ac44c11 Работа над ServerStorage, попутный рефакторинг 2019-03-22 13:00:59 +07:00
Book Pauk
f65a91dfed Работа над ServerStorage 2019-03-22 11:58:14 +07:00
Book Pauk
2a79207427 Поправил баг 2019-03-20 15:46:31 +07:00
Book Pauk
70be3d10d0 Дебаг 2019-03-20 15:38:06 +07:00
Book Pauk
3500a40599 Рефакторинг 2019-03-20 15:22:34 +07:00
Book Pauk
090ffa9921 Добавление событий, рефакторинг 2019-03-20 14:57:40 +07:00
Book Pauk
b12198fdcf Работа над ServerStorage, попутные оптимизации 2019-03-20 14:34:01 +07:00
Book Pauk
826ee18666 Работа над ServerStorage 2019-03-20 13:59:37 +07:00
Book Pauk
f9d8b37b1a Небольшие доработки 2019-03-20 12:30:13 +07:00
Book Pauk
e626cb6b40 Переделал механизм удаления recent 2019-03-18 19:38:47 +07:00
Book Pauk
20697ad9e4 Удалил более не нужный restoreOldSettings 2019-03-18 18:52:50 +07:00
Book Pauk
0800385b96 Мелкий рефакторинг 2019-03-18 18:48:16 +07:00
Book Pauk
d6e326e8be Небольшая оптимизация 2019-03-18 18:40:03 +07:00
Book Pauk
8b969a6d36 Мелкая поправка 2019-03-18 17:05:26 +07:00
Book Pauk
d520e13c88 Добавил настройку отображения уведомлений от синхронизатора 2019-03-18 17:03:28 +07:00
Book Pauk
ae4081001c Поправки сообщений 2019-03-18 16:52:30 +07:00
Book Pauk
5a48b597b9 Небольшие доработки 2019-03-18 16:47:35 +07:00
Book Pauk
c8a953db7c На LoaderPage всегда показываем toolBar 2019-03-17 23:46:25 +07:00
Book Pauk
d20ec144ff Небольшая доработка 2019-03-17 23:39:00 +07:00
Book Pauk
0147a82b0a Добавил ввод ключа доступа по ссылке 2019-03-17 23:33:34 +07:00
Book Pauk
8732a78d01 Небольшие поправки 2019-03-17 22:47:57 +07:00
Book Pauk
015254ae40 Мелкая поправка 2019-03-17 22:39:40 +07:00
Book Pauk
712bf405bb Работа с ServerStorage 2019-03-17 22:04:56 +07:00
Book Pauk
3a46a157f9 Работа над ServerStorage 2019-03-17 20:09:16 +07:00
Book Pauk
2a4ff926ae Работа над ServerStorage 2019-03-17 19:41:47 +07:00
Book Pauk
58941116c8 Работа над ключом доступа 2019-03-17 18:10:26 +07:00
Book Pauk
a13146d722 Работа над профилями 2019-03-17 16:14:03 +07:00
Book Pauk
02e6f392b4 Работа над профилями 2019-03-17 16:05:38 +07:00
Book Pauk
d4515bd643 Работа над профилями 2019-03-17 15:02:12 +07:00
Book Pauk
a73555b7ca Улучшение парсиннга 2019-03-16 16:45:38 +07:00
Book Pauk
983d9ee1b9 Улучшение парсинга html 2019-03-16 16:40:31 +07:00
Book Pauk
e800dfe796 Мелкий рефакторинг 2019-03-16 02:24:07 +07:00
Book Pauk
b0c59be340 Поправил комментарий 2019-03-16 01:56:16 +07:00
Book Pauk
dca12b6467 Сконфигурировал TerserPlugin 2019-03-16 01:52:51 +07:00
Book Pauk
5a0d98cbd0 Допиливание sjcl 2019-03-16 01:51:48 +07:00
Book Pauk
9cbaf22270 Добавлен модуль sjcl для шифрования AES, т.к. WebCrypto API не работает с http, а только с https 2019-03-16 01:11:20 +07:00
Book Pauk
a64687f64f Промежуточный коммит 2019-03-15 19:31:09 +07:00
Book Pauk
d229aab8c9 Работа над профилями и ключом доступа 2019-03-15 19:11:27 +07:00
Book Pauk
2ff94c1458 Работа над ServerStorage - профили 2019-03-15 17:56:19 +07:00
Book Pauk
3b9f3ea81d Мелкая поправка 2019-03-15 15:29:43 +07:00
Book Pauk
23f12ad3cf Добавил время записи в item 2019-03-15 13:52:37 +07:00
Book Pauk
01e7c1f183 Добавлено кодирование id в base58 при сохранении в server-storage 2019-03-15 13:15:08 +07:00
Book Pauk
37d60bc9b9 Улучшил парсинг имени автора из fb2 2019-03-15 11:27:34 +07:00
Book Pauk
cd5d3903fe Работа над ServerStorage 2019-03-13 20:22:04 +07:00
Book Pauk
6904cfd224 Добавил сжатие данных книги в кеше 2019-03-13 17:41:32 +07:00
Book Pauk
c430e2c8f4 Увличил каоличество шагов для отсылки прогресса 2019-03-13 17:41:08 +07:00
Book Pauk
0cf8a94b24 Небольшая оптимизация по памяти 2019-03-13 16:47:22 +07:00
Book Pauk
ff3674aca7 Мелкие поправки 2019-03-13 00:57:09 +07:00
Book Pauk
b50498fa46 Поправил баг 2019-03-13 00:51:16 +07:00
Book Pauk
571f71c7f0 Работа над ServerStorage 2019-03-13 00:14:43 +07:00
Book Pauk
091c50ec84 Модуль с утилитами шифрования 2019-03-12 23:35:25 +07:00
Book Pauk
e473dc8843 Работа над ServerStorage 2019-03-12 23:34:41 +07:00
Book Pauk
886af11d3a Добавлены пакеты base-x, pako, safe-buffer 2019-03-12 18:56:38 +07:00
Book Pauk
c72fd7ee9c Компонент ServerStorage, добавлена работа с api reader/storage 2019-03-11 19:22:59 +07:00
Book Pauk
7dc76b4222 Мелкая поправка 2019-03-11 16:02:05 +07:00
Book Pauk
5011e23050 Добавил периодическую очистку кэша 2019-03-11 15:59:05 +07:00
Book Pauk
89d9a90901 Небольшая оптимизация 2019-03-08 20:50:30 +07:00
Book Pauk
05128b12a8 Добавлен метод api /reader/storage и класс ReaderStorage 2019-03-08 20:38:07 +07:00
Book Pauk
c287ca9ea8 Пробные миграции 2019-03-08 18:26:03 +07:00
Book Pauk
5122cda6db Добавляем миграции в БД sqlite 2019-03-08 18:05:58 +07:00
Book Pauk
a39626f867 Добавлен connManager для управления пулами соединений к базам Sqlite, попутный рефакторинг 2019-03-08 16:50:44 +07:00
Book Pauk
c7abae10b7 Мелкий рефакторинг 2019-03-08 13:37:27 +07:00
Book Pauk
9a8f35fd8a Merge tag '0.5.6' into develop
0.5.6
2019-03-07 20:20:45 +07:00
Book Pauk
0341cc1630 Merge branch 'release/0.5.6' 2019-03-07 20:20:26 +07:00
Book Pauk
d307d233f0 Версия 0.5.6 2019-03-07 20:16:20 +07:00
Book Pauk
5931b9625b Изменил механизмы overflow при отрисовке страницы - теперь не "съедает" часть шрифта 2019-03-07 20:12:38 +07:00
Book Pauk
fb837f5b97 Merge tag '0.5.5' into develop
0.5.5
2019-03-05 18:38:45 +07:00
Book Pauk
8cfe95b3cf Merge branch 'release/0.5.5' 2019-03-05 18:38:32 +07:00
Book Pauk
5fd73ac1e1 Версия 0.5.5 2019-03-05 18:37:41 +07:00
Book Pauk
b51a574038 Мелкие поправки текста 2019-03-05 18:37:08 +07:00
Book Pauk
51b39f0775 Улучшение парсинга pdf 2019-03-04 23:28:27 +07:00
Book Pauk
17c4f96c94 Merge tag '0.5.4' into develop
0.5.4
2019-03-04 22:57:47 +07:00
Book Pauk
89bf907613 Merge branch 'release/0.5.4' 2019-03-04 22:57:38 +07:00
Book Pauk
641d0e45fd Версия 0.5.4 2019-03-04 22:57:10 +07:00
Book Pauk
b3e579d8b7 Улучшение парсинга pdf и html 2019-03-04 22:56:15 +07:00
Book Pauk
fcb61c89d5 Улучшение парсинга html 2019-03-04 22:42:54 +07:00
Book Pauk
3483d78c2c Улучшение парсинга pdf и текстов 2019-03-04 22:28:11 +07:00
Book Pauk
36b14d0b3a Мелкая поправка 2019-03-04 21:26:07 +07:00
Book Pauk
2f8b68ec62 Улучшение парсинга Pdf 2019-03-04 21:22:12 +07:00
Book Pauk
cb65cac333 Конвертер pdf - загружаем изображения 2019-03-04 20:00:51 +07:00
Book Pauk
d12ffc3d0d Поправил комментарий 2019-03-04 16:39:30 +07:00
Book Pauk
921744167e Поправка мелкого бага 2019-03-03 12:32:22 +07:00
Book Pauk
ebd96c4759 Merge tag '0.5.3' into develop
0.5.3
2019-03-01 21:30:38 +07:00
Book Pauk
dd9876fc43 Merge branch 'release/0.5.3' 2019-03-01 21:30:28 +07:00
Book Pauk
e0de614f30 Версия 0.5.3 2019-03-01 21:30:03 +07:00
Book Pauk
30260883fb Поправки текста приветствия 2019-03-01 21:29:11 +07:00
Book Pauk
91c331e5f3 Добавлен конвертер для Mobi 2019-03-01 21:23:33 +07:00
Book Pauk
db803bcd23 Отказ от пакета decompress 2019-03-01 20:47:55 +07:00
Book Pauk
cd482ea890 Небольшая поправка 2019-03-01 20:47:39 +07:00
Book Pauk
a2497c939a Добавлен пакет tar-fs 2019-03-01 20:12:19 +07:00
Book Pauk
2e5249d30b Доработки, переименования 2019-03-01 20:11:37 +07:00
Book Pauk
b1d60c19d5 Добавлен метод unTar 2019-03-01 19:48:50 +07:00
Book Pauk
d28a82b33a УБрал дебаг 2019-03-01 19:39:55 +07:00
Book Pauk
787821f64b Поправки багов 2019-03-01 19:28:25 +07:00
Book Pauk
612b15fecc Добавлен метод unBz2 2019-03-01 18:23:46 +07:00
Book Pauk
d88d5a1352 Начата переделка FileDecompressor - отказ от использования багнутого пакета decompress 2019-03-01 18:00:09 +07:00
Book Pauk
8584ddd00e Убрал сигнатуру Epub 2019-03-01 17:58:23 +07:00
Book Pauk
4f572b5a10 Добавлен конвертер для Epub 2019-03-01 17:55:42 +07:00
Book Pauk
90a0882c59 Рефакторинг 2019-03-01 15:10:01 +07:00
Book Pauk
759344bb34 Улучшение конвертера html 2019-03-01 00:24:19 +07:00
Book Pauk
c9b65a3c43 Улучшение конвертирования Pdf 2019-02-28 23:02:34 +07:00
Book Pauk
b06e600946 Поправка багов 2019-02-28 22:04:42 +07:00
Book Pauk
2777751e54 Улучшение конвертирования Pdf 2019-02-28 20:29:09 +07:00
Book Pauk
b4493b2e8d Работа над конвертером pdf 2019-02-28 20:02:14 +07:00
Book Pauk
55d5f6524d Работа над конвертером Pdf 2019-02-28 18:58:41 +07:00
Book Pauk
c7d376adf2 Некоторые доработки 2019-02-28 18:31:08 +07:00
Book Pauk
56bf69a770 Урезал очередь конвертирования до 10 процессов 2019-02-28 15:52:13 +07:00
Book Pauk
22f9287d8b Небольшие доработки 2019-02-28 15:38:03 +07:00
Book Pauk
ca47d9272c Поправка бага 2019-02-28 15:20:20 +07:00
Book Pauk
0d61f5523a Мелкая поправка 2019-02-28 15:00:06 +07:00
Book Pauk
863ea9089a Небольшие оптимизации работы с кешем и чистки 2019-02-28 14:58:48 +07:00
Book Pauk
ad2af95ebd Поправки текста 2019-02-28 01:01:55 +07:00
Book Pauk
d65092c203 Merge tag '0.5.2' into develop
0.5.2
2019-02-28 00:50:52 +07:00
Book Pauk
7982698880 Merge branch 'release/0.5.2' 2019-02-28 00:50:41 +07:00
Book Pauk
afbdff8a88 К предыдущему 2019-02-28 00:50:03 +07:00
Book Pauk
cec07208ac Версия 0.5.2 - поправки для продакшена 2019-02-28 00:48:49 +07:00
Book Pauk
c1b82d0fd2 Merge tag '0.5.1' into develop
0.5.1
2019-02-28 00:06:59 +07:00
Book Pauk
c69e9d4b69 Merge branch 'hotfix/0.5.1' 2019-02-28 00:06:47 +07:00
Book Pauk
cc66c7f5ce Версия 0.5.1, поправки для работы на продакшене 2019-02-28 00:06:16 +07:00
Book Pauk
7d3a689577 Merge tag '0.5.0' into develop
0.5.0
2019-02-27 23:32:32 +07:00
Book Pauk
7b5915cdf7 Merge branch 'release/0.5.0' 2019-02-27 23:32:20 +07:00
Book Pauk
5cc99366ef Версия 0.5.0 2019-02-27 23:31:51 +07:00
Book Pauk
73289543e7 Поправки приветствия 2019-02-27 23:30:22 +07:00
Book Pauk
716b8b5b9a Добавил распаковку вложенных архивов 2019-02-27 23:19:18 +07:00
Book Pauk
c4f6c9383c Меняем текст приветствия в зависимости от useExternalBookConverter 2019-02-27 22:09:00 +07:00
Book Pauk
7d77d478c1 Добавил конвертер файлов rtf 2019-02-27 21:59:45 +07:00
Book Pauk
572c0d0717 Увеличил время ожидания до 2х минут 2019-02-27 21:50:47 +07:00
Book Pauk
b786b7b2d6 Поправки spawnProcess 2019-02-27 21:31:47 +07:00
Book Pauk
3253858c7f Добавил конвертер для msdoc 2019-02-27 21:14:47 +07:00
Book Pauk
a5fe61078d Сделал конвертер для DocX 2019-02-27 20:39:15 +07:00
Book Pauk
528adae3d0 Подготовка к работе с внешними конвертерами 2019-02-27 20:00:55 +07:00
Book Pauk
d3ff0edbff Рефакторинг, плюс небольшие изменения - подготовка к использованию внешних конвертеров 2019-02-27 19:30:04 +07:00
Book Pauk
61cfee222f Переименования 2019-02-27 19:10:33 +07:00
Book Pauk
a96eb50784 Подготовка к запуску внешних конвертеров, для обработки файлов pdf, doc, epub, mobi и пр. 2019-02-27 17:48:37 +07:00
Book Pauk
8219e19c1b Улучшение FileDetector 2019-02-27 17:48:06 +07:00
Book Pauk
3d56a6915f Улучшил отображение ошибки 2019-02-27 17:47:06 +07:00
Book Pauk
4d6502f5e2 Удалил пакет detect-file-type 2019-02-27 17:46:39 +07:00
Book Pauk
f0f245884f Добавил инструкции по установке внешних конвертеров 2019-02-27 15:59:19 +07:00
Book Pauk
815f9178bf Merge tag '0.4.7' into develop
0.4.7
2019-02-27 06:22:09 +07:00
Book Pauk
2df88f4280 Merge branch 'release/0.4.7' 2019-02-27 06:21:57 +07:00
Book Pauk
156a1b4aa8 Версия 0.4.7 2019-02-27 06:21:41 +07:00
Book Pauk
962eda7860 Поправки багов 2019-02-27 06:12:59 +07:00
Book Pauk
954ce9e85c Поправки 2019-02-27 06:00:30 +07:00
Book Pauk
ff27ddd442 Оптимизация - улучшение скорости загрузки читалки 2019-02-27 05:52:59 +07:00
Book Pauk
f9cc2ad70a Мелкая поправка 2019-02-27 04:13:56 +07:00
Book Pauk
4bcd45a795 Рефакторинг - вынес методы конвертирования в отдельные классы 2019-02-27 03:59:08 +07:00
Book Pauk
9e7ccd6e20 Мелкий рефакторинг 2019-02-26 18:23:17 +07:00
Book Pauk
631c5930e9 Мелкая поправка 2019-02-25 20:49:12 +07:00
Book Pauk
0ddb182642 Улучшение парсинга fb2 2019-02-25 17:14:44 +07:00
Book Pauk
de2f0e74c8 Небольшая поправка 2019-02-25 15:29:58 +07:00
Book Pauk
ad4ee6ccc9 Merge tag '0.4.6' into develop
0.4.6
2019-02-24 22:02:53 +07:00
Book Pauk
1670df02db Merge branch 'release/0.4.6' 2019-02-24 22:02:43 +07:00
Book Pauk
0268e647cd Версия 0.4.6 2019-02-24 22:02:27 +07:00
Book Pauk
4b9315c13c Поправка бага 2019-02-24 22:00:41 +07:00
Book Pauk
14a9948dd2 Merge tag '0.4.5' into develop
0.4.5
2019-02-24 21:37:21 +07:00
Book Pauk
060ec98b0c Merge branch 'release/0.4.5' 2019-02-24 21:37:11 +07:00
Book Pauk
9c1efe381e Версия 0.4.5 2019-02-24 21:36:49 +07:00
Book Pauk
6a1b052d16 Поправил баг 2019-02-24 21:36:06 +07:00
Book Pauk
d6151d541e Поправил баг 2019-02-24 21:27:18 +07:00
Book Pauk
3e90277e1e Добавил опцию imageFitWidth 2019-02-24 19:32:16 +07:00
Book Pauk
641cbdfe85 Merge tag '0.4.4' into develop
0.4.4
2019-02-24 00:41:59 +07:00
Book Pauk
c36e9b36d8 Merge branch 'release/0.4.4' 2019-02-24 00:41:48 +07:00
Book Pauk
ecc3acce93 Версия 0.4.4 2019-02-24 00:41:35 +07:00
Book Pauk
33a2ca55f0 Поправки 2019-02-24 00:32:19 +07:00
Book Pauk
9d0bbec4b3 Поправка отображения картинок внутри title 2019-02-24 00:00:55 +07:00
Book Pauk
4de0b3cffd Поправки парсера fb2 2019-02-23 23:51:06 +07:00
Book Pauk
06221a474b Мелкая поправка 2019-02-23 23:38:19 +07:00
Book Pauk
e99a42b7af К предыдущему 2019-02-23 23:20:03 +07:00
Book Pauk
37822e8409 Мелкая поправка 2019-02-23 23:18:03 +07:00
Book Pauk
2e477e6c99 Улучшение парсинга СИ 2019-02-23 23:03:01 +07:00
Book Pauk
360ee98d8d Улучшение парсинга fb2 2019-02-23 22:17:16 +07:00
Book Pauk
69afd7720a Поправки форматирования fb2 2019-02-23 20:45:21 +07:00
Book Pauk
a75590c493 Улучшение формирования fb2 2019-02-23 20:15:40 +07:00
Book Pauk
2acb65f6b3 Мелкие поправки 2019-02-23 20:04:41 +07:00
Book Pauk
1e1a58b58c Улучшен convertHtml 2019-02-23 19:47:16 +07:00
Book Pauk
aeadb5aeb8 Улучшено определение кодировки и текстового файла 2019-02-23 19:09:57 +07:00
Book Pauk
3e2f01d56d Мелкие поправки 2019-02-23 18:06:29 +07:00
Book Pauk
cad97e639a Добавил цвет фона 2019-02-23 14:37:40 +07:00
Book Pauk
e2632f1802 Добавил вывод версии в консоль 2019-02-23 14:24:29 +07:00
Book Pauk
9aa0bb2bde Merge tag '0.4.3' into develop
0.4.3
2019-02-23 14:17:13 +07:00
Book Pauk
f015d5f7ed Merge branch 'release/0.4.3' 2019-02-23 14:17:00 +07:00
Book Pauk
74f8f7f9a4 0.4.3 2019-02-23 14:16:43 +07:00
Book Pauk
2598538de9 Улучшил отзывчивость historyPage при открытии 2019-02-23 14:13:41 +07:00
Book Pauk
a78a00df2b Поправка label 2019-02-23 14:03:28 +07:00
Book Pauk
92f6beb64e Merge tag '0.4.2' into develop
0.4.2
2019-02-21 21:39:19 +07:00
Book Pauk
3920b71613 Merge branch 'release/0.4.2' 2019-02-21 21:39:09 +07:00
Book Pauk
d661150665 0.4.2 2019-02-21 21:38:46 +07:00
Book Pauk
ab29c80dab Поправки багов 2019-02-21 21:36:17 +07:00
Book Pauk
e5384e27e5 Поправки багов 2019-02-21 21:02:27 +07:00
Book Pauk
06cdc6eb63 Улучшение парсинга samlib 2019-02-21 20:26:56 +07:00
Book Pauk
da284c793e Добавил загрузку внешних изображений 2019-02-21 20:22:25 +07:00
Book Pauk
c2cef91eb3 Merge tag '0.4.1' into develop
0.4.1
2019-02-20 21:40:10 +07:00
Book Pauk
19da1aff45 Merge branch 'hotfix/0.4.1' 2019-02-20 21:39:59 +07:00
Book Pauk
5f2206e766 Добавил опцию "Инлайн в центр" 2019-02-20 21:39:17 +07:00
Book Pauk
e272308823 Merge tag '0.4.0' into develop
0.4.0
2019-02-20 21:08:53 +07:00
Book Pauk
8491c40890 Merge branch 'release/0.4.0' 2019-02-20 21:08:42 +07:00
Book Pauk
dfa7013cbd Версия 0.4.0 2019-02-20 21:08:23 +07:00
Book Pauk
1a7ceb333d Добавлена обработка inline-изображений 2019-02-20 20:57:34 +07:00
Book Pauk
d3a30b87f4 Улучшение парсинга fb2 2019-02-20 19:43:39 +07:00
Book Pauk
dd61c04d63 Поправки багов 2019-02-20 19:25:52 +07:00
Book Pauk
5496e874c4 Улучшение парсинга fb2 2019-02-20 18:19:23 +07:00
Book Pauk
56d13288ff Поправка бага, комментариев 2019-02-20 18:04:05 +07:00
Book Pauk
618111ab05 Поправка бага 2019-02-20 17:32:26 +07:00
Book Pauk
55d02495a3 Добавил настройки для изображений, добаил автоматический ресайз 2019-02-20 17:17:51 +07:00
Book Pauk
83fc586e03 Работа над изображениями 2019-02-20 16:49:03 +07:00
Book Pauk
ae3dc9b22c Промежуточный коммит, работа над изображениями, небольшой рефакторинг попутно 2019-02-20 15:26:54 +07:00
Book Pauk
12e0f9459b Merge tag '0.3.5' into develop
0.3.5
2019-02-19 11:31:05 +07:00
Book Pauk
fd1dd54b99 Merge branch 'release/0.3.5' 2019-02-19 11:30:55 +07:00
Book Pauk
32cbb2a82c 0.3.5 2019-02-19 11:30:24 +07:00
Book Pauk
048b7c08ca Merge tag '0.3.4' into develop
0.3.4
2019-02-19 11:25:36 +07:00
Book Pauk
d98251e34a Merge branch 'release/0.3.4' 2019-02-19 11:25:26 +07:00
Book Pauk
1eea4e8fc1 0.3.4 2019-02-19 11:25:10 +07:00
Book Pauk
da330bc615 Вместо omnireader.ru:11080 будет old.omnireader.ru 2019-02-19 11:23:37 +07:00
Book Pauk
1134250954 Merge tag '0.3.3' into develop
0.3.3
2019-02-19 02:37:43 +07:00
Book Pauk
d8fddd4128 Merge branch 'release/0.3.3' 2019-02-19 02:37:29 +07:00
Book Pauk
42656cd690 0.3.3 2019-02-19 02:37:06 +07:00
Book Pauk
746f9517d9 Поправил определение кодировки 2019-02-19 02:31:03 +07:00
Book Pauk
2a373de5f5 Мелкая поправка 2019-02-19 02:16:45 +07:00
Book Pauk
b507f00929 Merge tag '0.3.2' into develop
0.3.2
2019-02-19 01:41:03 +07:00
Book Pauk
aea8a254bf Merge branch 'hotfix/0.3.2' 2019-02-19 01:40:50 +07:00
Book Pauk
4247665ba4 Добавил поддержку форматов .gz .bz2 2019-02-19 01:39:52 +07:00
Book Pauk
d59d0a6a42 Промежуточный коммит, парсинг изображений 2019-02-18 23:55:32 +07:00
Book Pauk
8d40ed0bda Добавлено распарсивание тега binary 2019-02-18 21:28:48 +07:00
Book Pauk
67bc893e22 Добавил горячую клавишу 2019-02-18 17:23:12 +07:00
Book Pauk
0f5d3b34a5 Merge tag '0.3.1' into develop
0.3.1
2019-02-18 16:10:56 +07:00
Book Pauk
39b577e935 Merge branch 'release/0.3.1' 2019-02-18 16:10:43 +07:00
Book Pauk
91380d2aed Версия 0.3.1 2019-02-18 16:10:27 +07:00
Book Pauk
f995c24264 Добавлена старая читалка для страждущих 2019-02-18 16:08:39 +07:00
Book Pauk
c7db0ec643 Рефакторинг 2019-02-18 15:00:20 +07:00
Book Pauk
f9e000034f Merge tag '0.3.0' into develop
0.3.0
2019-02-17 20:16:23 +07:00
Book Pauk
8ddd2d6290 Merge branch 'release/0.3.0' 2019-02-17 20:16:11 +07:00
Book Pauk
f6c8666f06 Версия 0.3.0 2019-02-17 20:15:50 +07:00
Book Pauk
1bf173fcae Поправка бага 2019-02-17 20:10:25 +07:00
Book Pauk
696866f065 Merge branch 'feature/draw-page' into develop 2019-02-17 20:08:06 +07:00
Book Pauk
c82283c7f0 Поправка дефолтов 2019-02-17 20:04:28 +07:00
Book Pauk
0e62f25557 Переделки отображения needle при поиске 2019-02-17 20:02:18 +07:00
Book Pauk
e2c51f44bf Изменение drawPage - теперь не позиционирует каждое слово отдельно,
вместо этого используются возможности CSS
2019-02-17 19:35:32 +07:00
Book Pauk
d4768392a6 Мелкая поправка 2019-02-17 19:32:30 +07:00
Book Pauk
580744819d Поправил настройки по-умолчанию 2019-02-17 19:31:50 +07:00
Book Pauk
959794de10 Merge tag '0.2.2' into develop
0.2.2
2019-02-17 14:59:34 +07:00
Book Pauk
3a32c09feb Merge branch 'release/0.2.2' 2019-02-17 14:59:14 +07:00
Book Pauk
09bfce4590 Версия 0.2.2 2019-02-17 14:58:37 +07:00
Book Pauk
092697a4b2 Поправил бажок 2019-02-17 14:55:01 +07:00
Book Pauk
a36de9424e Улучшил распарсивание текста 2019-02-17 14:46:52 +07:00
Book Pauk
db4bc2afb2 Улучшена эвристика определения текста 2019-02-17 14:27:49 +07:00
Book Pauk
73bfc07082 Увеличил количество хранимых недавних до 1000 2019-02-17 13:57:12 +07:00
Book Pauk
b3cf88aac3 Поправил бажок 2019-02-17 13:33:49 +07:00
Book Pauk
69bcc61a01 Включаем yandex-метрику только на production 2019-02-17 13:30:08 +07:00
Book Pauk
bdc124052c Merge tag '0.2.1' into develop
0.2.1
2019-02-16 03:38:38 +07:00
Book Pauk
cfcee9eb99 Merge branch 'hotfix/0.2.1' 2019-02-16 03:38:24 +07:00
Book Pauk
e93c873c17 Поправил названия в списке 2019-02-16 03:37:51 +07:00
Book Pauk
2b1bb6a299 Merge tag '0.2.0' into develop
0.2.0
2019-02-16 03:20:51 +07:00
Book Pauk
3d7c039fd4 Merge branch 'release/0.2.0' 2019-02-16 03:20:41 +07:00
Book Pauk
9971f1d4bf 0.2.0 2019-02-16 03:20:22 +07:00
Book Pauk
59702d3d50 Добавлена анимация перелистывания - слева-направо, снизу-вверх 2019-02-16 02:59:13 +07:00
Book Pauk
736e6ec075 Рефакторинг 2019-02-16 00:58:28 +07:00
Book Pauk
abdd6d4142 Рефакторинг 2019-02-15 23:22:36 +07:00
Book Pauk
16edc85e39 Добавлен эффект перелистывания - Мерцание 2019-02-15 22:57:29 +07:00
Book Pauk
bbeb92a9da Мелкие доработки 2019-02-15 21:41:45 +07:00
Book Pauk
daaf9ac70b Добавлен эффект листания - Протаивание 2019-02-15 21:36:06 +07:00
Book Pauk
0805353a9e Начало работы над анимацией перелистывания 2019-02-15 19:20:50 +07:00
Book Pauk
594ff954b1 Рефакторинг 2019-02-15 19:03:05 +07:00
Book Pauk
cf5203687e Добавил зеркало 'zhurnal.lib.ru' для парсинга samlib 2019-02-15 18:22:54 +07:00
Book Pauk
55ad2d664d Улчшение парсинга samlib 2019-02-15 18:21:37 +07:00
Book Pauk
4d1485a61f Мелкая поправка 2019-02-15 18:20:29 +07:00
Book Pauk
f260378c93 Мелкая поправка 2019-02-15 03:00:16 +07:00
Book Pauk
08f9175705 Мелкие поправки 2019-02-15 02:56:58 +07:00
Book Pauk
ca7cc322d7 Добавил настройку blinkCachedLoad 2019-02-15 02:46:27 +07:00
Book Pauk
a631befdf9 Добавил инфо для поисковиков 2019-02-14 23:40:51 +07:00
Book Pauk
b7875644bc Merge tag '0.1.7' into develop
0.1.7
2019-02-14 21:52:57 +07:00
Book Pauk
9d8507a1e4 Merge branch 'hotfix/0.1.7' 2019-02-14 21:52:41 +07:00
Book Pauk
50042aa36d Убрал редирект на последнюю открытую книгу, если не задан явно url 2019-02-14 21:51:22 +07:00
Book Pauk
d21cf6286a Merge tag '0.1.6' into develop
0.1.6
2019-02-14 19:00:15 +07:00
Book Pauk
27e2f26d5d Merge tag '0.1.5' into develop
0.1.5
2019-02-14 18:27:46 +07:00
255 changed files with 26252 additions and 6355 deletions

106
LICENSE.md Normal file
View File

@@ -0,0 +1,106 @@
# CC0 1.0 Universal
## Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator and
subsequent owner(s) (each and all, an “owner”) of an original work of
authorship and/or a database (each, a “Work”).
Certain owners wish to permanently relinquish those rights to a Work for the
purpose of contributing to a commons of creative, cultural and scientific works
(“Commons”) that the public can reliably and without fear of later claims of
infringement build upon, modify, incorporate in other works, reuse and
redistribute as freely as possible in any form whatsoever and for any purposes,
including without limitation commercial purposes. These owners may contribute
to the Commons to promote the ideal of a free culture and the further
production of creative, cultural and scientific works, or to gain reputation or
greater distribution for their Work in part through the use and efforts of
others.
For these and/or other purposes and motivations, and without any expectation of
additional consideration or compensation, the person associating CC0 with a
Work (the “Affirmer”), to the extent that he or she is an owner of Copyright
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
publicly distribute the Work under its terms, with knowledge of his or her
Copyright and Related Rights in the Work and the meaning and intended legal
effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights (“Copyright and
Related Rights”). Copyright and Related Rights include, but are not limited
to, the following:
1. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
2. moral rights retained by the original author(s) and/or performer(s);
3. publicity and privacy rights pertaining to a persons image or likeness
depicted in a Work;
4. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(i), below;
5. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
6. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
7. other similar, equivalent or corresponding rights throughout the world
based on applicable law or treaty, and any national implementations
thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
unconditionally waives, abandons, and surrenders all of Affirmers Copyright
and Related Rights and associated claims and causes of action, whether now
known or unknown (including existing as well as future claims and causes of
action), in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number of
copies, and (iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes
the Waiver for the benefit of each member of the public at large and to the
detriment of Affirmers heirs and successors, fully intending that such Waiver
shall not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of the Work
by the public as contemplated by Affirmers express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason be
judged legally invalid or ineffective under applicable law, then the Waiver
shall be preserved to the maximum extent permitted taking into account
Affirmers express Statement of Purpose. In addition, to the extent the Waiver
is so judged Affirmer hereby grants to each affected person a royalty-free, non
transferable, non sublicensable, non exclusive, irrevocable and unconditional
license to exercise Affirmers Copyright and Related Rights in the Work (i) in
all territories worldwide, (ii) for the maximum duration provided by applicable
law or treaty (including future time extensions), (iii) in any current or
future medium and for any number of copies, and (iv) for any purpose
whatsoever, including without limitation commercial, advertising or promotional
purposes (the “License”). The License shall be deemed effective as of the date
CC0 was applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder of the
License, and in such case Affirmer hereby affirms that he or she will not (i)
exercise any of his or her remaining Copyright and Related Rights in the Work
or (ii) assert any associated claims and causes of action with respect to the
Work, in either case contrary to Affirmers express Statement of Purpose.
4. Limitations and Disclaimers.
1. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
2. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied, statutory
or otherwise, including without limitation warranties of title,
merchantability, fitness for a particular purpose, non infringement, or
the absence of latent or other defects, accuracy, or the present or
absence of errors, whether or not discoverable, all to the greatest
extent permissible under applicable law.
3. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any persons Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the Work.
4. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
For more information, please see
http://creativecommons.org/publicdomain/zero/1.0/.

View File

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

31
build/includer.js Normal file
View File

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

View File

@@ -5,8 +5,7 @@ const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);
const got = require('got');
const decompress = require('decompress');
const decompressTargz = require('decompress-targz');
const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist');
const publicDir = `${distDir}/tmp/public`;
@@ -15,6 +14,8 @@ const outDir = `${distDir}/linux`;
const tempDownloadDir = `${distDir}/tmp/download`;
async function main() {
const decomp = new FileDecompressor();
await fs.emptyDir(outDir);
// перемещаем public на место
if (await fs.pathExists(publicDir))
@@ -23,8 +24,8 @@ async function main() {
await fs.ensureDir(tempDownloadDir);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-linux-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-linux-x64/node_sqlite3.node`;
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`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
@@ -32,11 +33,7 @@ async function main() {
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
plugins: [
decompressTargz()
]
});
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
@@ -53,11 +50,7 @@ async function main() {
console.log(`done downloading ${ipfsRemoteUrl}`);
//распаковываем
await decompress(`${tempDownloadDir}/ipfs.tar.gz`, `${tempDownloadDir}`, {
plugins: [
decompressTargz()
]
});
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/ipfs.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}

View File

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

View File

@@ -9,6 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const {GenerateSW} = require('workbox-webpack-plugin');
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
const clientDir = path.resolve(__dirname, '../client');
@@ -32,7 +33,15 @@ module.exports = merge(baseWpConfig, {
},
optimization: {
minimizer: [
new TerserPlugin(),
new TerserPlugin({
cache: true,
parallel: true,
terserOptions: {
output: {
comments: false,
},
},
}),
new OptimizeCSSAssetsPlugin()
]
},
@@ -45,6 +54,13 @@ module.exports = merge(baseWpConfig, {
template: `${clientDir}/index.html.template`,
filename: `${publicDir}/index.html`
}),
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
new GenerateSW({
cacheId: 'liberama',
swDest: `${publicDir}/service-worker.js`,
navigateFallback: '/index.html',
navigateFallbackDenylist: [new RegExp('^/api'), new RegExp('^/ws'), new RegExp('^/tmp'),],
skipWaiting: true,
}),
]
});

View File

@@ -5,8 +5,7 @@ const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);
const got = require('got');
const decompress = require('decompress');
const decompressTargz = require('decompress-targz');
const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist');
const publicDir = `${distDir}/tmp/public`;
@@ -15,6 +14,8 @@ const outDir = `${distDir}/win`;
const tempDownloadDir = `${distDir}/tmp/download`;
async function main() {
const decomp = new FileDecompressor();
await fs.emptyDir(outDir);
// перемещаем public на место
if (await fs.pathExists(publicDir))
@@ -23,8 +24,8 @@ async function main() {
await fs.ensureDir(tempDownloadDir);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.0.4/node-v64-win32-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v64-win32-x64/node_sqlite3.node`;
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`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
@@ -32,11 +33,7 @@ async function main() {
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
plugins: [
decompressTargz()
]
});
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
@@ -53,7 +50,7 @@ async function main() {
console.log(`done downloading ${ipfsRemoteUrl}`);
//распаковываем
await decompress(`${tempDownloadDir}/ipfs.zip`, `${tempDownloadDir}`);
console.log(await decomp.unpack(`${tempDownloadDir}/ipfs.zip`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,2 @@
User-agent: *
Disallow: /?*url=
Disallow: /#/

View File

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

View File

@@ -1,60 +1,29 @@
<template>
<el-container>
<el-aside v-if="showAsideBar" :width="asideWidth">
<div class="app-name"><span v-html="appName"></span></div>
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
<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>
</el-aside>
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
<keep-alive>
<router-view></router-view>
</keep-alive>
</el-main>
</el-container>
<div class="fit row">
<Notify ref="notify"/>
<StdDialog ref="stdDialog"/>
<keep-alive>
<router-view class="col"></router-view>
</keep-alive>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import Notify from './share/Notify.vue';
import StdDialog from './share/StdDialog.vue';
import * as utils from '../share/utils';
export default @Component({
components: {
Notify,
StdDialog,
},
watch: {
rootRoute: function() {
this.setAppTitle();
this.redirectIfNeeded();
},
mode: function() {
this.setAppTitle();
this.redirectIfNeeded();
}
},
@@ -69,7 +38,8 @@ class App extends Vue {
'/sources': 'Источники',
'/settings': 'Параметры',
'/help': 'Справка',
}
};
created() {
this.commit = this.$store.commit;
this.dispatch = this.$store.dispatch;
@@ -77,6 +47,28 @@ class App extends Vue {
this.uistate = this.$store.state.uistate;
this.config = this.$store.state.config;
//root route
let cachedRoute = '';
let cachedPath = '';
this.$root.rootRoute = () => {
if (this.$route.path != cachedPath) {
cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i);
cachedRoute = (m ? m[1] : this.$route.path);
}
return cachedRoute;
}
this.$router.beforeEach((to, from, next) => {
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
if (window.location.host.indexOf('b.') == 0 && to.path != '/external-libs' && to.path != '/404') {
next('/404');
} else {
next();
}
});
// set-app-title
this.$root.$on('set-app-title', this.setAppTitle);
@@ -100,26 +92,49 @@ class App extends Vue {
document.addEventListener('keyup', (event) => {
this.keyHook(event);
});
});
document.addEventListener('keypress', (event) => {
this.keyHook(event);
});
document.addEventListener('keydown', (event) => {
this.keyHook(event);
});
});
window.addEventListener('resize', () => {
this.$root.$emit('resize');
});
}
routerReady() {
return new Promise ((resolve) => {
this.$router.onReady(() => {
resolve();
});
});
}
mounted() {
this.$root.notify = this.$refs.notify;
this.$root.stdDialog = this.$refs.stdDialog;
this.dispatch('config/loadConfig');
this.$watch('apiError', function(newError) {
if (newError) {
this.$notify.error({
title: 'Ошибка API',
dangerouslyUseHTMLString: true,
message: newError.response.config.url + '<br>' + newError.response.statusText
});
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();
(async() => {
//запросим persistent storage
if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist();
}
await this.routerReady();
this.redirectIfNeeded();
})();
}
toggleCollapse() {
@@ -133,9 +148,9 @@ class App extends Vue {
get asideWidth() {
if (this.uistate.asideBarCollapse) {
return '64px';
return 64;
} else {
return '170px';
return 170;
}
}
@@ -159,15 +174,14 @@ class App extends Vue {
}
get rootRoute() {
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
this.$root.rootRoute = (m ? m[1] : this.$route.path);
return this.$root.rootRoute;
return this.$root.rootRoute();
}
setAppTitle(title) {
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]}`;
@@ -186,44 +200,33 @@ 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';
}
get showMain() {
return (this.showAsideBar || this.isReaderActive);
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
}
redirectIfNeeded() {
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
//старый url
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) {
const search = window.location.search.substr(1);
const url = search.split('url=')[1] || '';
if (url) {
window.location = `/#/reader?url=${url}`;
} else {
this.$router.replace('/reader');
//распознавание параметра 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 });
}
}
//yandex-метрика для omnireader
if (this.mode == 'omnireader' && !this.yaMetricsDone) {
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");// eslint-disable-line no-unexpected-multiline
ym(52347334, "init", {// eslint-disable-line no-undef
id:52347334,
clickmap:true,
trackLinks:true,
accurateTrackBounce:true
});
this.yaMetricsDone = true;
}
}
}
//-----------------------------------------------------------------------------
@@ -237,68 +240,28 @@ class App extends Vue {
line-height: 140%;
font-weight: bold;
}
.bold-font {
font-weight: bold;
}
.el-container {
height: 100%;
}
.el-aside {
line-height: 1;
background-color: #ccc;
color: #000;
}
.el-main {
padding: 0;
background-color: #E6EDF4;
color: #000;
}
.el-menu-vertical:not(.el-menu--collapse) {
background-color: inherit;
color: inherit;
text-align: left;
width: 100%;
border: 0;
}
.el-menu--collapse {
background-color: inherit;
color: inherit;
border: 0;
}
.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
background-color: inherit;
color: inherit;
margin-top: 5px;
width: 100%;
height: 64px;
border: 0;
}
.el-menu-item {
font-size: 85%;
}
</style>
<style>
body, html, #app {
body, html, #app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font: normal 12pt ReaderDefault;
}
.el-tabs__content {
flex: 1;
padding: 0 !important;
display: flex;
flex-direction: column;
overflow: hidden;
.dborder {
border: 2px solid magenta !important;
}
.icon-rotate {
vertical-align: middle;
animation: rotating 2s linear infinite;
}
.notify-button-icon {
font-size: 16px !important;
}
@font-face {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,247 @@
<template>
<Window width="600px" ref="window" @close="close">
<template slot="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="bookmarks" icon="la la-bookmark" label="Закладки" />
</q-tabs>
</div>
<div class="q-mb-sm"/>
<div class="tab-panel" v-show="selectedTab == 'contents'">
<div>
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<div class="row item q-px-sm no-wrap">
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
</div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
</div>
<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="`subitem${item.key}`" class="subitems-transition">
<div v-for="subitem in item.list" :key="subitem.key" class="row subitem q-px-sm no-wrap">
<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 class="tab-panel" v-show="selectedTab == 'bookmarks'">
<div class="column justify-center items-center" style="height: 100px">
Раздел находится в разработке
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
//import _ from 'lodash';
import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils';
export default @Component({
components: {
Window,
},
watch: {
},
})
class ContentsPage extends Vue {
selectedTab = 'contents';
contents = [];
created() {
}
async init(currentBook, parsed) {
this.$refs.window.init();
//закладки
//далее формаирование оглавления
if (this.parsed == parsed)
return;
this.parsed = parsed;
this.contents = [];
await this.$nextTick();
const pc = parsed.contents;
const newpc = [];
//преобразуем все, кроме первого, разделы 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);
}
}
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;
}
async expandClick(key) {
const item = this.contents[key];
const expanded = !item.expanded;
if (!expanded) {
const subitems = this.$refs[`subitem${key}`][0];
subitems.style.height = '0';
await utils.sleep(200);
}
this.$set(this.contents, key, Object.assign({}, item, {expanded}));
if (expanded) {
await this.$nextTick();
const subitems = this.$refs[`subitem${key}`][0];
subitems.style.height = subitems.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;
}
}
//-----------------------------------------------------------------------------
</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 {
border-bottom: 1px solid #e0e0e0;
}
.item:hover, .subitem:hover {
background-color: #f0f0f0;
}
.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);
}
</style>

View File

@@ -1,17 +1,13 @@
<template>
<div ref="main" class="main" @click="close">
<div class="mainWindow" @click.stop>
<Window @close="close">
<template slot="header">
Скопировать текст
</template>
<Window @close="close">
<template slot="header">
Скопировать текст
</template>
<div ref="text" class="text" tabindex="-1">
<div v-html="text"></div>
</div>
</Window>
<div ref="text" class="text" tabindex="-1">
<div v-html="text"></div>
</div>
</div>
</Window>
</template>
<script>
@@ -95,11 +91,11 @@ class CopyTextPage extends Vue {
close() {
this.stopInit = true;
this.$emit('copy-text-toggle');
this.$emit('do-action', {action: 'copyText'});
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
@@ -109,23 +105,6 @@ class CopyTextPage extends Vue {
</script>
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
height: 100%;
display: flex;
}
.text {
flex: 1;
overflow-wrap: anywhere;

View File

@@ -1,27 +1,40 @@
<template>
<div class="page">
<h4>Возможности читалки:</h4>
<span class="text-h6 text-bold">Возможности читалки:</span>
<ul>
<li>загрузка любой страницы интернета</li>
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
<li>работа в автономном режиме (без связи)</li>
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
<li>установка и запоминание текущей позиции и настроек в браузере (в будущем планируется сохранение и на сервер)</li>
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
<li>кэширование файлов книг на клиенте и на сервере</li>
<li>открытие книг с локального диска</li>
<li>плавный скроллинг текста</li>
<li>анимация перелистывания (скоро)</li>
<li>анимация перелистывания</li>
<li>поиск по тексту и копирование фрагмента</li>
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
<li>управление кликом и с клавиатуры</li>
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
<li>регистрация не требуется</li>
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
</ul>
<p>В качестве URL можно задавать html-страничку с книгой, либо прямую ссылку
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
<p>Поддерживаемые форматы: <strong>html, txt, fb2, fb2.zip</strong></p>
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
<div v-html="automationHtml"></div>
<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="bookmarkText">{{ (mode == 'omnireader' ? 'Omni' : 'Liberama') }} Reader</a>
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
</p>
</div>
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
</div>
</template>
@@ -31,21 +44,29 @@
import Vue from 'vue';
import Component from 'vue-class-component';
import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({
})
class CommonHelpPage extends Vue {
created() {
this.config = this.$store.state.config;
}
get automationHtml() {
if (this.config.mode == 'omnireader') {
return `<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`;
} else {
return '';
}
get mode() {
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 : 'Копирование не удалось');
if (result)
this.$root.notify.success(msg);
else
this.$root.notify.error(msg);
}
}
//-----------------------------------------------------------------------------
@@ -53,14 +74,16 @@ class CommonHelpPage extends Vue {
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
color: blue;
}
</style>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,29 +1,24 @@
<template>
<div ref="main" class="main" @click="close">
<div class="mainWindow" @click.stop>
<Window @close="close">
<template slot="header">
Справка
</template>
<Window @close="close">
<template slot="header">
Справка
</template>
<el-tabs type="border-card" v-model="selectedTab">
<el-tab-pane class="tab" label="Общее">
<CommonHelpPage></CommonHelpPage>
</el-tab-pane>
<el-tab-pane label="Клавиатура">
<HotkeysHelpPage></HotkeysHelpPage>
</el-tab-pane>
<el-tab-pane label="Мышь/тачпад">
<MouseHelpPage></MouseHelpPage>
</el-tab-pane>
<el-tab-pane label="Помочь проекту" name="donate">
<DonateHelpPage></DonateHelpPage>
</el-tab-pane>
<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>
</el-tabs>
</Window>
<keep-alive>
<component ref="page" class="col" :is="activePage"
></component>
</keep-alive>
</div>
</div>
</Window>
</template>
<script>
@@ -35,30 +30,58 @@ 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';
const pages = {
'CommonHelpPage': CommonHelpPage,
'HotkeysHelpPage': HotkeysHelpPage,
'MouseHelpPage': MouseHelpPage,
'VersionHistoryPage': VersionHistoryPage,
'DonateHelpPage': DonateHelpPage,
};
const tabs = [
['CommonHelpPage', 'Общее'],
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'],
];
export default @Component({
components: {
Window,
CommonHelpPage,
HotkeysHelpPage,
MouseHelpPage,
DonateHelpPage,
},
components: Object.assign({ Window }, pages),
})
class HelpPage extends Vue {
selectedTab = null;
selectedTab = 'CommonHelpPage';
close() {
this.$emit('help-toggle');
this.$emit('do-action', {action: 'help'});
}
get activePage() {
if (pages[this.selectedTab])
return pages[this.selectedTab];
return null;
}
get buttons() {
let result = [];
for (const tab of tabs)
result.push({label: tab[1], value: tab[0]});
return result;
}
activateDonateHelpPage() {
this.selectedTab = 'donate';
this.selectedTab = 'DonateHelpPage';
}
activateVersionHistoryHelpPage() {
this.selectedTab = 'VersionHistoryPage';
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
@@ -68,33 +91,8 @@ class HelpPage extends Vue {
</script>
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
height: 100%;
display: flex;
}
.el-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.el-tab-pane {
flex: 1;
display: flex;
overflow: hidden;
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style>

View File

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

View File

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

View File

@@ -0,0 +1,78 @@
<template>
<div id="versionHistoryPage" class="page">
<span class="text-h6 text-bold">История версий:</span>
<br><br>
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
<p>
{{ item }}
</p>
</span>
<br>
<div v-for="item in versionContent" :id="item.key" :key="item.key">
<span v-html="item.content"></span>
<br>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import {versionHistory} from '../../versionHistory';
export default @Component({
})
class VersionHistoryPage extends Vue {
versionHeader = [];
versionContent = [];
created() {
}
mounted() {
let vh = [];
for (const version of versionHistory) {
vh.push(version.header);
}
this.versionHeader = vh;
let vc = [];
for (const version of versionHistory) {
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
}
this.versionContent = vc;
}
showRelease(id) {
let el = document.getElementById(id);
if (el) {
document.getElementById('versionHistoryPage').scrollTop = el.offsetTop;
}
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.page {
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
position: relative;
}
p {
line-height: 15px;
}
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@@ -1,261 +0,0 @@
<template>
<div ref="main" class="main" @click="close">
<div class="mainWindow" @click.stop>
<Window @close="close">
<template slot="header">
Последние 100 открытых книг
</template>
<el-table
:data="tableData"
style="width: 100%"
size="mini"
height="1px"
stripe
border
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
:header-cell-style = "headerCellStyle"
>
<el-table-column
prop="touchDateTime"
min-width="90px"
sortable
>
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<span style="font-size: 90%">Время<br>просм.</span>
</template>
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<div class="desc" @click="loadBook(scope.row.url)">
{{ scope.row.touchDate }}<br>
{{ scope.row.touchTime }}
</div>
</template>
</el-table-column>
<el-table-column
>
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<!--el-input ref="input"
:value="search" @input="search = $event"
size="mini"
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
placeholder="Найти"/-->
<div class="el-input el-input--mini">
<input class="el-input__inner"
placeholder="Найти"
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
:value="search" @input="search = $event.target.value"
/>
</div>
</template>
<el-table-column
min-width="300px"
>
<template slot-scope="scope">
<div class="desc" @click="loadBook(scope.row.url)">
<span style="color: green">{{ scope.row.desc.author }}</span><br>
<span>{{ scope.row.desc.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column
min-width="100px"
>
<template slot-scope="scope">
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a>
</template>
</el-table-column>
<el-table-column
width="60px"
>
<template slot-scope="scope">
<el-button
size="mini"
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
</el-button>
</template>
</el-table-column>
</el-table-column>
</el-table>
</Window>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import path from 'path';
import _ from 'lodash';
import {formatDate} from '../../../share/utils';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
export default @Component({
components: {
Window,
},
watch: {
search: function() {
this.updateTableData();
}
},
})
class HistoryPage extends Vue {
search = null;
tableData = null;
created() {
}
mounted() {
this.updateTableData();
this.mostRecentBook = bookManager.mostRecentBook();
}
updateTableData() {
let result = [];
for (let bookKey in bookManager.recent) {
const book = bookManager.recent[bookKey];
let d = new Date();
d.setTime(book.touchTime);
const t = 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 = _.compact([
fb2.lastName,
fb2.firstName,
fb2.middleName
]).join(' ');
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
result.push({
touchDateTime: book.touchTime,
touchDate: t[0],
touchTime: t[1],
desc: {
title: `${title}${perc}${textLen}`,
author,
},
url: book.url,
path: book.path,
key: book.key,
});
}
const search = this.search;
this.tableData = 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())
});
}
headerCellStyle(cell) {
let result = {margin: 0, padding: 0};
if (cell.columnIndex > 0) {
result['border-bottom'] = 0;
}
if (cell.rowIndex > 0) {
result.height = '0px';
result['border-right'] = 0;
}
return result;
}
getFileNameFromPath(fb2Path) {
return path.basename(fb2Path).substr(0, 10) + '.fb2';
}
openOriginal(url) {
window.open(url, '_blank');
}
openFb2(path) {
window.open(path, '_blank');
}
async handleDel(key) {
await bookManager.delRecentBook({key});
this.updateTableData();
const newRecent = bookManager.mostRecentBook();
if (this.mostRecentBook != newRecent)
this.$emit('load-book', newRecent);
this.mostRecentBook = newRecent;
if (!this.mostRecentBook)
this.close();
}
loadBook(url) {
this.$emit('load-book', {url});
this.close();
}
isUrl(url) {
return (url.indexOf('file://') != 0);
}
close() {
this.$emit('history-toggle');
}
keyHook(event) {
if (event.type == 'keydown' && event.code == 'Escape') {
this.close();
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
}
.mainWindow {
height: 100%;
display: flex;
}
.desc {
cursor: pointer;
}
</style>

View File

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

View File

@@ -0,0 +1,134 @@
<template>
<div id="vue-github-corner">
<a :href="url" id="github-corner" target="_blank" aria-label="View source on Github" >
<svg id="github-corner-svg"
aria-hidden="true"
viewBox="0 0 250 250"
:width="size" :height="size"
:style="svgStyle" >
<path :d="svgPath1" @mouseenter="flipColor" @mouseleave="flipColor"></path>
<path :d="svgPath2" :style="gitStyle" class="octo-arm"></path>
<path :d="svgPath3" :style="gitStyle" class="octo-body"></path>
</svg>
</a>
</div>
</template>
<script>
export default {
name: 'GithubCorner',
props: {
url: {
type: String,
default: '/'
},
size: {
type: Number,
default: 80
},
colorScheme: {
type: String,
default: 'auto'
},
cornerColor: {
type: String,
default: '#625D5D'
},
gitColor: {
type: String,
default: 'PeachPuff'
},
leftCorner: {
type: Boolean,
default: false
},
flipOnHover: {
type: Boolean,
default: false
}
},
data() {
return {
svgStyle: {
fill: this.cornerColor,
right: (this.leftCorner ? 'auto' : '0'),
left: (this.leftCorner ? '0' : 'auto'),
transform: (this.leftCorner ? 'scale(-1, 1)' : 'none')
},
gitStyle: {
fill: this.gitColor
},
flipped: false,
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 ' +
'123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 ' +
'C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 ' +
'176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 ' +
'216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 ' +
'C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
}
},
methods: {
flipColor: function() {
if (this.flipOnHover) {
let holdSvgFill = this.svgStyle.fill
this.svgStyle.fill = this.gitStyle.fill
this.gitStyle.fill = holdSvgFill
}
}
},
beforeMount: function() {
if (this.colorScheme != 'auto') {
let sch = this.colorScheme
this.gitStyle.fill = '#fff'
if (sch.toLowerCase() == 'black') {
this.svgStyle.fill = '#151513'
}
if (sch.toLowerCase() == 'green') {
this.svgStyle.fill = '#64CEAA'
}
if (sch.toLowerCase() == 'red') {
this.svgStyle.fill = '#FD6C6C'
}
if (sch.toLowerCase() == 'blue') {
this.svgStyle.fill = '#70B7FD'
}
if (sch.toLowerCase() == 'white') {
this.svgStyle.fill = '#fff'
this.gitStyle.fill = '#151513'
}
}
}
}
</script>
<style scoped>
#github-corner .octo-arm {
transform-origin: 130px 106px
}
#github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0% { transform: rotate(0deg); }
20% { transform: rotate(-25deg); }
40% { transform: rotate(10deg); }
60% { transform: rotate(-25deg); }
80% { transform: rotate(10deg); }
100% { transform: rotate(0deg); }
}
#github-corner-svg {
color: #fff;
position: absolute;
top: 0;
border: 0;
}
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
transition: fill 1s ease;
}
</style>

View File

@@ -1,27 +1,58 @@
<template>
<div ref="main" class="main">
<div class="part">
<span class="greeting bold-font">{{ title }}</span>
<div ref="main" class="column no-wrap" style="min-height: 500px">
<div v-if="mode != 'liberama.top'" class="relative-position">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
</div>
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
<span class="greeting"><b>{{ title }}</b></span>
<div class="q-my-sm"></div>
<span class="greeting">Добро пожаловать!</span>
<span class="greeting">Поддерживаются форматы: fb2, fb2.zip, html, txt</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="part center">
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
</el-input>
<div class="space"></div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
<template v-slot:append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
</template>
</q-input>
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
<el-button size="mini" @click="loadFileClick">
<div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
Загрузить файл с диска
</el-button>
<div class="space"></div>
<span v-if="config.mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Комментарии</span>
</q-btn>
<div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
Из буфера обмена
</q-btn>
<div class="q-my-md"></div>
<!--div v-if="mode == 'omnireader'">
<div ref="yaShare2" class="ya-share2"
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
data-title="Omni Reader - браузерная онлайн-читалка"
data-url="https://omnireader.ru">
</div>
</div>
<div class="q-my-sm"></div-->
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
</div>
<div class="part bottom">
<div class="col column justify-end items-center no-wrap overflow-hidden">
<span class="bottom-span clickable" @click="openHelp">Справка</span>
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
<span class="bottom-span">{{ version }}</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>
</div>
</template>
@@ -29,20 +60,30 @@
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import GithubCorner from './GithubCorner/GithubCorner.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory';
export default @Component({
components: {
GithubCorner,
PasteTextPage,
},
})
class LoaderPage extends Vue {
bookUrl = null;
loadPercent = 0;
pasteTextActive = false;
created() {
this.commit = this.$store.commit;
this.config = this.$store.state.config;
}
mounted() {
this.progress = this.$refs.progress;
/*if (this.mode == 'omnireader')
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef*/
}
activated() {
@@ -50,19 +91,35 @@ class LoaderPage extends Vue {
}
get title() {
if (this.config.mode == 'omnireader')
if (this.mode == 'omnireader')
return 'Omni Reader - браузерная онлайн-читалка.';
if (this.mode == 'liberama.top')
return 'Liberama Reader - браузерная онлайн-читалка.';
return 'Универсальная читалка книг и ресурсов интернета.';
}
get mode() {
return this.$store.state.config.mode;
}
get version() {
return `v${this.config.version}`;
return this.$store.state.config.version;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
get clientVersion() {
let v = versionHistory[0].header;
v = v.split(' ')[0];
return v;
}
submitUrl() {
if (this.bookUrl) {
this.$emit('load-book', {url: this.bookUrl});
this.$emit('load-book', {url: this.bookUrl, force: true});
this.bookUrl = '';
}
}
@@ -72,63 +129,73 @@ class LoaderPage extends Vue {
}
loadFile() {
const file = this.$refs.file.files[0];
const file = this.$refs.file.files[0];
this.$refs.file.value = '';
if (file)
this.$emit('load-file', {file});
}
openHelp() {
this.$emit('help-toggle');
loadBufferClick() {
this.pasteTextToggle();
}
loadBuffer(opts) {
if (opts.buffer.length) {
const file = new File([opts.buffer], 'dummyName-PasteFromClipboard');
this.$emit('load-file', {file});
}
}
pasteTextToggle() {
this.pasteTextActive = !this.pasteTextActive;
}
openHelp(event) {
this.$emit('do-action', {action: 'help', event});
}
openDonate() {
this.$emit('donate-toggle');
this.$emit('do-action', {action: 'donate'});
}
openComments() {
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
}
openOldVersion() {
window.open('http://old.omnireader.ru', '_blank');
}
keyHook(event) {
//недостатки сторонних ui
const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
this.submitUrl();
if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event);
}
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
this.$emit('help-toggle');
event.preventDefault();
event.stopPropagation();
//недостатки сторонних ui
const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
this.submitUrl();
return true;
}
if (event.type == 'keydown' && document.activeElement !== input) {
const action = this.$root.readerActionByKeyEvent(event);
switch (action) {
case 'help':
this.openHelp(event);
return true;
}
}
return false;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.main {
flex: 1;
display: flex;
flex-direction: column;
}
.part {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.greeting {
font-size: 130%;
line-height: 170%;
}
.bold-font {
font-weight: bold;
font-size: 120%;
line-height: 160%;
}
.clickable {
@@ -137,25 +204,8 @@ class LoaderPage extends Vue {
cursor: pointer;
}
.center {
justify-content: flex-start;
padding: 0 10px 0 10px;
}
.bottom {
justify-content: flex-end;
}
.bottom-span {
font-size: 70%;
margin-bottom: 10px;
}
.el-input {
max-width: 700px;
}
.space {
height: 20px;
}
</style>

View File

@@ -0,0 +1,121 @@
<template>
<Window @close="close">
<template slot="header">
<span style="position: relative; top: -3px">
Вставьте текст и нажмите
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
или F2
</span>
</template>
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
<hr/>
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import Window from '../../../share/Window.vue';
import _ from 'lodash';
import * as utils from '../../../../share/utils';
export default @Component({
components: {
Window,
},
})
class PasteTextPage extends Vue {
bookTitle = '';
created() {
}
mounted() {
this.$refs.textArea.focus();
}
getNonEmptyLine3words(text, count) {
let result = '';
const lines = text.split("\n");
let i = 0;
while (i < lines.length) {
if (lines[i].trim() != '') {
count--;
if (count <= 0) {
result = lines[i];
break;
}
}
i++;
}
result = result.trim().split(' ');
return result.slice(0, 3).join(' ');
}
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(' - ');
}
}
loadBuffer() {
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
this.close();
}
close() {
this.$emit('paste-text-toggle');
}
keyHook(event) {
if (event.type == 'keydown') {
switch (event.key) {
case 'F2':
this.loadBuffer();
break;
case 'Escape':
this.close();
break;
}
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.text {
flex: 1;
overflow-wrap: anywhere;
overflow-y: auto;
padding: 0 10px 0 10px;
position: relative;
font-size: 120%;
min-width: 400px;
}
.text:focus {
outline: none;
}
hr {
margin: 0;
padding: 0;
}
.clickable {
color: blue;
cursor: pointer;
}
</style>

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,366 @@
<template>
<Window width="600px" ref="window" @close="close">
<template slot="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>
</template>
<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
/>
<span v-html="props.cols[2].label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="num" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 25px">
{{ props.row.num }}
</div>
</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>
</q-td>
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
<div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">{{ props.row.desc.author }}</div>
<div>{{ props.row.desc.title }}</div>
</div>
</q-td>
<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, props.row.fullTitle)">Скачать FB2</a>
</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>
</q-td>
<q-td key="last" :props="props" class="no-mp">
</q-td>
</q-tr>
</template>
</q-table>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import path from 'path';
//import _ from 'lodash';
import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
export default @Component({
components: {
Window,
},
watch: {
search: function() {
this.updateTableData();
}
},
})
class RecentBooksPage extends Vue {
loading = false;
search = null;
tableData = [];
columns = [];
pagination = {};
created() {
this.firstInit = true;
this.pagination = {rowsPerPage: 0};
this.columns = [
{
name: 'num',
label: '#',
align: 'center',
sortable: true,
field: 'num',
},
{
name: 'date',
label: 'Время<br>просм.',
align: 'left',
field: 'touchDateTime',
sortable: true,
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
},
{
name: 'desc',
label: 'Название',
align: 'left',
field: 'descString',
sortable: true,
},
{
name: 'links',
label: '',
align: 'left',
},
{
name: 'close',
label: '',
align: 'left',
},
{
name: 'last',
label: '',
align: 'left',
},
];
}
init() {
this.$refs.window.init();
this.$nextTick(() => {
//this.$refs.input.focus();//плохо на планшетах
});
(async() => {//подгрузка списка
if (this.initing)
return;
this.initing = true;
if (this.firstInit) {//для отзывчивости
await this.updateTableData(20);
this.firstInit = false;
}
await utils.sleep(50);
await this.updateTableData();
this.initing = false;
})();
}
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 bt = utils.getBookTitle(book.fb2);
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.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,
fullTitle: bt.fullTitle,
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;
}
wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
const deci = num % 100;
if (deci > 10 && deci < 20) {
return '';
} else {
return endings[num % 10];
}
}
get header() {
const len = (this.tableData ? this.tableData.length : 0);
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
}
async downloadBook(fb2path, fullTitle) {
try {
await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download;
d.href = fb2path;
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;
if (errMes.indexOf('404') >= 0)
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
}
}
async handleDel(key) {
await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
if (!bookManager.mostRecentBook())
this.close();
}
loadBook(url) {
this.$emit('load-book', {url});
this.close();
}
isUrl(url) {
if (url)
return (url.indexOf('disk://') != 0);
else
return false;
}
close() {
this.$emit('recent-books-close');
}
keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.recent-books-table {
width: 600px;
overflow-y: auto;
overflow-x: hidden;
}
.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;
word-wrap: break-word;
white-space: normal;
}
</style>
<style>
.recent-books-table .q-table__middle {
height: 100%;
overflow-x: hidden;
}
.recent-books-table thead tr:first-child th {
position: sticky;
z-index: 1;
top: 0;
background-color: #c1f4cd;
}
.recent-books-table tr:nth-child(even) {
background-color: #f8f8f8;
}
</style>

View File

@@ -1,28 +1,28 @@
<template>
<div ref="main" class="main" @click="close">
<div class="mainWindow" @click.stop>
<Window @close="close">
<template slot="header">
{{ header }}
</template>
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header">
{{ header }}
</template>
<div class="content">
<span v-show="initStep">{{ initPercentage }}%</span>
<div class="content">
<span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input">
<input ref="input" class="el-input__inner"
placeholder="что ищем"
:value="needle" @input="needle = $event.target.value"/>
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
</div>
<el-button-group v-show="!initStep" class="button-group">
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
</el-button-group>
</div>
</Window>
<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"
/>
<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-group>
</div>
</div>
</Window>
</template>
<script>
@@ -43,7 +43,10 @@ export default @Component({
},
foundText: function(newValue) {
this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
//недостатки сторонних ui
const el = this.$refs.input.$el.querySelector('label div div div input');
if (el)
el.style.paddingRight = newValue.length*12 + 'px';
},
},
})
@@ -61,6 +64,8 @@ class SearchPage extends Vue {
}
async init(parsed) {
this.$refs.window.init();
if (this.parsed != parsed) {
this.initStep = true;
this.stopInit = false;
@@ -159,16 +164,17 @@ class SearchPage extends Vue {
close() {
this.stopInit = true;
this.$emit('search-toggle');
this.$emit('do-action', {action: 'search'});
}
inputKeyDown(event) {
if (event.key == 'Enter') {
this.showNext();
}
}
keyHook(event) {
//недостатки сторонних ui
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
this.showNext();
}
if (event.type == 'keydown' && (event.code == 'Escape')) {
if (event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
@@ -178,32 +184,13 @@ class SearchPage extends Vue {
</script>
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
max-width: 500px;
height: 125px;
display: flex;
position: relative;
top: -50px;
}
.content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
min-width: 430px;
}
.input {
@@ -215,16 +202,14 @@ class SearchPage extends Vue {
}
.button-group {
width: 150px;
width: 100px;
margin: 0;
padding: 0;
height: 37px;
}
.el-button {
.button {
padding: 9px 17px 9px 17px;
}
i {
font-size: 20px;
width: 50px;
}
</style>

View File

@@ -0,0 +1,731 @@
<template>
<div class="hidden"></div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
import localForage from 'localforage';
const ssCacheStore = localForage.createInstance({
name: 'ssCacheStore'
});
export default @Component({
watch: {
serverSyncEnabled: function() {
this.serverSyncEnabledChanged();
},
serverStorageKey: function() {
this.serverStorageKeyChanged(true);
},
settings: function() {
this.debouncedSaveSettings();
},
profiles: function() {
this.saveProfiles();
},
currentProfile: function() {
this.currentProfileChanged(true);
},
libs: function() {
this.debouncedSaveLibs();
},
},
})
class ServerStorage extends Vue {
created() {
this.inited = false;
this.keyInited = false;
this.commit = this.$store.commit;
this.prevServerStorageKey = null;
this.$root.$on('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.cleanCachedRecent('cachedRecent');
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
if (!this.cachedRecentPatch)
await this.cleanCachedRecent('cachedRecentPatch');
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
if (!this.cachedRecentMod)
await this.cleanCachedRecent('cachedRecentMod');
if (!this.serverStorageKey) {
//генерируем новый ключ
await this.generateNewServerStorageKey();
} else {
await this.serverStorageKeyChanged();
}
} finally {
this.inited = true;
}
}
async setCachedRecent(value) {
await ssCacheStore.setItem('recent', value);
this.cachedRecent = value;
}
async setCachedRecentPatch(value) {
await ssCacheStore.setItem('recent-patch', value);
this.cachedRecentPatch = value;
}
async setCachedRecentMod(value) {
await ssCacheStore.setItem('recent-mod', value);
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);
await this.serverStorageKeyChanged(true);
}
async serverSyncEnabledChanged() {
if (this.serverSyncEnabled) {
this.prevServerStorageKey = null;
if (!this.serverStorageKey) {
//генерируем новый ключ
await this.generateNewServerStorageKey();
} else {
await this.serverStorageKeyChanged(true);
}
}
}
async serverStorageKeyChanged(force) {
if (this.prevServerStorageKey != this.serverStorageKey) {
this.prevServerStorageKey = this.serverStorageKey;
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
this.keyInited = true;
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) {
await this.saveRecent();
}
}
}
async currentProfileChanged(force) {
if (!this.currentProfile)
return;
await this.loadSettings(force);
}
get serverSyncEnabled() {
return this.$store.state.reader.serverSyncEnabled;
}
get settings() {
return this.$store.state.reader.settings;
}
get settingsRev() {
return this.$store.state.reader.settingsRev;
}
get serverStorageKey() {
return this.$store.state.reader.serverStorageKey;
}
get profiles() {
return this.$store.state.reader.profiles;
}
get profilesRev() {
return this.$store.state.reader.profilesRev;
}
get currentProfile() {
return this.$store.state.reader.currentProfile;
}
get showServerStorageMessages() {
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', '');
}
}
success(message) {
if (this.showServerStorageMessages)
this.$root.notify.success(message);
}
warning(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$root.notify.warning(message);
}
error(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$root.notify.error(message);
}
async loadSettings(force = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
return;
const setsId = `settings-${this.currentProfile}`;
const oldRev = this.settingsRev[setsId] || 0;
//проверим ревизию на сервере
if (!force) {
try {
const revs = await this.storageCheck({[setsId]: {}});
if (revs.state == 'success' && revs.items[setsId].rev == oldRev) {
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let sets = null;
try {
sets = await this.storageGet({[setsId]: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (sets.state == 'success') {
sets = sets.items[setsId];
if (sets.rev == 0)
sets.data = {};
this.oldSettings = _.cloneDeep(sets.data);
this.commit('reader/setSettings', sets.data);
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
if (doNotifySuccess)
this.debouncedNotifySuccess();
} else {
this.warning(`Неверный ответ сервера: ${sets.state}`);
}
}
async saveSettings() {
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
return;
const diff = utils.getObjDiff(this.oldSettings, this.settings);
if (utils.isEmptyObjDiff(diff))
return;
this.savingSettings = true;
try {
const setsId = `settings-${this.currentProfile}`;
let result = {state: ''};
const oldRev = this.settingsRev[setsId] || 0;
try {
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
await this.loadSettings(true, false);
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else if (result.state == 'success') {
this.oldSettings = _.cloneDeep(this.settings);
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
}
} finally {
this.savingSettings = false;
}
}
async loadProfiles(force = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled)
return;
const oldRev = this.profilesRev;
//проверим ревизию на сервере
if (!force) {
try {
const revs = await this.storageCheck({profiles: {}});
if (revs.state == 'success' && revs.items.profiles.rev == oldRev) {
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let prof = null;
try {
prof = await this.storageGet({profiles: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (prof.state == 'success') {
prof = prof.items.profiles;
if (prof.rev == 0)
prof.data = {};
this.oldProfiles = _.cloneDeep(prof.data);
this.commit('reader/setProfiles', prof.data);
this.commit('reader/setProfilesRev', prof.rev);
this.checkCurrentProfile();
if (doNotifySuccess)
this.debouncedNotifySuccess();
} else {
this.warning(`Неверный ответ сервера: ${prof.state}`);
}
}
async saveProfiles() {
if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
return;
const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
if (utils.isEmptyObjDiff(diff))
return;
//обнуляются профили во время разработки при hotReload, подстраховка
if (!this.$store.state.reader.allowProfilesSave) {
console.error('Сохранение профилей не санкционировано');
return;
}
this.savingProfiles = true;
try {
let result = {state: ''};
try {
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
await this.loadProfiles(true, false);
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else if (result.state == 'success') {
this.oldProfiles = _.cloneDeep(this.profiles);
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;
this.loadingRecent = true;
try {
//проверим ревизию на сервере
let query = {recent: {}, recentPatch: {}, recentMod: {}};
let revs = null;
if (!skipRevCheck) {
try {
revs = await this.storageCheck(query);
if (revs.state == 'success') {
if (revs.items.recent.rev != this.cachedRecent.rev) {
//no changes
} else if (revs.items.recentPatch.rev != this.cachedRecentPatch.rev) {
query = {recentPatch: {}, recentMod: {}};
} else if (revs.items.recentMod.rev != this.cachedRecentMod.rev) {
query = {recentMod: {}};
} else
return true;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let recent = null;
try {
recent = await this.storageGet(query);
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (recent.state == 'success') {
let newRecent = recent.items.recent;
let newRecentPatch = recent.items.recentPatch;
let newRecentMod = recent.items.recentMod;
if (!newRecent) {
newRecent = _.cloneDeep(this.cachedRecent);
}
if (!newRecentPatch) {
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
}
if (!newRecentMod) {
newRecentMod = _.cloneDeep(this.cachedRecentMod);
}
if (newRecent.rev == 0) newRecent.data = {};
if (newRecentPatch.rev == 0) newRecentPatch.data = {};
if (newRecentMod.rev == 0) newRecentMod.data = {};
let result = Object.assign({}, newRecent.data, newRecentPatch.data);
const md = newRecentMod.data;
if (md.key && result[md.key])
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
/*if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией');
while (!bookManager.loaded) await utils.sleep(100);
}*/
if (newRecent.rev != this.cachedRecent.rev)
await this.setCachedRecent(newRecent);
if (newRecentPatch.rev != this.cachedRecentPatch.rev)
await this.setCachedRecentPatch(newRecentPatch);
if (newRecentMod.rev != this.cachedRecentMod.rev)
await this.setCachedRecentMod(newRecentMod);
await bookManager.setRecent(result);
} else {
this.warning(`Неверный ответ сервера: ${recent.state}`);
return;
}
if (doNotifySuccess)
this.debouncedNotifySuccess();
} finally {
this.loadingRecent = false;
}
return true;
}
async saveRecent(itemKey, recurse) {
while (!this.inited || this.savingRecent)
await utils.sleep(100);
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
return;
this.savingRecent = true;
try {
const bm = bookManager;
let needSaveRecent = false;
let needSaveRecentPatch = false;
let needSaveRecentMod = false;
//newRecentMod
let newRecentMod = {};
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
newRecentMod = _.cloneDeep(this.cachedRecentMod);
newRecentMod.rev++;
newRecentMod.data.key = itemKey;
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
needSaveRecentMod = true;
}
this.prevItemKey = itemKey;
//newRecentPatch
let newRecentPatch = {};
if (itemKey && !needSaveRecentMod) {
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
newRecentPatch.rev++;
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
let applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
needSaveRecentPatch = true;
needSaveRecentMod = true;
}
//newRecent
let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent
/*while (!bookManager.loaded)
await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
needSaveRecent = true;
needSaveRecentPatch = true;
needSaveRecentMod = true;
}
//query
let query = {};
if (needSaveRecent) {
query = {recent: newRecent, recentPatch: newRecentPatch, recentMod: newRecentMod};
} else if (needSaveRecentPatch) {
query = {recentPatch: newRecentPatch, recentMod: newRecentMod};
} else {
query = {recentMod: newRecentMod};
}
//сохранение
let result = {state: ''};
try {
result = await this.storageSet(query);
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
const res = await this.loadRecent(false, false);
if (res)
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) {
this.savingRecent = false;
this.saveRecent(itemKey, true);
return;
}
} else if (result.state == 'success') {
if (needSaveRecent && newRecent.rev)
await this.setCachedRecent(newRecent);
if (needSaveRecentPatch && newRecentPatch.rev)
await this.setCachedRecentPatch(newRecentPatch);
if (needSaveRecentMod && newRecentMod.rev)
await this.setCachedRecentMod(newRecentMod);
}
} finally {
this.savingRecent = false;
}
}
async storageCheck(items) {
return await this.storageApi('check', items);
}
async storageGet(items) {
return await this.storageApi('get', items);
}
async storageSet(items, force) {
return await this.storageApi('set', items, force);
}
async storageApi(action, items, force) {
const request = {action, items};
if (force)
request.force = true;
const encodedRequest = await this.encodeStorageItems(request);
return await this.decodeStorageItems(await readerApi.storage(encodedRequest));
}
async encodeStorageItems(request) {
if (!this.hashedStorageKey)
throw new Error('hashedStorageKey is empty');
if (!_.isObject(request.items))
throw new Error('items is not an object');
let result = Object.assign({}, request);
let items = {};
for (const id of Object.keys(request.items)) {
const item = request.items[id];
if (request.action == 'set' && !_.isObject(item.data))
throw new Error('encodeStorageItems: data is not an object');
let encoded = Object.assign({}, item);
if (item.data) {
const comp = utils.pako.deflate(JSON.stringify(item.data), {level: 1});
let encrypted = null;
try {
encrypted = cryptoUtils.aesEncrypt(comp, this.serverStorageKey);
} catch (e) {
throw new Error('encrypt failed');
}
encoded.data = '1' + utils.toBase64(encrypted);
}
items[`${this.hashedStorageKey}.${utils.toBase58(id)}`] = encoded;
}
result.items = items;
return result;
}
async decodeStorageItems(response) {
if (!this.hashedStorageKey)
throw new Error('hashedStorageKey is empty');
let result = Object.assign({}, response);
let items = {};
if (response.items) {
if (!_.isObject(response.items))
throw new Error('items is not an object');
for (const id of Object.keys(response.items)) {
const item = response.items[id];
let decoded = Object.assign({}, item);
if (item.data) {
if (!_.isString(item.data) || !item.data.length)
throw new Error('decodeStorageItems: data is not a string');
if (item.data[0] !== '1')
throw new Error('decodeStorageItems: unknown data format');
const a = utils.fromBase64(item.data.substr(1));
let decrypted = null;
try {
decrypted = cryptoUtils.aesDecrypt(a, this.serverStorageKey);
} catch (e) {
throw new Error('decrypt failed');
}
decoded.data = JSON.parse(utils.pako.inflate(decrypted, {to: 'string'}));
}
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;
}
}
result.items = items;
return result;
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,24 +1,26 @@
<template>
<div ref="main" class="main" @click="close">
<div class="mainWindow" @click.stop>
<Window @close="close">
<template slot="header">
Установить позицию
</template>
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header">
Установить позицию
</template>
<div class="slider">
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
</div>
</Window>
<div id="set-position-slider" class="slider q-px-md">
<q-slider
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
v-model="sliderValue"
:max="sliderMax"
label
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import Window from '../../share/Window.vue';
@@ -28,7 +30,8 @@ export default @Component({
},
watch: {
sliderValue: function(newValue) {
this.$emit('book-pos-changed', {bookPos: newValue});
if (this.initialized)
this.$emit('book-pos-changed', {bookPos: newValue});
},
},
})
@@ -39,13 +42,15 @@ class SetPositionPage extends Vue {
created() {
this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
this.initialized = false;
}
formatTooltip(val) {
if (this.sliderMax)
return (val/this.sliderMax*100).toFixed(2) + '%';
else
return 0;
init(sliderValue, sliderMax) {
this.$refs.window.init();
this.sliderMax = sliderMax;
this.sliderValue = sliderValue;
this.initialized = true;
}
close() {
@@ -53,8 +58,11 @@ class SetPositionPage extends Vue {
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
this.close();
if (event.type == 'keydown') {
const action = this.$root.readerActionByKeyEvent(event);
if (event.key == 'Escape' || action == 'setPosition') {
this.close();
}
}
return true;
}
@@ -63,34 +71,18 @@ class SetPositionPage extends Vue {
</script>
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
max-width: 600px;
height: 140px;
display: flex;
position: relative;
top: -50px;
}
.slider {
margin: 20px;
background-color: #efefef;
border-radius: 15px;
}
</style>
.el-slider {
margin-right: 20px;
margin-left: 20px;
<style>
#set-position-slider .q-slider__thumb path {
fill: white !important;
stroke: blue !important;
stroke-width: 2 !important;
}
</style>

View File

@@ -1,323 +1,104 @@
<template>
<div ref="main" class="main" @click="close">
<div class="mainWindow" @click.stop>
<Window @close="close">
<template slot="header">
Настройки
</template>
<template lang="includer">
<Window ref="window" height="95%" width="600px" @close="close">
<template slot="header">
Настройки
</template>
<el-tabs type="border-card" tab-position="left" v-model="selectedTab">
<!--------------------------------------------------------------------------->
<el-tab-pane label="Вид">
<div class="col row">
<div class="full-height">
<q-tabs
ref="tabs"
class="bg-grey-3 text-black"
v-model="selectedTab"
left-icon="la la-caret-up"
right-icon="la la-caret-down"
active-color="white"
active-bg-color="primary"
indicator-color="black"
vertical
no-caps
stretch
inline-label
>
<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="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" 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"/>
</q-tabs>
</div>
<el-form :model="form" size="small" label-width="120px" @submit.native.prevent>
<div class="partHeader">Цвет</div>
<div class="col fit">
<!-- Профили --------------------------------------------------------------------->
<div v-if="selectedTab == 'profiles'" class="fit tab-panel">
@@include('./include/ProfilesTab.inc');
</div>
<!-- Вид ------------------------------------------------------------------------->
<div v-if="selectedTab == 'view'" class="fit column">
@@include('./include/ViewTab.inc');
</div>
<!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
@@include('./include/ButtonsTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column">
@@include('./include/KeysTab.inc');
</div>
<!-- Листание -------------------------------------------------------------------->
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
@@include('./include/PageMoveTab.inc');
</div>
<!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel">
@@include('./include/OthersTab.inc');
</div>
<!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./include/ResetTab.inc');
</div>
</div>
<el-form-item label="Текст">
<el-col :span="12">
<el-color-picker v-model="textColor" color-format="hex" :predefine="predefineTextColors"></el-color-picker>
<span class="color-picked"><b>{{ textColor }}</b></span>
</el-col>
<el-col :span="5">
<span style="position: relative; top: 20px;">Обои:</span>
</el-col>
</el-form-item>
<el-form-item label="Фон">
<el-col :span="12">
<el-color-picker v-model="backgroundColor" color-format="hex" :predefine="predefineBackgroundColors" :disabled="wallpaper != ''"></el-color-picker>
<span v-show="wallpaper == ''" class="color-picked"><b>{{ backgroundColor }}</b></span>
</el-col>
<el-col :span="11">
<el-select v-model="wallpaper">
<el-option label="Нет" value=""></el-option>
<el-option label="1" value="paper1"></el-option>
<el-option label="2" value="paper2"></el-option>
<el-option label="3" value="paper3"></el-option>
<el-option label="4" value="paper4"></el-option>
<el-option label="5" value="paper5"></el-option>
<el-option label="6" value="paper6"></el-option>
<el-option label="7" value="paper7"></el-option>
<el-option label="8" value="paper8"></el-option>
<el-option label="9" value="paper9"></el-option>
</el-select>
</el-col>
</el-form-item>
</el-form>
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<div class="partHeader">Шрифт</div>
<el-form-item label="Локальный/веб">
<el-col :span="11">
<el-select v-model="fontName" placeholder="Шрифт" :disabled="webFontName != ''">
<el-option v-for="item in fonts"
:key="item.name"
:label="item.label"
:value="item.name">
</el-option>
</el-select>
</el-col>
<el-col :span="1">
&nbsp;
</el-col>
<el-col :span="11">
<el-tooltip :open-delay="500" effect="light" placement="top">
<template slot="content">
Веб шрифты дают большое разнообразие,<br>
однако есть шанс, что шрифт будет загружаться<br>
очень медленно или вовсе не загрузится
</template>
<el-select v-model="webFontName">
<el-option label="Нет" value=""></el-option>
<el-option v-for="item in webFonts"
:key="item.name"
:value="item.name">
</el-option>
</el-select>
</el-tooltip>
</el-col>
</el-form-item>
<el-form-item label="Размер">
<el-col :span="17">
<el-input-number v-model="fontSize" :min="5" :max="200"></el-input-number>
</el-col>
<el-col :span="1">
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
</el-col>
</el-form-item>
<el-form-item label="Сдвиг">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Сдвиг шрифта по вертикали в процентах от размера.<br>
Отрицательное значение сдвигает вверх, положительное -<br>
вниз. Значение зависит от метрики шрифта.
</template>
<el-input-number v-model="vertShift" :min="-100" :max="100"></el-input-number>
</el-tooltip>
</el-form-item>
<el-form-item label="Стиль">
<el-col :span="8">
<el-checkbox v-model="fontBold">Жирный</el-checkbox>
</el-col>
<el-col :span="8">
<el-checkbox v-model="fontItalic">Курсив</el-checkbox>
</el-col>
</el-form-item>
</el-form>
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<div class="partHeader">Текст</div>
<el-form-item label="Интервал">
<el-input-number v-model="lineInterval" :min="0" :max="200"></el-input-number>
</el-form-item>
<el-form-item label="Параграф">
<el-input-number v-model="p" :min="0" :max="2000"></el-input-number>
</el-form-item>
<el-form-item label="Отступ">
<el-col :span="11">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Слева/справа
</template>
<el-input-number v-model="indentLR" :min="0" :max="2000"></el-input-number>
</el-tooltip>
</el-col>
<el-col :span="1">
&nbsp;
</el-col>
<el-col :span="11">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Сверху/снизу
</template>
<el-input-number v-model="indentTB" :min="0" :max="2000"></el-input-number>
</el-tooltip>
</el-col>
</el-form-item>
<el-form-item label="Сдвиг">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
Отрицательное значение сдвигает вверх, положительное -<br>
вниз.
</template>
<el-input-number v-model="textVertShift" :min="-100" :max="100"></el-input-number>
</el-tooltip>
</el-form-item>
<el-form-item label="Скроллинг">
<el-col :span="11">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Замедление скроллинга в миллисекундах.<br>
Определяет время, за которое текст<br>
прокручивается на одну строку.
</template>
<el-input-number v-model="scrollingDelay" :min="1" :max="10000"></el-input-number>
</el-tooltip>
</el-col>
<el-col :span="1">
&nbsp;
</el-col>
<el-col :span="11">
<el-tooltip :open-delay="500" effect="light" placement="top">
<template slot="content">
Вид скроллинга: линейный,<br>
ускорение-замедление и пр.
</template>
<el-select v-model="scrollingType">
<el-option value="linear"></el-option>
<el-option value="ease"></el-option>
<el-option value="ease-in"></el-option>
<el-option value="ease-out"></el-option>
<el-option value="ease-in-out"></el-option>
</el-select>
</el-tooltip>
</el-col>
</el-form-item>
<el-form-item label="Выравнивание">
<el-checkbox v-model="textAlignJustify">По ширине</el-checkbox>
<el-checkbox v-model="wordWrap">Перенос по слогам</el-checkbox>
</el-form-item>
<el-form-item label="Обработка">
<el-checkbox v-model="cutEmptyParagraphs" @change="needReload">Убирать пустые параграфы</el-checkbox>
</el-form-item>
<el-form-item label="">
<el-col :span="12">
Добавлять пустые
</el-col>
<el-input-number v-model="addEmptyParagraphs" :min="0" :max="2" @change="needReload"></el-input-number>
</el-form-item>
</el-form>
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<div class="partHeader">Строка статуса</div>
<el-form-item label="Статус">
<el-checkbox v-model="showStatusBar">Показывать</el-checkbox>
<el-checkbox v-model="statusBarTop" :disabled="!showStatusBar">Вверху/внизу</el-checkbox>
</el-form-item>
<el-form-item label="Высота">
<el-input-number v-model="statusBarHeight" :min="5" :max="100" :disabled="!showStatusBar"></el-input-number>
</el-form-item>
<el-form-item label="Прозрачность">
<el-input-number v-model="statusBarColorAlpha" :min="0" :max="1" :precision="2" :step="0.1" :disabled="!showStatusBar"></el-input-number>
</el-form-item>
</el-form>
</el-tab-pane>
<!--------------------------------------------------------------------------->
<el-tab-pane label="Листание">
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<div class="partHeader">Анимация</div>
<el-form-item label="Вид">
не готово
</el-form-item>
<el-form-item label="Скорость">
не готово
</el-form-item>
</el-form>
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<div class="partHeader">Другое</div>
<el-form-item label="Страница">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Переносить последнюю строку страницы<br>
в начало следующей при листании
</template>
<el-checkbox v-model="keepLastToFirst">Переносить последнюю строку</el-checkbox>
</el-tooltip>
</el-form-item>
</el-form>
</el-tab-pane>
<!--------------------------------------------------------------------------->
<el-tab-pane label="Прочее">
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
<el-form-item label="Управление">
<el-checkbox v-model="clickControl">Включить управление кликом</el-checkbox>
</el-form-item>
<el-form-item label="Подсказка">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Показывать или нет подсказку при каждой загрузке книги
</template>
<el-checkbox v-model="showClickMapPage">Показывать области управления кликом</el-checkbox>
</el-tooltip>
</el-form-item>
<el-form-item label="URL">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Добавление параметра "__p" в строке браузера<br>
позволяет передавать ссылку на книгу в читалке<br>
без потери текущей позиции. Однако в этом случае<br>
при листании забивается история браузера, т.к. на<br>
каждое изменение позиции происходит смена URL.
</template>
<el-checkbox v-model="allowUrlParamBookPos">Добавлять параметр "__p"</el-checkbox>
</el-tooltip>
</el-form-item>
<el-form-item label="Парсинг">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Включение этой опции позволяет делать предварительную<br>
обработку текста в ленивом режиме сразу после загрузки<br>
книги. Это может повысить отзывчивость читалки, но<br>
нагружает процессор каждый раз при открытии книги.
</template>
<el-checkbox v-model="lazyParseEnabled">Предварительная обработка текста</el-checkbox>
</el-tooltip>
</el-form-item>
<el-form-item label="Копирование">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Загружать весь текст в окно<br>
копирования текста со страницы
</template>
<el-checkbox v-model="copyFullText">Загружать весь текст</el-checkbox>
</el-tooltip>
</el-form-item>
</el-form>
</el-tab-pane>
<!--------------------------------------------------------------------------->
<el-tab-pane label="Сброс">
<el-button @click="setDefaults">Установить по-умолчанию</el-button>
</el-tab-pane>
</el-tabs>
</Window>
</div>
</div>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
export default @Component({
components: {
Window,
NumInput,
UserHotKeys,
},
data: function() {
return Object.assign({}, rstore.settingDefaults);
},
watch: {
settings: function() {
this.settingsChanged();
},
form: function(newValue) {
this.commit('reader/setSettings', newValue);
if (this.inited)
this.commit('reader/setSettings', newValue);
},
fontBold: function(newValue) {
this.fontWeight = (newValue ? 'bold' : '');
@@ -327,8 +108,10 @@ export default @Component({
},
vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName);
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
if (this.fontShifts[font] != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
}
},
fontName: function(newValue) {
const font = (this.webFontName ? this.webFontName : newValue);
@@ -338,29 +121,88 @@ export default @Component({
const font = (newValue ? newValue : this.fontName);
this.vertShift = this.fontShifts[font] || 0;
},
wallpaper: function(newValue) {
if (newValue != '' && this.pageChangeAnimation == 'flip')
this.pageChangeAnimation = '';
},
textColor: function(newValue) {
this.textColorFiltered = newValue;
},
textColorFiltered: function(newValue) {
if (hex.test(newValue))
this.textColor = newValue;
},
backgroundColor: function(newValue) {
this.bgColorFiltered = newValue;
},
bgColorFiltered: function(newValue) {
if (hex.test(newValue))
this.backgroundColor = newValue;
},
},
})
class SettingsPage extends Vue {
selectedTab = null;
selectedTab = 'profiles';
selectedViewTab = 'color';
selectedKeysTab = 'mouse';
form = {};
fontBold = false;
fontItalic = false;
vertShift = 0;
tabsScrollable = false;
textColorFiltered = '';
bgColorFiltered = '';
webFonts = [];
fonts = [];
serverStorageKeyVisible = false;
toolButtons = [];
rstore = {};
created() {
this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
this.form = {};
this.rstore = rstore;
this.toolButtons = rstore.toolButtons;
this.settingsChanged();
}
mounted() {
this.$watch(
'$refs.tabs.scrollable',
(newValue) => {
this.tabsScrollable = newValue && !this.$isMobileDevice;
}
);
}
init() {
this.$refs.window.init();
this.inited = true;
}
settingsChanged() {
if (_.isEqual(this.form, this.settings))
return;
this.form = Object.assign({}, this.settings);
if (!this.unwatch)
this.unwatch = {};
for (let prop in rstore.settingDefaults) {
if (this.unwatch && this.unwatch[prop])
this.unwatch[prop]();
this[prop] = this.form[prop];
this.$watch(prop, (newValue) => {
this.unwatch[prop] = this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue});
});
}
this.fontBold = (this.fontWeight == 'bold');
this.fontItalic = (this.fontStyle == 'italic');
@@ -368,52 +210,157 @@ class SettingsPage extends Vue {
this.webFonts = rstore.webFonts;
const font = (this.webFontName ? this.webFontName : this.fontName);
this.vertShift = this.fontShifts[font] || 0;
this.textColorFiltered = this.textColor;
this.bgColorFiltered = this.backgroundColor;
}
get mode() {
return this.$store.state.config.mode;
}
get settings() {
return this.$store.state.reader.settings;
}
get serverSyncEnabled() {
return this.$store.state.reader.serverSyncEnabled;
}
set serverSyncEnabled(newValue) {
this.commit('reader/setServerSyncEnabled', newValue);
}
get profiles() {
return this.$store.state.reader.profiles;
}
get currentProfileOptions() {
const profNames = Object.keys(this.profiles)
profNames.sort();
let result = [{label: 'Нет', value: ''}];
profNames.forEach(name => {
result.push({label: name, value: name});
});
return result;
}
get wallpaperOptions() {
let result = [{label: 'Нет', value: ''}];
for (let i = 1; i < 10; i++) {
result.push({label: i, value: `paper${i}`});
}
return result;
}
get fontsOptions() {
let result = [];
this.fonts.forEach(font => {
result.push({label: (font.label ? font.label : font.name), value: font.name});
});
return result;
}
get webFontsOptions() {
let result = [{label: 'Нет', value: ''}];
this.webFonts.forEach(font => {
result.push({label: font.name, value: font.name});
});
return result;
}
get pageChangeAnimationOptions() {
let result = [
{label: 'Нет', value: ''},
{label: 'Вверх-вниз', value: 'downShift'},
{label: 'Вправо-влево', value: 'rightShift'},
{label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'},
];
if (this.wallpaper == '')
result.push({label: 'Листание', value: 'flip'});
return result;
}
get currentProfile() {
return this.$store.state.reader.currentProfile;
}
set currentProfile(newValue) {
this.commit('reader/setCurrentProfile', newValue);
}
get partialStorageKey() {
return this.serverStorageKey.substr(0, 7) + '***';
}
get serverStorageKey() {
return this.$store.state.reader.serverStorageKey;
}
get setStorageKeyLink() {
return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
}
get predefineTextColors() {
return [
return defPalette.concat([
'#ffffff',
'#000000',
'#202020',
'#323232',
'#aaaaaa',
'#00c0c0',
];
'#ebe2c9',
'#cfdc99',
'#478355',
'#909080',
]);
}
get predefineBackgroundColors() {
return [
return defPalette.concat([
'#ffffff',
'#000000',
'#202020',
'#ebe2c9',
'#cfdc99',
'#478355',
'#a6caf0',
'#909080',
'#808080',
'#c8c8c8',
'#478355',
'#a6caf0',
];
]);
}
colorPanStyle(type) {
let result = 'width: 30px; height: 30px; border: 1px solid black; border-radius: 4px;';
switch (type) {
case 'text':
result += `background-color: ${this.textColor};`
break;
case 'bg':
result += `background-color: ${this.backgroundColor};`
break;
}
return result;
}
needReload() {
this.$notify.warning({message: 'Необходимо обновить страницу (F5), чтобы изменения возымели эффект'});
this.$root.notify.warning('Необходимо обновить страницу (F5), чтобы изменения возымели эффект');
}
needTextReload() {
this.$root.notify.warning('Необходимо обновить книгу в обход кэша, чтобы изменения возымели эффект');
}
close() {
this.$emit('settings-toggle');
this.$emit('do-action', {action: 'settings'});
}
async setDefaults() {
try {
if (await this.$confirm('Подтвердите установку настроек по-умолчанию', '', {
confirmButtonText: 'OK',
cancelButtonText: 'Отмена',
type: 'warning'
})) {
if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) {
this.form = Object.assign({}, rstore.settingDefaults);
for (let prop in rstore.settingDefaults) {
this[prop] = this.form[prop];
@@ -424,8 +371,145 @@ 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) {
this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
return;
}
const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
});
if (result && result.value) {
if (this.profiles[result.value]) {
this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
} else {
const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', newProfiles);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = result.value;
}
}
} catch (e) {
//
}
}
async delProfile() {
if (!this.currentProfile)
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
if (this.profiles[this.currentProfile]) {
const newProfiles = Object.assign({}, this.profiles);
delete newProfiles[this.currentProfile];
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', newProfiles);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = '';
}
}
} catch (e) {
//
}
}
async delAllProfiles() {
if (!Object.keys(this.profiles).length)
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', {});
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = '';
}
} catch (e) {
//
}
}
async copyToClip(text, prefix) {
const result = await utils.copyTextToClipboard(text);
const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
if (result)
this.$root.notify.success(msg);
else
this.$root.notify.error(msg);
}
async showServerStorageKey() {
this.serverStorageKeyVisible = !this.serverStorageKeyVisible;
}
async enterServerStorageKey(key) {
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Изменение ключа доступа приведет к замене всех профилей и читаемых книг в читалке.` +
`<br><br>Введите новый ключ доступа:`, ' ', {
inputValidator: (str) => {
try {
if (str && utils.fromBase58(str).length == 32) {
return true;
}
} catch (e) {
//
}
return 'Неверный формат ключа';
},
inputValue: (key && _.isString(key) ? key : null),
});
if (result && result.value && utils.fromBase58(result.value).length == 32) {
this.commit('reader/setServerStorageKey', result.value);
}
} catch (e) {
//
}
}
async generateServerStorageKey() {
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
`<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.$root.$emit('generateNewServerStorageKey');
}
} catch (e) {
//
}
}
keyHook(event) {
if (event.type == 'keydown' && event.code == 'Escape') {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close();
}
return true;
@@ -435,57 +519,79 @@ class SettingsPage extends Vue {
</script>
<style scoped>
.main {
position: absolute;
.tab {
justify-content: initial;
}
.tab-panel {
overflow-x: hidden;
overflow-y: auto;
font-size: 90%;
padding: 0 10px 15px 10px;
}
.part-header {
border-top: 2px solid #bbbbbb;
font-weight: bold;
font-size: 110%;
margin-top: 15px;
margin-bottom: 5px;
}
.item {
width: 100%;
height: 100%;
z-index: 60;
margin-top: 5px;
margin-bottom: 5px;
}
.label-1 {
width: 75px;
}
.label-2, .label-3, .label-4, .label-5 {
width: 110px;
}
.label-6 {
width: 100px;
}
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: right;
margin-right: 10px;
overflow: hidden;
}
.mainWindow {
height: 70%;
display: flex;
position: relative;
.text {
font-size: 90%;
line-height: 130%;
}
.el-form {
border-top: 2px solid #bbbbbb;
margin-bottom: 5px;
.button {
margin: 3px 15px 3px 0;
padding: 0 5px 0 5px;
}
.el-form-item {
padding: 0;
.copy-icon {
margin-left: 5px;
cursor: pointer;
font-size: 120%;
color: blue;
}
.input {
max-width: 150px;
}
.no-mp {
margin: 0;
margin-bottom: 5px;
padding: 0;
}
.color-picked {
margin-left: 10px;
position: relative;
top: -11px;
.col-left {
width: 150px;
}
.partHeader {
font-weight: bold;
margin-bottom: 5px;
}
.el-tabs {
flex: 1;
display: flex;
}
.el-tab-pane {
flex: 1;
display: flex;
flex-direction: column;
width: 420px;
overflow-y: auto;
padding: 15px;
}
</style>
</style>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedKeysTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="mouse" label="Мышь/тачскрин" />
<q-tab name="keyboard" label="Клавиатура" />
</q-tabs>
</div>
<div class="q-mb-sm"/>
<div class="col tab-panel">
<div v-if="selectedKeysTab == 'mouse'">
<div class="item row">
<div class="label-4"></div>
<div class="col row">
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
</div>
</div>
</div>
<div v-if="selectedKeysTab == 'keyboard'">
<div class="item row">
<UserHotKeys v-model="userHotKeys" />
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,165 @@
import {sleep} from '../../../share/utils';
export default class DrawHelper {
fontBySize(size) {
return `${size}px ${this.fontName}`;
}
fontByStyle(style) {
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
this.context.font = this.fontByStyle(style);
return this.context.measureText(text).width;
}
measureTextFont(text, font) {// eslint-disable-line no-unused-vars
this.context.font = font;
return this.context.measureText(text).width;
}
drawPage(lines, isScrolling) {
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
return '';
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;` +
` 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 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 lineText = '';
let center = false;
let space = 0;
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++;
}
} 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 {
//
}
}
}
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 += (i > 0 ? '<br>' : '') + lineText;
}
out += '</div>';
return out;
}
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength) {
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 t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
const w1 = this.measureTextFont(t1, font) + fh2;
const read = (bookPos + 1)/textLength;
const t2 = `${(read*100).toFixed(2)}%`;
@@ -95,4 +246,127 @@ export default class DrawHelper {
return `<div style="position: absolute; left: ${x}px; top: ${y}px; ` +
`width: ${w}px; height: ${h}px; box-sizing: border-box; border: 1px solid ${color}"></div>`;
}
async doPageAnimationThaw(page1, page2, duration, isDown, animation1Finish) {
page1.style.animation = `page1-animation-thaw ${duration}ms ease-in 1`;
page2.style.animation = `page2-animation-thaw ${duration}ms ease-in 1`;
await animation1Finish(duration);
}
async doPageAnimationBlink(page1, page2, duration, isDown, animation1Finish, animation2Finish) {
page1.style.opacity = '0';
page2.style.opacity = '0';
page2.style.animation = `page2-animation-thaw ${duration/2}ms ease-out 1`;
await animation2Finish(duration/2);
page1.style.opacity = '1';
page1.style.animation = `page1-animation-thaw ${duration/2}ms ease-in 1`;
await animation1Finish(duration/2);
page2.style.opacity = '1';
}
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
const s = this.w + this.fontSize;
if (isDown) {
page1.style.transform = `translateX(${s}px)`;
await sleep(30);
page1.style.transition = `${duration}ms ease-in-out`;
page1.style.transform = `translateX(0px)`;
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `translateX(-${s}px)`;
await animation1Finish(duration);
} else {
page1.style.transform = `translateX(-${s}px)`;
await sleep(30);
page1.style.transition = `${duration}ms ease-in-out`;
page1.style.transform = `translateX(0px)`;
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `translateX(${s}px)`;
await animation1Finish(duration);
}
}
async doPageAnimationDownShift(page1, page2, duration, isDown, animation1Finish) {
const s = this.h + this.fontSize/2;
if (isDown) {
page1.style.transform = `translateY(${s}px)`;
await sleep(30);
page1.style.transition = `${duration}ms ease-in-out`;
page1.style.transform = `translateY(0px)`;
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `translateY(-${s}px)`;
await animation1Finish(duration);
} else {
page1.style.transform = `translateY(-${s}px)`;
await sleep(30);
page1.style.transition = `${duration}ms ease-in-out`;
page1.style.transform = `translateY(0px)`;
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `translateY(${s}px)`;
await animation1Finish(duration);
}
}
async doPageAnimationRotate(page1, page2, duration, isDown, animation1Finish, animation2Finish) {
if (isDown) {
page1.style.transform = `rotateY(90deg)`;
await sleep(30);
page2.style.transition = `${duration/2}ms ease-in`;
page2.style.transform = `rotateY(-90deg)`;
await animation2Finish(duration/2);
page1.style.transition = `${duration/2}ms ease-out`;
page1.style.transform = `rotateY(0deg)`;
await animation1Finish(duration/2);
} else {
page1.style.transform = `rotateY(-90deg)`;
await sleep(30);
page2.style.transition = `${duration/2}ms ease-in`;
page2.style.transform = `rotateY(90deg)`;
await animation2Finish(duration/2);
page1.style.transition = `${duration/2}ms ease-out`;
page1.style.transform = `rotateY(0deg)`;
await animation1Finish(duration/2);
}
}
async doPageAnimationFlip(page1, page2, duration, isDown, animation1Finish, animation2Finish, backgroundColor) {
page2.style.background = backgroundColor;
if (isDown) {
page2.style.transformOrigin = '5%';
await sleep(30);
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `rotateY(-120deg) translateX(${this.w/4}px)`;
await animation2Finish(duration);
} else {
page2.style.transformOrigin = '95%';
await sleep(30);
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `rotateY(120deg) translateX(-${this.w/4}px)`;
await animation2Finish(duration);
}
page2.style.transformOrigin = 'center';
page2.style.background = '';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,15 @@
import he from 'he';
import sax from '../../../../server/core/BookConverter/sax';
import {sleep} from '../../../share/utils';
import sax from '../../../../server/core/sax';
import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
export default class BookParser {
constructor(settings) {
if (settings) {
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
}
// defaults
this.p = 30;// px, отступ параграфа
this.w = 300;// px, ширина страницы
@@ -13,12 +19,6 @@ export default class BookParser {
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
};
//настройки
if (settings) {
this.cutEmptyParagraphs = settings.cutEmptyParagraphs;
this.addEmptyParagraphs = settings.addEmptyParagraphs;
}
}
async parse(data, callback) {
@@ -32,9 +32,6 @@ export default class BookParser {
//defaults
let fb2 = {
firstName: '',
middleName: '',
lastName: '',
bookTitle: '',
};
@@ -43,6 +40,26 @@ export default class BookParser {
let center = false;
let bold = false;
let italic = false;
let space = 0;
let inPara = false;
let isFirstBody = true;
let isFirstSection = true;
let isFirstTitlePara = false;
//изображения
this.binary = {};
let binaryId = '';
let binaryType = '';
let dimPromises = [];
//оглавление
this.contents = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
let paraIndex = -1;
let paraOffset = 0;
@@ -51,25 +68,72 @@ export default class BookParser {
index: Number,
offset: Number, //сумма всех length до этого параграфа
length: Number, //длина text без тегов
text: String //текст параграфа (или title или epigraph и т.д) с вложенными тегами
text: String, //текст параграфа с вложенными тегами
addIndex: Number, //индекс добавляемого пустого параграфа (addEmptyParagraphs)
}
*/
const newParagraph = (text, len, noCut) => {
//схлопывание пустых параграфов
if (!noCut && this.cutEmptyParagraphs && paraIndex >= 0 && len == 1 && text[0] == ' ') {
let p = para[paraIndex];
if (p.length == 1 && p.text[0] == ' ')
return;
}
const getImageDimensions = (binaryId, binaryType, data) => {
return new Promise ((resolve, reject) => { (async() => {
const i = new Image();
let resolved = false;
i.onload = () => {
resolved = true;
this.binary[binaryId] = {
w: i.width,
h: i.height,
type: binaryType,
data
};
resolve();
};
i.onerror = reject;
i.src = `data:${binaryType};base64,${data}`;
await utils.sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
})().catch(reject); });
};
const getExternalImageDimensions = (src) => {
return new Promise ((resolve, reject) => { (async() => {
const i = new Image();
let resolved = false;
i.onload = () => {
resolved = true;
this.binary[src] = {
w: i.width,
h: i.height,
};
resolve();
};
i.onerror = reject;
i.src = src;
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,
addIndex: (addIndex ? addIndex : 0),
};
if (inSubtitle) {
curSubtitle.title += '<p>';
} else if (inTitle) {
curTitle.title += '<p>';
}
para[paraIndex] = p;
paraOffset += p.length;
};
@@ -81,50 +145,144 @@ export default class BookParser {
return;
}
const prevParaIndex = paraIndex;
let p = para[paraIndex];
if (p) {
//добавление пустых параграфов
if (this.addEmptyParagraphs && p.length == 1 && p.text[0] == ' ' && len > 0) {
let i = this.addEmptyParagraphs;
while (i > 0) {
newParagraph(' ', 1, true);
i--;
}
p = para[paraIndex];
paraOffset -= p.length;
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
paraIndex--;
for (let i = 0; i < 2; i++) {
newParagraph(' ', 1, i + 1);
}
paraOffset -= p.length;
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
p.length = 0;
p.text = p.text.substr(1);
}
p.length += len;
p.text += text;
} else {
p = {
index: paraIndex,
offset: paraOffset,
length: len,
text: text
};
paraIndex++;
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
//уберем начальный пробел
p.length = 0;
p.text = p.text.substr(1);
}
p.length += len;
p.text += text;
if (inSubtitle) {
curSubtitle.title += text;
} else if (inTitle) {
curTitle.title += text;
}
para[paraIndex] = p;
paraOffset += p.length;
};
const onStartNode = (elemName) => {// eslint-disable-line no-unused-vars
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
if (elemName == '?xml')
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')
binaryId = (attrs.id.value ? attrs.id.value : '');
}
if (tag == 'image') {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
const href = attrs.href.value;
if (href[0] == '#') {//local
if (inPara && !this.showInlineImagesInCenter && !center)
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
else
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
if (inPara && this.showInlineImagesInCenter)
newParagraph(' ', 1);
} else {//external
dimPromises.push(getExternalImageDimensions(href));
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
}
}
}
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(' ', 1);
}
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
const bt = utils.getBookTitle(fb2);
if (bt.sequence) {
newParagraph(bt.sequence, bt.sequence.length);
newParagraph(' ', 1);
}
}
if (!isFirstBody)
newParagraph(' ', 1);
isFirstBody = false;
bodyIndex++;
}
if (tag == 'title') {
newParagraph(' ', 1);
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);
isFirstSection = false;
sectionLevel++;
}
if (tag == 'emphasis' || tag == 'strong') {
@@ -132,16 +290,28 @@ export default class BookParser {
}
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
newParagraph(' ', 1);
if (!(tag == 'p' && isFirstTitlePara))
newParagraph(' ', 1);
if (tag == 'p') {
inPara = true;
isFirstTitlePara = false;
}
}
if (tag == 'subtitle') {
newParagraph(' ', 1);
isFirstTitlePara = true;
bold = true;
center = true;
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') {
@@ -149,34 +319,59 @@ export default class BookParser {
}
if (tag == 'text-author') {
newParagraph(' <s> <s> <s> ', 4);
newParagraph(' ', 1);
space += 1;
}
}
};
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
if (tag == elemName) {
if (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong') {
growParagraph(`</${tag}>`, 0);
}
if (tag == 'subtitle') {
bold = false;
if (tag == 'p') {
inPara = false;
}
if (tag == 'epigraph') {
if (tag == 'subtitle') {
isFirstTitlePara = false;
bold = false;
center = false;
inSubtitle = false;
}
if (tag == 'epigraph' || tag == 'annotation') {
italic = false;
space -= 1;
if (tag == 'annotation')
newParagraph(' ', 1);
}
if (tag == 'stanza') {
newParagraph(' ', 1);
}
if (tag == 'text-author') {
space -= 1;
}
}
path = path.substr(0, path.length - tag.length - 1);
@@ -194,23 +389,27 @@ export default class BookParser {
text = text.replace(/>/g, '&gt;');
text = text.replace(/</g, '&lt;');
if (text != ' ' && text.trim() == '')
text = text.trim();
if (text && text.trim() == '')
text = (text.indexOf(' ') >= 0 ? ' ' : '');
if (text == '')
if (!text)
return;
text = text.replace(/[\t\n\r]/g, ' ');
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':
fb2.firstName = text;
if (authorLength)
fb2.author[authorLength - 1].firstName = text;
break;
case '/fictionbook/description/title-info/author/middle-name':
fb2.middleName = text;
if (authorLength)
fb2.author[authorLength - 1].middleName = text;
break;
case '/fictionbook/description/title-info/author/last-name':
fb2.lastName = text;
if (authorLength)
fb2.author[authorLength - 1].lastName = text;
break;
case '/fictionbook/description/title-info/genre':
fb2.genre = text;
@@ -238,27 +437,26 @@ export default class BookParser {
let tOpen = (center ? '<center>' : '');
tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
let tClose = (italic ? '</emphasis>' : '');
tOpen += (space ? `<space w="${space}">` : '');
let tClose = (space ? '</space>' : '');
tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
tClose += (center ? '</center>' : '');
if (path.indexOf('/fictionbook/body/title') == 0) {
growParagraph(`${tOpen}${text}${tClose}`, text.length, true);
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);
}
if (path.indexOf('/fictionbook/body/section') == 0) {
switch (tag) {
case 'p':
growParagraph(`${tOpen}${text}${tClose}`, text.length);
break;
default:
growParagraph(`${tOpen}${text}${tClose}`, text.length);
}
if (binaryId) {
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
}
};
const onProgress = async(prog) => {
await sleep(1);
await utils.sleep(1);
callback(prog);
};
@@ -266,13 +464,21 @@ export default class BookParser {
onStartNode, onEndNode, onTextNode, onProgress
});
if (dimPromises.length) {
try {
await Promise.all(dimPromises);
} catch (e) {
//
}
}
this.fb2 = fb2;
this.para = para;
this.textLength = paraOffset;
callback(100);
await sleep(10);
await utils.sleep(10);
return {fb2};
}
@@ -301,19 +507,22 @@ export default class BookParser {
splitToStyle(s) {
let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, center: Boolean},
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number},
image: {local: Boolean, inline: Boolean, id: String},
text: String,
}*/
let style = {};
let image = {};
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
result.push({
style: Object.assign({}, style),
text: text
image,
text
});
};
const onStartNode = async(elemName) => {// eslint-disable-line no-unused-vars
const onStartNode = async(elemName, tail) => {// eslint-disable-line no-unused-vars
switch (elemName) {
case 'strong':
style.bold = true;
@@ -324,6 +533,42 @@ export default class BookParser {
case 'center':
style.center = true;
break;
case 'space': {
let attrs = sax.getAttrsSync(tail);
if (attrs.w && attrs.w.value)
style.space = attrs.w.value;
break;
}
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};
}
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;
}
result.push({
style: Object.assign({}, style),
image: {local, inline: true, id},
text: ''
});
}
break;
}
}
};
@@ -338,6 +583,14 @@ export default class BookParser {
case 'center':
style.center = false;
break;
case 'space':
style.space = 0;
break;
case 'image':
image = {};
break;
case 'image-inline':
break;
}
};
@@ -345,27 +598,28 @@ export default class BookParser {
onStartNode, onEndNode, onTextNode
});
//длинные слова (или белиберду без пробелов) тоже разобьем
const maxWordLength = this.maxWordLength;
const parts = result;
result = [];
for (const part of parts) {
let p = part;
let i = 0;
let spaceIndex = -1;
while (i < p.text.length) {
if (p.text[i] == ' ')
spaceIndex = i;
if (!p.image.id) {
let i = 0;
let spaceIndex = -1;
while (i < p.text.length) {
if (p.text[i] == ' ')
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) {
result.push({style: p.style, text: p.text.substr(0, i + 1)});
p = {style: p.style, text: p.text.substr(i + 1)};
spaceIndex = -1;
i = -1;
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) {
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;
i = -1;
}
i++;
}
i++;
}
result.push(p);
}
@@ -428,20 +682,35 @@ export default class BookParser {
if (!this.force &&
para.parsed &&
para.parsed.testWidth === this.testWidth &&
para.parsed.w === this.w &&
para.parsed.p === this.p &&
para.parsed.wordWrap === this.wordWrap &&
para.parsed.maxWordLength === this.maxWordLength &&
para.parsed.font === this.font
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
)
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: true, //вычисляется позже
};
@@ -453,25 +722,95 @@ export default class BookParser {
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, w: Number, h: Number},
text: String,
}
}*/
let parts = this.splitToStyle(para.text);
//инициализация парсера
let line = {begin: para.offset, parts: []};
let paragraphText = '';//текст параграфа
let partText = '';//накапливаемый кусок со стилем
let str = '';//измеряемая строка
let prevStr = '';//строка без крайнего слова
let prevW = 0;
let j = 0;//номер строки
let style = {};
let ofs = 0;//смещение от начала параграфа para.offset
// тут начинается самый замес, перенос по слогам и стилизация
let imgW = 0;
let imageInPara = false;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) {
const words = part.text.split(' ');
style = part.style;
paragraphText += part.text;
//изображения
if (part.image.id && !part.image.inline) {
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);
const maxH = lineCount*this.lineHeight;
let maxH2 = maxH;
if (this.imageFitWidth && bin.w > this.w) {
maxH2 = bin.h*this.w/bin.w;
c = Math.ceil(maxH2/this.lineHeight);
}
lineCount = (c < lineCount ? c : lineCount);
let imageHeight = (maxH2 < maxH ? maxH2 : maxH);
imageHeight = (imageHeight <= bin.h ? imageHeight : bin.h);
let imageWidth = (bin.h > imageHeight ? bin.w*imageHeight/bin.h : bin.w);
let i = 0;
for (; i < lineCount - 1; i++) {
line.end = para.offset + ofs;
line.first = (j == 0);
line.last = false;
line.parts.push({style, text: ' ', image: {
local: part.image.local,
inline: false,
id: part.image.id,
imageLine: i,
lineCount,
paraIndex,
w: imageWidth,
h: imageHeight,
}});
lines.push(line);
line = {begin: line.end + 1, parts: []};
ofs++;
j++;
}
line.first = (j == 0);
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}
});
continue;
}
if (part.image.id && part.image.inline && this.showImages) {
const bin = this.binary[part.image.id];
if (bin) {
let imgH = (bin.h > this.fontSize ? this.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}});
}
}
let words = part.text.split(' ');
let sp1 = '';
let sp2 = '';
@@ -484,10 +823,11 @@ export default class BookParser {
str += sp1 + word;
let p = (j == 0 ? parsed.p : 0);
let p = (j == 0 ? parsed.p : 0) + imgW;
p = (style.space ? p + parsed.p*style.space : p);
let w = this.measureText(str, style) + p;
let wordTail = word;
if (w > parsed.w && prevStr != '') {
if (w > parsed.w + compactWidth && prevStr != '') {
if (parsed.wordWrap) {//по слогам
let slogi = this.splitToSlogi(word);
@@ -500,7 +840,7 @@ export default class BookParser {
for (let k = 0; k < slogiLen - 1; k++) {
let slog = slogi[0];
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
if (ww <= parsed.w) {
if (ww <= parsed.w + compactWidth) {
s += slog;
ss += slog;
} else
@@ -510,7 +850,6 @@ export default class BookParser {
}
if (pw) {
prevW = pw;
partText += ss + (ss[ss.length - 1] == '-' ? '' : '-');
wordTail = slogi.join('');
}
@@ -524,7 +863,6 @@ export default class BookParser {
let t = line.parts[line.parts.length - 1].text;
if (t[t.length - 1] == ' ') {
line.parts[line.parts.length - 1].text = t.trimRight();
prevW -= this.measureText(' ', style);
}
}
@@ -532,7 +870,6 @@ export default class BookParser {
if (line.end - line.begin < 0)
console.error(`Parse error, empty line in paragraph ${paraIndex}`);
line.width = prevW;
line.first = (j == 0);
line.last = false;
lines.push(line);
@@ -541,6 +878,7 @@ export default class BookParser {
partText = '';
sp2 = '';
str = wordTail;
imgW = 0;
j++;
}
@@ -548,7 +886,6 @@ export default class BookParser {
partText += sp2 + wordTail;
sp1 = ' ';
sp2 = ' ';
prevW = w;
}
if (partText != '')
@@ -560,14 +897,12 @@ export default class BookParser {
let t = line.parts[line.parts.length - 1].text;
if (t[t.length - 1] == ' ') {
line.parts[line.parts.length - 1].text = t.trimRight();
prevW -= this.measureText(' ', style);
}
line.end = para.offset + para.length - 1;
if (line.end - line.begin < 0)
console.error(`Parse error, empty line in paragraph ${paraIndex}`);
line.width = prevW;
line.first = (j == 0);
line.last = true;
lines.push(line);
@@ -581,6 +916,16 @@ export default class BookParser {
}
}
//parsed.visible
if (imageInPara) {
parsed.visible = this.showImages;
} else {
parsed.visible = !(
(para.addIndex > this.addEmptyParagraphs) ||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
);
}
parsed.lines = lines;
para.parsed = parsed;
@@ -614,16 +959,19 @@ export default class BookParser {
let paraIndex = this.findParaIndex(bookPos);
if (paraIndex === undefined)
return result;
return null;
if (n > 0) {
let parsed = this.parsePara(paraIndex);
let i = this.findLineIndex(bookPos, parsed.lines);
if (i === undefined)
return result;
return null;
while (n > 0) {
result.push(parsed.lines[i]);
if (parsed.visible) {
result.push(parsed.lines[i]);
n--;
}
i++;
if (i >= parsed.lines.length) {
@@ -631,21 +979,22 @@ export default class BookParser {
if (paraIndex < this.para.length)
parsed = this.parsePara(paraIndex);
else
return result;
break;
i = 0;
}
n--;
}
} else if (n < 0) {
n = -n;
let parsed = this.parsePara(paraIndex);
let i = this.findLineIndex(bookPos, parsed.lines);
if (i === undefined)
return result;
return null;
while (n > 0) {
result.push(parsed.lines[i]);
if (parsed.visible) {
result.push(parsed.lines[i]);
n--;
}
i--;
if (i < 0) {
@@ -653,16 +1002,15 @@ export default class BookParser {
if (paraIndex >= 0)
parsed = this.parsePara(paraIndex);
else
return result;
break;
i = parsed.lines.length - 1;
}
n--;
}
}
if (!result.length)
result = null;
return result;
}
}

View File

@@ -1,49 +1,147 @@
import localForage from 'localforage';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
const maxDataSize = 500*1024*1024;//chars, not bytes
const maxDataSize = 500*1024*1024;//compressed bytes
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
name: 'bmMetaStore'
});
//локальный кэш самих книг, ограничение maxDataSize
const bmDataStore = localForage.createInstance({
name: 'bmDataStore'
});
const bmRecentStore = localForage.createInstance({
//список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
class BookManager {
async init(settings) {
this.loaded = false;
this.settings = settings;
this.eventListeners = [];
this.books = {};
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 < 1000 ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300});
//загрузка bmRecentStore
this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
if (this.recentRev) {
this.recent = await bmRecentStoreNew.getItem('recent');
if (!this.recent)
this.recent = {};
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem;
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
await this.cleanRecentBooks();
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
{
await this.convertFileToDiskPrefix();
if (this.recentRev > 10)
await bmRecentStoreOld.clear();
}
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
let key = null;
const len = await bmRecentStoreOld.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStoreOld.key(i);
if (key) {
let r = await bmRecentStoreOld.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStoreOld.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await bmRecentStoreNew.setItem('recent', this.recent);
this.recentRev = 1;
await bmRecentStoreNew.setItem('rev', this.recentRev);
}
this.recentChanged = true;
this.loadStored();//no await
}
//Ленивая асинхронная загрузка bmMetaStore
async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
await utils.sleep(2000);
let len = await bmMetaStore.length();
for (let i = 0; i < len; i++) {
for (let i = len - 1; i >= 0; i--) {
const key = await bmMetaStore.key(i);
const keySplit = key.split('-');
if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
let meta = await bmMetaStore.getItem(key);
this.books[meta.key] = meta;
if (_.isObject(meta)) {
//уже может быть распарсена книга
const oldBook = this.books[meta.key];
this.books[meta.key] = meta;
if (oldBook && oldBook.parsed) {
this.books[meta.key].parsed = oldBook.parsed;
}
} else {
await bmMetaStore.removeItem(key);
}
}
}
len = await bmRecentStore.length();
for (let i = 0; i < len; i++) {
const key = await bmRecentStore.key(i);
let r = await bmRecentStore.getItem(key);
this.recent[r.key] = r;
}
await this.cleanBooks();
this.loaded = true;
this.emit('load-stored-finish');
}
async cleanBooks() {
@@ -53,7 +151,8 @@ class BookManager {
let toDel = null;
for (let key in this.books) {
let book = this.books[key];
size += (book.length ? book.length : 0);
const bookLength = (book.length ? book.length : 0);
size += (book.dataCompressedLength ? book.dataCompressedLength : bookLength);
if (book.addTime < min) {
toDel = book;
@@ -69,49 +168,173 @@ class BookManager {
}
}
async addBook(newBook, callback) {
if (!this.books)
await this.init();
async deflateWithProgress(data, callback) {
const chunkSize = 128*1024;
const deflator = new utils.pako.Deflate({level: 5});
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
let chunkNum = 0;
let perc = 0;
let prevPerc = 0;
for (var i = 0; i < data.length; i += chunkSize) {
if ((i + chunkSize) >= data.length) {
deflator.push(data.substring(i, i + chunkSize), true);
} else {
deflator.push(data.substring(i, i + chunkSize), false);
}
chunkNum++;
perc = Math.round(chunkNum/chunkTotal*100);
if (perc != prevPerc) {
callback(perc);
await utils.sleep(1);
prevPerc = perc;
}
}
if (deflator.err) {
throw new Error(deflator.msg);
}
callback(100);
return deflator.result;
}
async inflateWithProgress(data, callback) {
const chunkSize = 64*1024;
const inflator = new utils.pako.Inflate({to: 'string'});
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
let chunkNum = 0;
let perc = 0;
let prevPerc = 0;
for (var i = 0; i < data.length; i += chunkSize) {
if ((i + chunkSize) >= data.length) {
inflator.push(data.subarray(i, i + chunkSize), true);
} else {
inflator.push(data.subarray(i, i + chunkSize), false);
}
chunkNum++;
perc = Math.round(chunkNum/chunkTotal*100);
if (perc != prevPerc) {
callback(perc);
await utils.sleep(1);
prevPerc = perc;
}
}
if (inflator.err) {
throw new Error(inflator.msg);
}
callback(100);
return inflator.result;
}
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now();
const result = await this.parseBook(meta, newBook.data, callback);
const cb = (perc) => {
const p = Math.round(30*perc/100);
callback(p);
};
const cb2 = (perc) => {
const p = Math.round(30 + 65*perc/100);
callback(p);
};
const result = await this.parseBook(meta, newBook.data, cb);
result.dataCompressed = true;
let data = newBook.data;
if (result.dataCompressed) {
//data = utils.pako.deflate(data, {level: 5});
data = await this.deflateWithProgress(data, cb2);
result.dataCompressedLength = data.byteLength;
}
callback(95);
this.books[meta.key] = result;
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
await bmDataStore.setItem(`bmData-${meta.key}`, result.data);
await bmDataStore.setItem(`bmData-${meta.key}`, data);
callback(100);
return result;
}
hasBookParsed(meta) {
async hasBookParsed(meta) {
if (!this.books)
return false;
if (!meta.url)
return false;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
let book = this.books[meta.key];
if (!book && !this.loaded) {
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
if (book)
this.books[meta.key] = book;
}
return !!(book && book.parsed);
}
async getBook(meta, callback) {
if (!this.books)
await this.init();
let result = undefined;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
result = this.books[meta.key];
if (result && !result.data) {
result.data = await bmDataStore.getItem(`bmData-${meta.key}`);
this.books[meta.key] = result;
if (!result) {
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) {
result = await this.parseBook(result, result.data, callback);
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5);
await utils.sleep(10);
let cb = (perc) => {
const p = 5 + Math.round(15*perc/100);
callback(p);
};
if (result.dataCompressed) {
try {
//data = utils.pako.inflate(data, {to: 'string'});
data = await this.inflateWithProgress(data, cb);
} catch (e) {
this.delBook(meta);
throw e;
}
}
callback(20);
cb = (perc) => {
const p = 20 + Math.round(80*perc/100);
callback(p);
};
result = await this.parseBook(result, data, cb);
this.books[meta.key] = result;
}
@@ -119,9 +342,6 @@ class BookManager {
}
async delBook(meta) {
if (!this.books)
await this.init();
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
await bmDataStore.removeItem(`bmData-${meta.key}`);
@@ -129,15 +349,12 @@ class BookManager {
}
async parseBook(meta, data, callback) {
if (!this.books)
await this.init();
const parsed = new BookParser(this.settings);
const parsedMeta = await parsed.parse(data, callback);
const result = Object.assign({}, meta, parsedMeta, {
length: data.length,
textLength: parsed.textLength,
data,
parsed
});
@@ -146,7 +363,6 @@ class BookManager {
metaOnly(book) {
let result = Object.assign({}, book);
delete result.data;
delete result.parsed;
return result;
}
@@ -155,81 +371,199 @@ class BookManager {
return utils.stringToHex(url);
}
async setRecentBook(value, noTouch) {
if (!this.recent)
await this.init();
const result = Object.assign({}, value);
if (!noTouch)
Object.assign(result, {touchTime: Date.now()});
//-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev');
if (rev != this.recentRev && !skipCheck) {
const newRecent = await bmRecentStoreNew.getItem('recent');
Object.assign(this.recent, newRecent);
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
this.recentRev = rev;
}
if (result.textLength && !result.bookPos && result.bookPosPercent)
result.bookPos = Math.round(result.bookPosPercent*result.textLength);
const prevKey = (this.recentItem ? this.recentItem.key : '');
if (item) {
this.recent[item.key] = item;
this.recentItem = item;
} else {
this.recentItem = null;
}
this.recent[result.key] = result;
this.saveRecentItem();
await bmRecentStore.setItem(result.key, result);
await this.cleanRecentBooks();
if (!item || prevKey != item.key) {
this.saveRecent();
}
this.recentChanged = true;
if (item) {
this.emit('recent-changed', item.key);
} else {
this.emit('recent-changed');
}
}
async recentSetLastKey(key) {
this.recentLastKey = key;
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
}
async setRecentBook(value) {
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;
}
await this.recentSetLastKey(result.key);
await this.recentSetItem(result);
return result;
}
async getRecentBook(value) {
if (!this.recent)
await this.init();
return this.recent[value.key];
}
async delRecentBook(value) {
if (!this.recent)
await this.init();
const item = this.recent[value.key];
item.deleted = 1;
await bmRecentStore.removeItem(value.key);
delete this.recent[value.key];
this.recentChanged = true;
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
}
await this.recentSetItem(item);
this.emit('recent-deleted', value.key);
}
async cleanRecentBooks() {
if (!this.recent)
await this.init();
const sorted = this.getSortedRecent();
if (Object.keys(this.recent).length > 100) {
let min = Date.now();
let found = null;
for (let key in this.recent) {
const book = this.recent[key];
if (book.touchTime < min) {
min = book.touchTime;
found = book;
}
}
if (found) {
await this.delRecentBook(found);
await this.cleanRecentBooks();
}
let isDel = false;
for (let i = 1000; i < sorted.length; i++) {
delete this.recent[sorted[i].key];
isDel = true;
}
this.sortedRecentCached = null;
if (isDel)
await this.recentSetItem();
return isDel;
}
async convertFileToDiskPrefix() {
let isConverted = false;
const newRecent = {};
for (let key of Object.keys(this.recent)) {
let newKey = key;
let newUrl = this.recent[key].url;
if (newKey.indexOf('66696c65') == 0) {
newKey = newKey.replace(/^66696c65/, '6469736b');
if (newUrl)
newUrl = newUrl.replace(/^file/, 'disk');
isConverted = true;
}
newRecent[newKey] = this.recent[key];
newRecent[newKey].key = newKey;
if (newUrl)
newRecent[newKey].url = newUrl;
}
if (isConverted) {
this.recent = newRecent;
await this.recentSetItem(null, true);
}
return isConverted;
}
mostRecentBook() {
if (!this.recentChanged && this.mostRecentCached) {
return this.mostRecentCached;
if (this.recentLastKey) {
return this.recent[this.recentLastKey];
}
const oldKey = this.recentLastKey;
let max = 0;
let result = null;
for (let key in this.recent) {
const book = this.recent[key];
if (book.touchTime > max) {
if (!book.deleted && book.touchTime > max) {
max = book.touchTime;
result = book;
}
}
this.mostRecentCached = result;
const newRecentLastKey = (result ? result.key : null);
this.recentSetLastKey(newRecentLastKey);//no await
if (newRecentLastKey !== oldKey)
this.emit('recent-changed');
return result;
}
getSortedRecent() {
if (!this.recentChanged && this.sortedRecentCached) {
return this.sortedRecentCached;
}
let result = Object.values(this.recent);
result.sort((a, b) => b.touchTime - a.touchTime);
this.sortedRecentCached = result;
this.recentChanged = false;
return result;
}
async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent);
Object.assign(mergedRecent, value);
//подстраховка от hotReload
for (let i of Object.keys(mergedRecent)) {
if (!mergedRecent[i].key || mergedRecent[i].key !== i)
delete mergedRecent[i];
}
this.recent = mergedRecent;
await this.recentSetLastKey(null);
await this.recentSetItem(null, true);
this.emit('set-recent');
}
addEventListener(listener) {
if (this.eventListeners.indexOf(listener) < 0)
this.eventListeners.push(listener);
}
removeEventListener(listener) {
const i = this.eventListeners.indexOf(listener);
if (i >= 0)
this.eventListeners.splice(i, 1);
}
emit(eventName, value) {
if (this.eventListeners) {
for (const listener of this.eventListeners) {
//console.log(eventName);
listener(eventName, value);
}
}
}
}
export default new BookManager();

View File

@@ -1,70 +0,0 @@
export default async function restoreOldSettings(settings, bookManager, commit) {
const oldSets = localStorage['colorSetting'];
let isOld = false;
for (let i = 0; i < localStorage.length; i++) {
let key = unescape(localStorage.key(i));
if (key.indexOf('bpr-book-') == 0)
isOld = true;
}
if (isOld || oldSets) {
let newSettings = null;
if (oldSets) {
const [textColor, backgroundColor, lineStep, , , statusBarHeight, scInt] = unescape(oldSets).split('|');
const fontSize = Math.round(lineStep*0.8);
const scrollingDelay = fontSize*scInt;
newSettings = Object.assign({}, settings, {
textColor,
backgroundColor,
fontSize,
statusBarHeight: statusBarHeight*1,
scrollingDelay,
});
}
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
if (key.indexOf('bpr-') == 0) {
let v = unescape(localStorage[key]);
key = unescape(key);
if (key.lastIndexOf('=timestamp') == key.length - 10) {
continue;
}
if (key.indexOf('bpr-book-') == 0) {
const url = key.substr(9);
const [scrollTop, scrollHeight, ] = v.split('|');
const bookPosPercent = scrollTop*1/(scrollHeight*1 + 1);
const title = unescape(localStorage[`bpr-title-${escape(url)}`]);
const author = unescape(localStorage[`bpr-author-${escape(url)}`]);
const time = unescape(localStorage[`bpr-book-${escape(url)}=timestamp`]).split(';')[0];
const touchTime = Date.parse(time);
const bookKey = bookManager.keyFromUrl(url);
const recent = await bookManager.getRecentBook({key: bookKey});
if (!recent) {
await bookManager.setRecentBook({
key: bookKey,
touchTime,
bookPosPercent,
url,
fb2: {
bookTitle: title,
lastName: author,
}
}, true);
}
}
}
}
localStorage.clear();
if (oldSets)
commit('reader/setSettings', newSettings);
}
}

View File

@@ -0,0 +1,420 @@
export const versionHistory = [
{
showUntil: '2020-12-10',
header: '0.9.10 (2020-12-03)',
content:
`
<ul>
<li>добавлена частичная поддержка формата Djvu</li>
<li>добавлена поддержка Rar-архивов</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-11-20',
header: '0.9.9 (2020-11-21)',
content:
`
<ul>
<li>оптимизации, исправления багов</li>
</ul>
`
},
{
showUntil: '2020-11-12',
header: '0.9.8 (2020-11-13)',
content:
`
<ul>
<li>добавлено окно "Оглавление/закладки"</li>
</ul>
`
},
{
showUntil: '2020-11-11',
header: '0.9.7 (2020-11-12)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content:
`
<ul>
<li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)',
content:
`
<ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-10-28',
header: '0.9.4 (2020-10-29)',
content:
`
<ul>
<li>заработал новый сайт <a href="https://liberama.top">https://liberama.top</a>, где будет более свободный обмен книгами</li>
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-05-20',
header: '0.9.3 (2020-05-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content:
`
<ul>
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content:
`
<ul>
<li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',
content:
`
<ul>
<li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li>
</ul>
`
},
{
showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content:
`
<ul>
<li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content:
`
<ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content:
`
<ul>
<li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li>
</ul>
`
},
{
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>
`
},
{
showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content:
`
<ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)',
content:
`
<ul>
<li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-11-10',
header: '0.7.7 (2019-11-06)',
content:
`
<ul>
<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>
</ul>
</ul>
`
},
{
showUntil: '2019-10-29',
header: '0.7.6 (2019-10-30)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-10-21',
header: '0.7.5 (2019-10-22)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-10-17',
header: '0.7.3 (2019-10-18)',
content:
`
<ul>
<li>внутренние переделки механизма синхронизации с сервером</li>
<li>добавлен html-фильтр для сайтов www.fanfiction.net, archiveofourown.org</li>
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content:
`
<ul>
<li>исправления багов</li>
<li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li>
</ul>
`
},
{
showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content:
`
<ul>
<li>налажена работа https-версии сайта, рекомендуется плавный переход</li>
<li>добавлена возможность загрузки и работы https-версии читалки в оффлайн-режиме (при отсутствии интернета)</li>
<li>упрощение механизма серверной синхронизации с целью повышения надежности и избавления от багов</li>
<li>окна теперь можно перемещать за заголовок</li>
<li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul>
`
},
{
showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content:
`
<ul>
<li>исправлен баг - падение сервера при распаковке битых архивов книг</li>
<li>исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8</li>
<li>добавлены новые варианты анимации перелистывания</li>
<li>на страницу загрузки добавлен блок "Поделиться"</li>
<li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li>
</ul>
`
},
{
showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content:
`
<ul>
<li>добавлен диалог "Что нового"</li>
<li>в справку добавлена история версий проекта</li>
<li>добавлена возможность настройки отображаемых кнопок на панели управления</li>
<li>некоторые кнопки на панели управления были скрыты по умолчанию</li>
<li>на страницу загрузки добавлена возможность загрузки книги из буфера обмена</li>
<li>добавлен GET-параметр вида "/reader?__refresh=1&url=..." для принудительного обновления загружаемого текста</li>
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li>
</ul>
`
},
{
showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content:
`
<ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul>
`
},
{
showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content:
`
<ul>
<li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li>
</ul>
`
},
{
showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content:
`
<ul>
<li>добавлена поддержка форматов pdf, epub, mobi</li>
<li>(0.5.2) добавлена поддержка форматов rtf, doc, docx</li>
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul>
`
},
{
showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content:
`
<ul>
<li>поправки багов</li>
<li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul>
`
},
{
showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content:
`
<ul>
<li>увеличены верхние границы отступов и др.размеров</li>
<li>добавлена настройка для удаления/вставки пустых параграфов</li>
<li>добавлена настройка включения/отключения управления кликом</li>
<li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul>
`
},
{
showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content:
`
<ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul>
`
},
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,342 @@
<template>
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
<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="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'confirm'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'prompt'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn flat round dense v-close-popup>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
<div class="q-my-md text-center">
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
<div>{{ hotKeyCode }}</div>
</div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
</div>
</div>
</q-dialog>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import * as utils from '../../share/utils';
export default @Component({
watch: {
inputValue: function(newValue) {
this.validate(newValue);
},
}
})
class StdDialog extends Vue {
caption = '';
message = '';
active = false;
type = '';
inputValue = '';
error = '';
iconColor = '';
iconName = '';
hotKeyCode = '';
created() {
if (this.$root.addKeyHook) {
this.$root.addKeyHook(this.keyHook);
}
}
init(message, caption, opts) {
this.caption = caption;
this.message = message;
this.ok = false;
this.type = '';
this.inputValidator = null;
this.inputValue = '';
this.error = '';
this.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;
}
}
onHide() {
if (this.hideTrigger) {
this.hideTrigger();
this.hideTrigger = null;
}
this.showed = false;
}
onShow() {
if (this.type == 'prompt') {
this.enableValidator = true;
if (this.inputValue)
this.validate(this.inputValue);
this.$refs.input.focus();
}
this.showed = true;
}
validate(value) {
if (!this.enableValidator)
return false;
if (this.inputValidator) {
const result = this.inputValidator(value);
if (result !== true) {
this.error = result;
return false;
}
}
this.error = '';
return true;
}
okClick() {
if (this.type == 'prompt' && !this.validate(this.inputValue)) {
this.$refs.dialog.shake();
return;
}
if (this.type == 'hotKey' && this.hotKeyCode == '') {
this.$refs.dialog.shake();
return;
}
this.ok = true;
this.$refs.dialog.hide();
}
alert(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'alert';
this.active = true;
});
}
confirm(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'confirm';
this.active = true;
});
}
prompt(message, caption, opts) {
return new Promise((resolve) => {
this.enableValidator = false;
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve({value: this.inputValue});
} else {
resolve(false);
}
};
this.type = 'prompt';
if (opts) {
this.inputValidator = opts.inputValidator || null;
this.inputValue = opts.inputValue || '';
}
this.active = true;
});
}
getHotKey(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(this.hotKeyCode);
} else {
resolve(false);
}
};
this.type = 'hotKey';
this.active = true;
});
}
keyHook(event) {
if (this.active && this.showed) {
let handled = false;
if (this.type == 'hotKey') {
if (event.type == 'keydown') {
this.hotKeyCode = utils.keyEventToCode(event);
handled = true;
}
} else {
if (event.key == 'Enter') {
this.okClick();
handled = true;
}
if (event.key == 'Escape') {
this.$nextTick(() => {
this.$refs.dialog.hide();
});
handled = true;
}
}
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.header {
height: 50px;
}
.caption {
font-size: 110%;
overflow: hidden;
}
.close-icon {
width: 50px;
}
.buttons {
height: 60px;
}
.error {
height: 20px;
font-size: 80%;
color: red;
}
</style>

View File

@@ -1,10 +1,17 @@
<template>
<div class="window">
<div class="header">
<span class="header-text"><slot name="header"></slot></span>
<span class="close-button" @click="close"><i class="el-icon-close"></i></span>
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
<div ref="window" class="window flexfit column no-wrap">
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
<span class="header-text col"><slot name="header"></slot></span>
<slot name="buttons"></slot>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
</div>
<slot></slot>
</div>
</div>
<slot></slot>
</div>
</template>
@@ -14,21 +21,124 @@ import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
props: {
height: { type: String, default: '100%' },
width: { type: String, default: '100%' },
maxWidth: { type: String, default: '' },
topShift: { type: Number, default: 0 },
margin: '',
}
})
class Window extends Vue {
close() {
this.$emit('close');
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)
this.$refs.windowBox.style.maxWidth = this.maxWidth;
const left = (this.$refs.main.offsetWidth - this.$refs.windowBox.offsetWidth)/2;
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)
return;
if (event.button == 0) {
this.$refs.header.style.cursor = 'move';
this.startX = event.screenX;
this.startY = event.screenY;
this.moving = true;
}
}
onMouseUp(event) {
if (event.button == 0) {
this.$refs.header.style.cursor = 'default';
this.moving = false;
}
}
onMouseMove(event) {
if (this.moving) {
const deltaX = event.screenX - this.startX;
const deltaY = event.screenY - this.startY;
this.startX = event.screenX;
this.startY = event.screenY;
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
}
}
onTouchStart(event) {
if (!this.$isMobileDevice)
return;
if (event.touches.length == 1) {
const touch = event.touches[0];
this.$refs.header.style.cursor = 'move';
this.startX = touch.screenX;
this.startY = touch.screenY;
this.moving = true;
}
}
onTouchMove(event) {
if (!this.$isMobileDevice)
return;
if (event.touches.length == 1 && this.moving) {
const touch = event.touches[0];
const deltaX = touch.screenX - this.startX;
const deltaY = touch.screenY - this.startY;
this.startX = touch.screenX;
this.startY = touch.screenY;
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
}
}
onTouchEnd() {
if (!this.$isMobileDevice)
return;
this.$refs.header.style.cursor = 'default';
this.moving = false;
}
close() {
if (!this.moving)
this.$emit('close');
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.window {
.main {
background-color: transparent !important;
z-index: 50;
}
.xyfit {
height: 100%;
width: 100%;
}
.flexfit {
flex: 1;
display: flex;
flex-direction: column;
}
.window {
margin: 10px;
background-color: #ffffff;
border: 3px double black;
@@ -37,25 +147,28 @@ class Window extends Vue {
}
.header {
display: flex;
justify-content: flex-end;
background-color: #e5e7ea;
background: linear-gradient(to bottom right, green, #59B04F);
align-items: center;
height: 40px;
height: 30px;
}
.header-text {
flex: 1;
margin-left: 10px;
margin-right: 10px;
color: yellow;
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
overflow: hidden;
white-space: nowrap;
}
.close-button {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
width: 30px;
height: 30px;
cursor: pointer;
}
.close-button:hover {
background-color: #69C05F;
}
</style>

View File

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

View File

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

View File

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

93
client/quasar.js Normal file
View File

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

View File

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

View File

@@ -0,0 +1,26 @@
//WebCrypto API (crypto.subtle) не работает без https, поэтому приходится извращаться через sjcl
import sjclWrapper from './sjclWrapper';
//не менять
const iv = 'B6E2XejNh2dS';
const salt = 'Liberama project is awesome';
export function aesEncrypt(data, password) {
return sjclWrapper.codec.bytes.fromBits(
sjclWrapper.encryptArray(
password, sjclWrapper.codec.bytes.toBits(data), {iv, salt}
).ct
);
}
export function aesDecrypt(data, password) {
return sjclWrapper.codec.bytes.fromBits(
sjclWrapper.decryptArray(
password, {ct: sjclWrapper.codec.bytes.toBits(data)}, {iv, salt}
)
);
}
export function sha256(str) {
return sjclWrapper.codec.bytes.fromBits(sjclWrapper.hash.sha256.hash(str));
}

60
client/share/sjcl.js Normal file
View File

@@ -0,0 +1,60 @@
"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c&
255]]};
sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e=
0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}};
function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m<n;m++)h=a[e>>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m=
0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r}
sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<<c)-1},concat:function(a,b){if(0===a.length||0===b.length)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return 32===d?a.concat(b):sjcl.bitArray.$(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;return 0===
b?0:32*(b-1)+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(32*a.length<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b=b&31;0<c&&b&&(a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d<a.length;d++)c|=a[d]^b[d];return 0===
c},$:function(a,b,c,d){var e;e=0;for(void 0===d&&(d=[]);32<=b;b-=32)d.push(c),c=0;if(0===b)return d.concat(a);for(e=0;e<a.length;e++)d.push(c|a[e]>>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32<b+a?c:d.pop(),1));return d},i:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]},byteswapM:function(a){var b,c;for(b=0;b<a.length;++b)c=a[b],a[b]=c>>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}};
sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d<c/8;d++)0===(d&3)&&(e=a[d/4]),b+=String.fromCharCode(e>>>8>>>8>>>8),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++)d=d<<8|a.charCodeAt(c),3===(c&3)&&(b.push(d),d=0);c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a=a+"00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,4*d)}};
sjcl.codec.base32={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",X:"0123456789ABCDEFGHIJKLMNOPQRSTUV",BITS:32,BASE:5,REMAINING:27,fromBits:function(a,b,c){var d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f="",g=0,h=sjcl.codec.base32.B,k=0,l=sjcl.bitArray.bitLength(a);c&&(h=sjcl.codec.base32.X);for(c=0;f.length*d<l;)f+=h.charAt((k^a[c]>>>g)>>>e),g<d?(k=a[c]<<d-g,g+=e,c++):(k<<=d,g-=d);for(;f.length&7&&!b;)f+="=";return f},toBits:function(a,b){a=a.replace(/\s|=/g,"").toUpperCase();var c=sjcl.codec.base32.BITS,
d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f=[],g,h=0,k=sjcl.codec.base32.B,l=0,n,m="base32";b&&(k=sjcl.codec.base32.X,m="base32hex");for(g=0;g<a.length;g++){n=k.indexOf(a.charAt(g));if(0>n){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<<c-h):(h+=d,l^=n<<c-h)}h&56&&f.push(sjcl.bitArray.partial(h&56,l,1));return f}};
sjcl.codec.base32hex={fromBits:function(a,b){return sjcl.codec.base32.fromBits(a,b,1)},toBits:function(a){return sjcl.codec.base32.toBits(a,1)}};
sjcl.codec.base64={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.B,g=0,h=sjcl.bitArray.bitLength(a);c&&(f=f.substr(0,62)+"-_");for(c=0;6*d.length<h;)d+=f.charAt((g^a[c]>>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;d<a.length;d++){h=f.indexOf(a.charAt(d));
if(0>h)throw new sjcl.exception.invalid("this isn't base64!");26<e?(e-=26,c.push(g^h>>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffff<a)throw new sjcl.exception.invalid("Cannot hash more than 2^53 - 1 bits");if("undefined"!==typeof Uint32Array){var d=new Uint32Array(c),e=0;for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,d.subarray(16*e,
16*(e+1))),e+=1;c.splice(0,16*e)}else for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,c.splice(0,16));return this},finalize:function(){var a,b=this.A,c=this.F,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.l/0x100000000));for(b.push(this.l|0);b.length;)u(this,b.splice(0,16));this.reset();return c},Y:[],b:[],O:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}for(var b=0,c=2,d,e;64>b;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e=
!1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}};
function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]=
f[7]+q|0}
sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1<a&&sjcl.mode.ccm.G.splice(a,1)},fa:function(a){var b=sjcl.mode.ccm.G.slice(),c;for(c=0;c<b.length;c+=1)b[c](a)},encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,k=h.bitLength(c)/8,l=h.bitLength(g)/8;e=e||64;d=d||[];if(7>k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c,
8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");
return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;b<g.length;b+=4)d=a.encrypt(k(d,g.slice(b,b+4).concat([0,0,0])));return d},V:function(a,b,c,d,e,f){var g=sjcl.bitArray,h=g.i;e/=8;if(e%2||4>e||16<e)throw new sjcl.exception.invalid("ccm: invalid tag length");
if(0xffffffff<d.length||0xffffffff<b.length)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");c=sjcl.mode.ccm.na(a,d,c,e,g.bitLength(b)/8,f);for(d=0;d<b.length;d+=4)c=a.encrypt(h(c,b.slice(d,d+4).concat([0,0,0])));return g.clamp(c,8*e)},C:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.i;var k=b.length,l=h.bitLength(b),n=k/50,m=n;c=h.concat([h.partial(8,f-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(g(d,a.encrypt(c)),0,e);if(!k)return{tag:d,data:[]};for(g=0;g<k;g+=4)g>n&&(sjcl.mode.ccm.fa(g/
k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}};
sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4<b.length;g+=4)m=b.slice(g,g+4),n=l(n,m),p=p.concat(l(c,a.encrypt(l(c,m)))),c=h(c);m=b.slice(g);b=k.bitLength(m);g=a.encrypt(l(c,[0,0,0,b]));m=k.clamp(l(m.concat([0,0,0]),g),b);n=l(n,l(m.concat([0,0,0]),g));n=a.encrypt(l(n,l(c,h(c))));
d.length&&(n=l(n,f?d:sjcl.mode.ocb2.pmac(a,d)));return p.concat(k.concat(m,k.clamp(n,e)))},decrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.S,h=sjcl.bitArray,k=h.i,l=[0,0,0,0],n=g(a.encrypt(c)),m,p,r=sjcl.bitArray.bitLength(b)-e,q=[];d=d||[];for(c=0;c+4<r/32;c+=4)m=k(n,a.decrypt(k(n,b.slice(c,c+4)))),l=k(l,m),q=q.concat(m),n=g(n);p=r-32*c;m=a.encrypt(k(n,[0,0,0,p]));m=k(m,h.clamp(b.slice(c),p).concat([0,
0,0]));l=k(l,m);l=a.encrypt(k(l,k(n,g(n))));d.length&&(l=k(l,f?d:sjcl.mode.ocb2.pmac(a,d)));if(!h.equal(h.clamp(l,e),h.bitSlice(b,r)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return q.concat(h.clamp(m,p))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.S,e=sjcl.bitArray,f=e.i,g=[0,0,0,0],h=a.encrypt([0,0,0,0]),h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4)h=d(h),g=f(g,a.encrypt(f(h,b.slice(c,c+4))));c=b.slice(c);128>e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c);
return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}};
sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0,
0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0<d;d--)f[d]=f[d]>>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;d<e;d+=4)b[0]^=0xffffffff&c[d],b[1]^=0xffffffff&c[d+1],b[2]^=0xffffffff&c[d+2],b[3]^=0xffffffff&c[d+3],b=sjcl.mode.gcm.ka(b,a);return b},C:function(a,b,c,d,e,f){var g,h,k,l,n,m,p,r,q=sjcl.bitArray;m=c.length;p=q.bitLength(c);r=q.bitLength(d);h=q.bitLength(e);
g=b.encrypt([0,0,0,0]);96===h?(e=e.slice(0),e=q.concat(e,[1])):(e=sjcl.mode.gcm.j(g,[0,0,0,0],e),e=sjcl.mode.gcm.j(g,e,[0,0,Math.floor(h/0x100000000),h&0xffffffff]));h=sjcl.mode.gcm.j(g,[0,0,0,0],d);n=e.slice(0);d=h.slice(0);a||(d=sjcl.mode.gcm.j(g,h,c));for(l=0;l<m;l+=4)n[3]++,k=b.encrypt(n),c[l]^=k[0],c[l+1]^=k[1],c[l+2]^=k[2],c[l+3]^=k[3];c=q.clamp(c,p);a&&(d=sjcl.mode.gcm.j(g,h,c));a=[Math.floor(r/0x100000000),r&0xffffffff,Math.floor(p/0x100000000),p&0xffffffff];d=sjcl.mode.gcm.j(g,d,a);k=b.encrypt(e);
d[0]^=k[0];d[1]^=k[1];d[2]^=k[2];d[3]^=k[3];return{tag:q.bitSlice(d,0,f),data:c}}};sjcl.misc.hmac=function(a,b){this.W=b=b||sjcl.hash.sha256;var c=[[],[]],d,e=b.prototype.blockSize/32;this.w=[new b,new b];a.length>e&&(a=b.hash(a));for(d=0;d<e;d++)c[0][d]=a[d]^909522486,c[1][d]=a[d]^1549556828;this.w[0].update(c[0]);this.w[1].update(c[1]);this.R=new b(this.w[0])};
sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a){if(this.aa)throw new sjcl.exception.invalid("encrypt on already updated hmac called!");this.update(a);return this.digest(a)};sjcl.misc.hmac.prototype.reset=function(){this.R=new this.W(this.w[0]);this.aa=!1};sjcl.misc.hmac.prototype.update=function(a){this.aa=!0;this.R.update(a)};sjcl.misc.hmac.prototype.digest=function(){var a=this.R.finalize(),a=(new this.W(this.w[1])).update(a).finalize();this.reset();return a};
sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E4;if(0>d||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;g<c;g++)for(f=a.encrypt(f),h=0;h<f.length;h++)e[h]^=f[h];l=l.concat(e)}d&&(l=n.clamp(l,d));return l};
sjcl.prng=function(a){this.c=[new sjcl.hash.sha256];this.m=[0];this.P=0;this.H={};this.N=0;this.U={};this.Z=this.f=this.o=this.ha=0;this.b=[0,0,0,0,0,0,0,0];this.h=[0,0,0,0];this.L=void 0;this.M=a;this.D=!1;this.K={progress:{},seeded:{}};this.u=this.ga=0;this.I=1;this.J=2;this.ca=0x10000;this.T=[0,48,64,96,128,192,0x100,384,512,768,1024];this.da=3E4;this.ba=80};
sjcl.prng.prototype={randomWords:function(a,b){var c=[],d;d=this.isReady(b);var e;if(d===this.u)throw new sjcl.exception.notReady("generator isn't seeded");if(d&this.J){d=!(d&this.I);e=[];var f=0,g;this.Z=e[0]=(new Date).valueOf()+this.da;for(g=0;16>g;g++)e.push(0x100000000*Math.random()|0);for(g=0;g<this.c.length&&(e=e.concat(this.c[g].finalize()),f+=this.m[g],this.m[g]=0,d||!(this.P&1<<g));g++);this.P>=1<<this.c.length&&(this.c.push(new sjcl.hash.sha256),this.m.push(0));this.f-=f;f>this.o&&(this.o=
f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d<a;d+=4)0===(d+1)%this.ca&&y(this),e=z(this),c.push(e[0],e[1],e[2],e[3]);y(this);return c.slice(0,a)},setDefaultParanoia:function(a,b){if(0===a&&"Setting paranoia=0 will ruin your security; use it only for testing"!==b)throw new sjcl.exception.invalid("Setting paranoia=0 will ruin your security; use it only for testing");this.M=a},addEntropy:function(a,
b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.H[c],h=this.isReady(),k=0;d=this.U[c];void 0===d&&(d=this.U[c]=this.ha++);void 0===g&&(g=this.H[c]=0);this.H[c]=(this.H[c]+1)%this.c.length;switch(typeof a){case "number":void 0===b&&(b=1);this.c[g].update([d,this.N++,1,b,f,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if("[object Uint32Array]"===c){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else for("[object Array]"!==c&&(k=1),c=0;c<a.length&&!k;c++)"number"!==typeof a[c]&&
(k=1);if(!k){if(void 0===b)for(c=b=0;c<a.length;c++)for(e=a[c];0<e;)b++,e=e>>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))},
isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load",
this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event");
this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove",
this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;c<f.length;c++)d=f[c],delete e[d]},la:function(){C(this,1)},oa:function(a){var b,c;try{b=a.x||a.clientX||a.offsetX||0,c=a.y||a.clientY||a.offsetY||0}catch(d){c=b=0}0!=b&&0!=c&&this.addEntropy([b,c],2,"mouse");C(this,0)},qa:function(a){a=
a.touches[0]||a.changedTouches[0];this.addEntropy([a.pageX||a.clientX,a.pageY||a.clientY],1,"touch");C(this,0)},ma:function(){C(this,2)},ea:function(a){a=a.accelerationIncludingGravity.x||a.accelerationIncludingGravity.y||a.accelerationIncludingGravity.z;if(window.orientation){var b=window.orientation;"number"===typeof b&&this.addEntropy(b,1,"accelerometer")}a&&this.addEntropy(a,2,"accelerometer");C(this,0)}};
function A(a,b){var c,d=sjcl.random.K[a],e=[];for(c in d)d.hasOwnProperty(c)&&e.push(d[c]);for(c=0;c<e.length;c++)e[c](b)}function C(a,b){"undefined"!==typeof window&&window.performance&&"function"===typeof window.performance.now?a.addEntropy(window.performance.now(),b,"loadtime"):a.addEntropy((new Date).valueOf(),b,"loadtime")}function y(a){a.b=z(a).concat(z(a));a.L=new sjcl.cipher.aes(a.b)}function z(a){for(var b=0;4>b&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)}
function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6);
a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=null}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F);
else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))}
sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length||
4<f.iv.length)throw new sjcl.exception.invalid("json encrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,f),a=g.key.slice(0,f.ks/32),f.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.publicKey&&(g=a.kem(),f.kemtag=g.tag,a=g.key.slice(0,f.ks/32));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));"string"===typeof c&&(f.adata=c=sjcl.codec.utf8String.toBits(c));g=new sjcl.cipher[f.cipher](a);e.g(d,f);d.key=a;f.ct="ccm"===f.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&
b instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.encrypt(g,b,f.iv,c,f.ts):sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return f},encrypt:function(a,b,c,d){var e=sjcl.json,f=e.ja.apply(e,arguments);return e.encode(f)},ia:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.g(e.g(e.g({},e.defaults),b),c,!0);var f,g;f=b.adata;"string"===typeof b.salt&&(b.salt=sjcl.codec.base64.toBits(b.salt));"string"===typeof b.iv&&(b.iv=sjcl.codec.base64.toBits(b.iv));if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||"string"===
typeof a&&100>=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4<b.iv.length)throw new sjcl.exception.invalid("json decrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,b),a=g.key.slice(0,b.ks/32),b.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.secretKey&&(a=a.unkem(sjcl.codec.base64.toBits(b.kemtag)).slice(0,b.ks/32));"string"===typeof f&&(f=sjcl.codec.utf8String.toBits(f));g=new sjcl.cipher[b.cipher](a);f="ccm"===
b.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&b.ct instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.decrypt(g,b.ct,b.iv,b.tag,f,b.ts):sjcl.mode[b.mode].decrypt(g,b.ct,b.iv,f,b.ts);e.g(d,b);d.key=a;return 1===c.raw?f:sjcl.codec.utf8String.fromBits(f)},decrypt:function(a,b,c,d){var e=sjcl.json;return e.ia(a,e.decode(b),c,d)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+
b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],0)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^\s*(?:(["']?)([a-z][a-z0-9]*)\1)\s*:\s*(?:(-?\d+)|"([a-z0-9+\/%*_.@=\-]*)"|(true|false))$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");
null!=d[3]?b[d[2]]=parseInt(d[3],10):null!=d[4]?b[d[2]]=d[2].match(/^(ct|adata|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4]):null!=d[5]&&(b[d[2]]="true"===d[5])}return b},g:function(a,b,c){void 0===a&&(a={});if(void 0===b)return a;for(var d in b)if(b.hasOwnProperty(d)){if(c&&void 0!==a[d]&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},sa:function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&a[d]!==b[d]&&(c[d]=a[d]);return c},ra:function(a,
b){var c={},d;for(d=0;d<b.length;d++)void 0!==a[b[d]]&&(c[b[d]]=a[b[d]]);return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.pa={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.pa,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=void 0===b.salt?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
"undefined"!==typeof module&&module.exports&&(module.exports=sjcl);"function"===typeof define&&define([],function(){return sjcl});

146
client/share/sjclWrapper.js Normal file
View File

@@ -0,0 +1,146 @@
//sjcl.js подправлен (убран лишний require, добавлявший +400kb к bundle) и скопирован локально
import sjcl from './sjcl';
//везде недоработки...
sjcl.codec.bytes = {
fromBits: function(arr) {
var out = [], bl = sjcl.bitArray.bitLength(arr), i, tmp;
for (i=0; i<bl/8; i++) {
if ((i&3) === 0) {
tmp = arr[i/4];
}
out.push(tmp >>> 24);
tmp <<= 8;
}
return out;
},
toBits: function(bytes) {
var out = [], i, tmp=0;
for (i=0; i<bytes.length; i++) {
tmp = tmp << 8 | bytes[i];
if ((i&3) === 3) {
out.push(tmp);
tmp = 0;
}
}
if (i&3) {
out.push(sjcl.bitArray.partial(8*(i&3), tmp));
}
return out;
}
};
sjcl.json._add = function(target, src, requireSame) {
if (target === undefined) { target = {}; }
if (src === undefined) { return target; }
var i;
for (i in src) {
if (src.hasOwnProperty(i)) {
if (requireSame && target[i] !== undefined && target[i] !== src[i]) {
throw new sjcl.exception.invalid("required parameter overridden");
}
target[i] = src[i];
}
}
return target;
}
sjcl.encryptArray = function(password, plaintext, params, rp) {
params = params || {};
rp = rp || {};
var j = sjcl.json, p = j._add({ iv: sjcl.random.randomWords(4,0) },
j.defaults), tmp, prp, adata;
j._add(p, params);
adata = p.adata;
if (typeof p.salt === "string") {
p.salt = sjcl.codec.base64.toBits(p.salt);
}
if (typeof p.iv === "string") {
p.iv = sjcl.codec.base64.toBits(p.iv);
}
if (!sjcl.mode[p.mode] ||
!sjcl.cipher[p.cipher] ||
(typeof password === "string" && p.iter <= 100) ||
(p.ts !== 64 && p.ts !== 96 && p.ts !== 128) ||
(p.ks !== 128 && p.ks !== 192 && p.ks !== 256) ||
(p.iv.length < 2 || p.iv.length > 4)) {
throw new sjcl.exception.invalid("json encrypt: invalid parameters");
}
if (typeof password === "string") {
tmp = sjcl.misc.cachedPbkdf2(password, p);
password = tmp.key.slice(0,p.ks/32);
p.salt = tmp.salt;
} else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.publicKey) {
tmp = password.kem();
p.kemtag = tmp.tag;
password = tmp.key.slice(0,p.ks/32);
}
if (typeof plaintext === "string") {
plaintext = sjcl.codec.utf8String.toBits(plaintext);
}
if (typeof adata === "string") {
p.adata = adata = sjcl.codec.utf8String.toBits(adata);
}
prp = new sjcl.cipher[p.cipher](password);
j._add(rp, p);
rp.key = password;
/* do the encryption */
if (p.mode === "ccm" && sjcl.arrayBuffer && sjcl.arrayBuffer.ccm && plaintext instanceof ArrayBuffer) {
p.ct = sjcl.arrayBuffer.ccm.encrypt(prp, plaintext, p.iv, adata, p.ts);
} else {
p.ct = sjcl.mode[p.mode].encrypt(prp, plaintext, p.iv, adata, p.ts);
}
return p;
}
sjcl.decryptArray = function(password, ciphertext, params) {
params = params || {};
var j = sjcl.json, p = j._add(j._add(j._add({},j.defaults),ciphertext), params, true), ct, tmp, prp, adata=p.adata;
if (typeof p.salt === "string") {
p.salt = sjcl.codec.base64.toBits(p.salt);
}
if (typeof p.iv === "string") {
p.iv = sjcl.codec.base64.toBits(p.iv);
}
if (!sjcl.mode[p.mode] ||
!sjcl.cipher[p.cipher] ||
(typeof password === "string" && p.iter <= 100) ||
(p.ts !== 64 && p.ts !== 96 && p.ts !== 128) ||
(p.ks !== 128 && p.ks !== 192 && p.ks !== 256) ||
(!p.iv) ||
(p.iv.length < 2 || p.iv.length > 4)) {
throw new sjcl.exception.invalid("json decrypt: invalid parameters");
}
if (typeof password === "string") {
tmp = sjcl.misc.cachedPbkdf2(password, p);
password = tmp.key.slice(0,p.ks/32);
p.salt = tmp.salt;
} else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.secretKey) {
password = password.unkem(sjcl.codec.base64.toBits(p.kemtag)).slice(0,p.ks/32);
}
if (typeof adata === "string") {
adata = sjcl.codec.utf8String.toBits(adata);
}
prp = new sjcl.cipher[p.cipher](password);
/* do the decryption */
if (p.mode === "ccm" && sjcl.arrayBuffer && sjcl.arrayBuffer.ccm && p.ct instanceof ArrayBuffer) {
ct = sjcl.arrayBuffer.ccm.decrypt(prp, p.ct, p.iv, p.tag, adata, p.ts);
} else {
ct = sjcl.mode[p.mode].decrypt(prp, p.ct, p.iv, adata, p.ts);
}
return ct;
}
export default sjcl;

View File

@@ -1,21 +1,34 @@
import _ from 'lodash';
import baseX from 'base-x';
import PAKO from 'pako';
import {Buffer} from 'safe-buffer';
import sjclWrapper from './sjclWrapper';
export const pako = PAKO;
const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const bs58 = baseX(BASE58);
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function stringToHex(str) {
let result = '';
for (let i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16);
}
return result;
return Buffer.from(str).toString('hex');
}
export function hexToString(str) {
let result = '';
for (let i = 0; i < str.length; i += 2) {
result += String.fromCharCode(parseInt(str.substr(i, 2), 16));
}
return result;
return Buffer.from(str, 'hex').toString();
}
export function randomArray(len) {
const a = new Uint8Array(len);
window.crypto.getRandomValues(a);
return a;
}
export function randomHexString(len) {
return Buffer.from(randomArray(len)).toString('hex');
}
export function formatDate(d, format) {
@@ -26,6 +39,10 @@ export function formatDate(d, format) {
case 'normal':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` +
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
case 'coDate':
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
case 'noDate':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
}
}
@@ -61,5 +78,281 @@ export async function copyTextToClipboard(text) {
//
}
return result;
}
export function toBase58(data) {
return bs58.encode(Buffer.from(data));
}
export function fromBase58(data) {
return bs58.decode(data);
}
//base-x слишком тормозит, используем sjcl
export function toBase64(data) {
return sjclWrapper.codec.base64.fromBits(
sjclWrapper.codec.bytes.toBits(Buffer.from(data))
);
}
//base-x слишком тормозит, используем sjcl
export function fromBase64(data) {
return Buffer.from(sjclWrapper.codec.bytes.fromBits(
sjclWrapper.codec.base64.toBits(data)
));
}
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 (newObj.hasOwnProperty(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);
}
}
for (const key of Object.keys(newObj)) {
const kp = `${keyPath}${key}`;
if (exAdd.has(kp))
continue;
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 && diff.change && diff.add && diff.del);
}
export function isEmptyObjDiff(diff) {
return (!isObjDiff(diff) ||
!(Object.keys(diff.change).length ||
Object.keys(diff.add).length ||
diff.del.length
)
);
}
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 (_.isObject(change[key])) {
result[key] = applyObjDiff(result[key], change[key], opts);
} else {
if (isApplyChange)
result[key] = _.cloneDeep(change[key]);
}
} else if (isAddChanged) {
result[key] = _.cloneDeep(change[key]);
}
}
if (isApplyAdd) {
for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[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;
}
export function parseQuery(str) {
if (typeof str != 'string' || str.length == 0)
return {};
let s = str.split('&');
let s_length = s.length;
let bit, query = {}, first, second;
for (let i = 0; i < s_length; i++) {
bit = s[i].split('=');
first = decodeURIComponent(bit[0]);
if (first.length == 0)
continue;
second = decodeURIComponent(bit[1]);
if (typeof query[first] == 'undefined')
query[first] = second;
else
if (query[first] instanceof Array)
query[first].push(second);
else
query[first] = [query[first], second];
}
return query;
}
export function escapeXml(str) {
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
;
}
export function keyEventToCode(event) {
let result = [];
let code = event.code;
const modCode = code.substring(0, 3);
if (event.metaKey && modCode != 'Met')
result.push('Meta');
if (event.ctrlKey && modCode != 'Con')
result.push('Ctrl');
if (event.shiftKey && modCode != 'Shi')
result.push('Shift');
if (event.altKey && modCode != 'Alt')
result.push('Alt');
if (modCode == 'Dig') {
code = code.substring(5, 6);
} else if (modCode == 'Key') {
code = code.substring(3, 4);
}
result.push(code);
return result.join('+');
}
export function userHotKeysObjectSwap(userHotKeys) {
let result = {};
for (const [name, codes] of Object.entries(userHotKeys)) {
for (const code of codes) {
result[code] = name;
}
}
return result;
}
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

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

View File

@@ -1,4 +1,85 @@
import Vue from 'vue';
import * as utils from '../../share/utils';
const readerActions = {
'help': 'Вызвать cправку',
'loader': 'На страницу загрузки',
'settings': 'Настроить',
'undoAction': 'Действие назад',
'redoAction': 'Действие вперед',
'fullScreen': 'На весь экран',
'scrolling': 'Плавный скроллинг',
'stopScrolling': '',
'setPosition': 'Установить позицию',
'search': 'Найти в тексте',
'copyText': 'Скопировать текст со страницы',
'splitToPara': 'Обновить с разбиением на параграфы',
'refresh': 'Принудительно обновить книгу',
'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
'bookBegin': 'В начало книги',
'bookEnd': 'В конец книги',
'pageBack': 'Страницу назад',
'pageForward': 'Страницу вперед',
'lineBack': 'Строчку назад',
'lineForward': 'Строчку вперед',
'incFontSize': 'Увеличить размер шрифта',
'decFontSize': 'Уменьшить размер шрифта',
'scrollingSpeedUp': 'Увеличить скорость скроллинга',
'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
};
//readerActions[name]
const toolButtons = [
{name: 'undoAction', show: true},
{name: 'redoAction', show: true},
{name: 'fullScreen', show: true},
{name: 'scrolling', show: false},
{name: 'setPosition', show: true},
{name: 'search', show: true},
{name: 'copyText', show: false},
{name: 'splitToPara', show: false},
{name: 'refresh', show: true},
{name: 'contents', show: true},
{name: 'libs', show: true},
{name: 'recentBooks', show: true},
{name: 'offlineMode', show: false},
];
//readerActions[name]
const hotKeys = [
{name: 'help', codes: ['F1', 'H']},
{name: 'loader', codes: ['Escape']},
{name: 'settings', codes: ['S']},
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
{name: 'fullScreen', codes: ['Enter', 'Backquote', 'F']},
{name: 'scrolling', codes: ['Z']},
{name: 'setPosition', codes: ['P']},
{name: 'search', codes: ['Ctrl+F']},
{name: 'copyText', codes: ['Ctrl+C']},
{name: 'splitToPara', codes: ['Shift+R']},
{name: 'refresh', codes: ['R']},
{name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']},
{name: 'offlineMode', codes: ['O']},
{name: 'switchToolbar', codes: ['Tab', 'Q']},
{name: 'bookBegin', codes: ['Home']},
{name: 'bookEnd', codes: ['End']},
{name: 'pageBack', codes: ['PageUp', 'ArrowLeft', 'Backspace', 'Shift+Space']},
{name: 'pageForward', codes: ['PageDown', 'ArrowRight', 'Space']},
{name: 'lineBack', codes: ['ArrowUp']},
{name: 'lineForward', codes: ['ArrowDown']},
{name: 'incFontSize', codes: ['A']},
{name: 'decFontSize', codes: ['Shift+A']},
{name: 'scrollingSpeedUp', codes: ['Shift+ArrowDown']},
{name: 'scrollingSpeedDown', codes: ['Shift+ArrowUp']},
];
const fonts = [
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
@@ -6,7 +87,7 @@ const fonts = [
{name: 'Arimo', fontVertShift: 0},
{name: 'Avrile', fontVertShift: -10},
{name: 'OpenSans', fontVertShift: -5},
{name: 'Roboto', fontVertShift: 10},
{name: 'Roboto', fontVertShift: 0},
{name: 'Rubik', fontVertShift: 0},
];
@@ -124,57 +205,128 @@ const webFonts = [
];
//----------------------------------------------------------------------------------------------------------
const settingDefaults = {
textColor: '#000000',
backgroundColor: '#EBE2C9',
wallpaper: '',
fontStyle: '',// 'italic'
fontWeight: '',// 'bold'
fontSize: 20,// px
fontName: 'ReaderDefault',
webFontName: '',
fontVertShift: 0,
textVertShift: -20,
textColor: '#000000',
backgroundColor: '#EBE2C9',
wallpaper: '',
fontStyle: '',// 'italic'
fontWeight: '',// 'bold'
fontSize: 20,// px
fontName: 'ReaderDefault',
webFontName: '',
fontVertShift: 0,
textVertShift: 0,
lineInterval: 3,// px, межстрочный интервал
textAlignJustify: true,// выравнивание по ширине
p: 25,// px, отступ параграфа
indentLR: 15,// px, отступ всего текста слева и справа
indentTB: 0,// px, отступ всего текста сверху и снизу
wordWrap: true,//перенос по слогам
keepLastToFirst: true,// перенос последней строки в первую при листании
lineInterval: 3,// px, межстрочный интервал
textAlignJustify: true,// выравнивание по ширине
p: 25,// px, отступ параграфа
indentLR: 15,// px, отступ всего текста слева и справа
indentTB: 0,// px, отступ всего текста сверху и снизу
wordWrap: true,//перенос по слогам
keepLastToFirst: false,// перенос последней строки в первую при листании
showStatusBar: true,
statusBarTop: false,// top, bottom
statusBarHeight: 19,// px
statusBarColorAlpha: 0.4,
showStatusBar: true,
statusBarTop: false,// top, bottom
statusBarHeight: 19,// px
statusBarColorAlpha: 0.4,
statusBarClickOpen: true,
scrollingDelay: 3000,// замедление, ms
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
scrollingDelay: 3000,// замедление, ms
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
pageChangeTransition: '',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание
pageChangeTransitionSpeed: 50, //0-100%
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
pageChangeAnimationSpeed: 80, //0-100%
allowUrlParamBookPos: false,
lazyParseEnabled: false,
copyFullText: false,
showClickMapPage: true,
clickControl: true,
cutEmptyParagraphs: false,
addEmptyParagraphs: 0,
allowUrlParamBookPos: false,
lazyParseEnabled: false,
copyFullText: false,
showClickMapPage: true,
clickControl: true,
cutEmptyParagraphs: false,
addEmptyParagraphs: 0,
blinkCachedLoad: true,
showImages: true,
showInlineImagesInCenter: true,
compactTextPerc: 0,
imageHeightLines: 100,
imageFitWidth: true,
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
showLiberamaTopDialog2020: true,
enableSitesFilter: true,
fontShifts: {},
fontShifts: {},
showToolButton: {},
userHotKeys: {},
};
for (const font of fonts)
settingDefaults.fontShifts[font.name] = font.fontVertShift;
for (const font of webFonts)
settingDefaults.fontShifts[font.name] = font.fontVertShift;
for (const button of toolButtons)
settingDefaults.showToolButton[button.name] = button.show;
for (const hotKey of hotKeys)
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
const excludeDiffHotKeys = [];
for (const hotKey of hotKeys)
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
}
return false;
}
const libsDefaults = {
startLink: 'http://flibusta.is',
comment: 'Флибуста | Книжное братство',
closeAfterSubmit: false,
openInFrameOnEnter: false,
openInFrameOnAdd: false,
groups: [
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
{l: 'https://flibs.in', c: 'Flibs'},
]},
{r: 'http://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,
serverSyncEnabled: false,
serverStorageKey: '',
profiles: {},
profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '',
donationRemindDate: '',
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settingsRev: {},
libs: Object.assign({}, libsDefaults),
libsRev: 0,
};
// getters
@@ -188,15 +340,59 @@ const mutations = {
setToolBarActive(state, value) {
state.toolBarActive = value;
},
setServerSyncEnabled(state, value) {
state.serverSyncEnabled = value;
},
setServerStorageKey(state, value) {
state.serverStorageKey = value;
},
setProfiles(state, value) {
state.profiles = value;
},
setProfilesRev(state, value) {
state.profilesRev = value;
},
setAllowProfilesSave(state, value) {
state.allowProfilesSave = value;
},
setWhatsNewContentHash(state, value) {
state.whatsNewContentHash = value;
},
setDonationRemindDate(state, value) {
state.donationRemindDate = value;
},
setCurrentProfile(state, value) {
state.currentProfile = value;
},
setSettings(state, value) {
state.settings = Object.assign({}, state.settings, value);
}
const newSettings = Object.assign({}, state.settings, value);
const added = addDefaultsToSettings(newSettings);
if (added) {
state.settings = added;
} else {
state.settings = newSettings;
}
},
setSettingsRev(state, value) {
state.settingsRev = Object.assign({}, state.settingsRev, value);
},
setLibs(state, value) {
state.libs = value;
},
setLibsRev(state, value) {
state.libsRev = value;
},
};
export default {
readerActions,
toolButtons,
hotKeys,
fonts,
webFonts,
settingDefaults,
addDefaultsToSettings,
libsDefaults,
namespaced: true,
state,

View File

@@ -1 +0,0 @@
.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success{background-color:#f0f9eb;color:#67c23a}.el-alert--success .el-alert__description{color:#67c23a}.el-alert--info{background-color:#f4f4f5;color:#909399}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning .el-alert__description{color:#e6a23c}.el-alert--error{background-color:#fef0f0;color:#f56c6c}.el-alert--error .el-alert__description{color:#f56c6c}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;color:#c0c4cc;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-alert-fade-enter,.el-alert-fade-leave-active{opacity:0}

View File

@@ -1 +0,0 @@
.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#f56c6c;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#00468F}.el-badge__content--success{background-color:#67c23a}.el-badge__content--warning{background-color:#e6a23c}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#f56c6c}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#00468F;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.el-card{border-radius:4px;border:1px solid #ebeef5;background-color:#fff;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__body{padding:20px}

View File

@@ -1 +0,0 @@
.el-carousel__item,.el-carousel__mask{position:absolute;height:100%;top:0;left:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#fff;opacity:.24;-webkit-transition:.2s;transition:.2s}

View File

@@ -1 +0,0 @@
.el-carousel{overflow-x:hidden;position:relative}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0;padding:0;z-index:2}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#c0c4cc;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{display:inline-block;background-color:transparent;padding:12px 4px;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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