Compare commits

...

400 Commits

Author SHA1 Message Date
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
98 changed files with 12953 additions and 4330 deletions

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))
@@ -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

@@ -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 AppCachePlugin = require('appcache-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,7 @@ 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 AppCachePlugin({exclude: ['../index.html']})
]
});

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))
@@ -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

@@ -6,7 +6,9 @@ const api = axios.create({
class Misc {
async loadConfig() {
const response = await api.post('/config', {params: ['name', 'version', 'mode', 'maxUploadFileSize', 'branch']});
const response = await api.post('/config', {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
]});
return response.data;
}
}

View File

@@ -1,17 +1,20 @@
import _ from 'lodash';
import axios from 'axios';
import {sleep} from '../share/utils';
import {Buffer} from 'safe-buffer';
import * as utils from '../share/utils';
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;
const refreshPause = 300;
if (!callback) callback = () => {};
let response = await api.post('/load-book', {type: 'url', url});
@@ -41,16 +44,17 @@ class Reader {
throw new Error(errMes);
}
if (i > 0)
await sleep(refreshPause);
await utils.sleep(refreshPause);
i++;
if (i > 30*1000/refreshPause) {//30 сек ждем телодвижений воркера
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
const prevProgress = response.data.progress;
const prevState = response.data.state;
response = await workerApi.post('/get-state', {workerId});
i = (prevProgress != response.data.progress ? 1 : i);
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
}
}
@@ -105,6 +109,16 @@ class Reader {
return url;
}
async storage(request) {
let response = await api.post('/storage', request);
const state = response.data.state;
if (!state)
throw new Error('Неверный ответ api');
return response.data;
}
}
export default new Reader();

View File

@@ -47,14 +47,12 @@
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import * as utils from '../share/utils';
export default @Component({
watch: {
rootRoute: function() {
this.setAppTitle();
this.redirectIfNeeded();
},
mode: function() {
this.setAppTitle();
this.redirectIfNeeded();
}
},
@@ -113,13 +111,19 @@ class App extends Vue {
this.dispatch('config/loadConfig');
this.$watch('apiError', function(newError) {
if (newError) {
let mes = newError.message;
if (newError.response && newError.response.config)
mes = newError.response.config.url + '<br>' + newError.response.statusText;
this.$notify.error({
title: 'Ошибка API',
dangerouslyUseHTMLString: true,
message: newError.response.config.url + '<br>' + newError.response.statusText
message: mes
});
}
});
this.setAppTitle();
this.redirectIfNeeded();
}
toggleCollapse() {
@@ -198,15 +202,18 @@ class App extends Vue {
}
redirectIfNeeded() {
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
//старый url
const search = window.location.search.substr(1);
const url = search.split('url=')[1] || '';
const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) {
window.location = `/#/reader?url=${url}`;
} else {
this.$router.replace('/reader');
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
}
//yandex-метрика для omnireader

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>
@@ -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

@@ -3,8 +3,10 @@
<h4>Возможности читалки:</h4>
<ul>
<li>загрузка любой страницы интернета</li>
<li>работа в автономном режиме (без связи)</li>
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
<li>установка и запоминание текущей позиции и настроек в браузере (в будущем планируется сохранение и на сервер)</li>
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
<li>кэширование файлов книг на клиенте и на сервере</li>
<li>открытие книг с локального диска</li>
<li>плавный скроллинг текста</li>
@@ -12,16 +14,23 @@
<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'">
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
<strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
</span>
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически откроете ее в Omni Reader.
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
</p>
</div>
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
</div>
</template>
@@ -31,21 +40,25 @@
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;
}
async copyText(text, mes) {
const result = await copyTextToClipboard(text);
const msg = (result ? mes : 'Копирование не удалось');
if (result)
this.$notify.success({message: msg});
else
this.$notify.error({message: msg});
}
}
//-----------------------------------------------------------------------------
@@ -63,4 +76,10 @@ class CommonHelpPage extends Vue {
h4 {
margin: 0;
}
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@@ -53,11 +53,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.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
else
this.$notify.error({message: msg});
this.$notify.error({message: 'Копирование не удалось'});
}
}
//-----------------------------------------------------------------------------

View File

@@ -1,29 +1,27 @@
<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>
</el-tabs>
</Window>
</div>
</div>
<el-tabs type="border-card" v-model="selectedTab">
<el-tab-pane class="tab" label="Общее">
<CommonHelpPage></CommonHelpPage>
</el-tab-pane>
<el-tab-pane label="Клавиатура">
<HotkeysHelpPage></HotkeysHelpPage>
</el-tab-pane>
<el-tab-pane label="Мышь/тачпад">
<MouseHelpPage></MouseHelpPage>
</el-tab-pane>
<el-tab-pane label="История версий" name="releases">
<VersionHistoryPage></VersionHistoryPage>
</el-tab-pane>
<el-tab-pane label="Помочь проекту" name="donate">
<DonateHelpPage></DonateHelpPage>
</el-tab-pane>
</el-tabs>
</Window>
</template>
<script>
@@ -36,6 +34,7 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
export default @Component({
components: {
@@ -44,6 +43,7 @@ export default @Component({
HotkeysHelpPage,
MouseHelpPage,
DonateHelpPage,
VersionHistoryPage,
},
})
class HelpPage extends Vue {
@@ -57,6 +57,10 @@ class HelpPage extends Vue {
this.selectedTab = 'donate';
}
activateVersionHistoryHelpPage() {
this.selectedTab = 'releases';
}
keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) {
this.close();
@@ -68,23 +72,6 @@ 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;

View File

@@ -4,7 +4,7 @@
<ul>
<li><b>F1, H</b> - открыть справку</li>
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
<li><b>Tab</b> - показать/скрыть панель управления</li>
<li><b>Tab, Q</b> - показать/скрыть панель управления</li>
<li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
<li><b>PageDown, Right, Space</b> - страницу вперед</li>
<li><b>Home</b> - в начало книги</li>
@@ -20,6 +20,7 @@
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
<li><b>X</b> - открыть недавние</li>
<li><b>O</b> - автономный режим</li>
<li><b>S</b> - открыть окно настроек</li>
</ul>
</div>

View File

@@ -0,0 +1,81 @@
<template>
<div id="versionHistoryPage" class="page">
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
<p>
{{ item }}
</p>
</span>
<br>
<h4>История версий:</h4>
<br>
<div v-for="item in versionContent" :id="item.key" :key="item.key">
<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 {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
}
p {
line-height: 15px;
}
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@@ -1,263 +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 = [];
const sorted = bookManager.getSortedRecent();
const len = (sorted.length < 100 ? sorted.length : 100);
for (let i = 0; i < len; i++) {
const book = sorted[i];
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

@@ -2,9 +2,12 @@
<div ref="main" class="main">
<div class="part">
<span class="greeting bold-font">{{ title }}</span>
<div class="space"></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</b></span>
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
</div>
<div class="part center">
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
@@ -15,13 +18,28 @@
Загрузить файл с диска
</el-button>
<div class="space"></div>
<span v-if="config.mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Комментарии</span>
<el-button size="mini" @click="loadBufferClick">
Из буфера обмена
</el-button>
<div class="space"></div>
<div class="space"></div>
<div v-if="mode == 'omnireader'" 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 class="space"></div>
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
</div>
<div class="part bottom">
<span class="bottom-span clickable" @click="openHelp">Справка</span>
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
<span class="bottom-span">{{ version }}</span>
</div>
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
</div>
</template>
@@ -29,20 +47,26 @@
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
export default @Component({
components: {
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,14 +74,22 @@ class LoaderPage extends Vue {
}
get title() {
if (this.config.mode == 'omnireader')
if (this.mode == 'omnireader')
return 'Omni Reader - браузерная онлайн-читалка.';
return 'Универсальная читалка книг и ресурсов интернета.';
}
get mode() {
return this.$store.state.config.mode;
}
get version() {
return `v${this.config.version}`;
return `v${this.$store.state.config.version}`;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
submitUrl() {
@@ -72,12 +104,27 @@ 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});
}
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() {
this.$emit('help-toggle');
}
@@ -91,6 +138,10 @@ class LoaderPage extends Vue {
}
keyHook(event) {
if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event);
}
//недостатки сторонних ui
const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
@@ -103,6 +154,13 @@ class LoaderPage extends Vue {
event.stopPropagation();
return true;
}
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
this.$emit('tool-bar-toggle');
event.preventDefault();
event.stopPropagation();
return true;
}
}
}
//-----------------------------------------------------------------------------
@@ -112,6 +170,7 @@ class LoaderPage extends Vue {
flex: 1;
display: flex;
flex-direction: column;
min-height: 400px;
}
.part {
@@ -123,8 +182,8 @@ class LoaderPage extends Vue {
}
.greeting {
font-size: 130%;
line-height: 170%;
font-size: 120%;
line-height: 160%;
}
.bold-font {

View File

@@ -0,0 +1,123 @@
<template>
<Window @close="close">
<template slot="header">
<span style="position: relative; top: -3px">
Вставьте текст и нажмите
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
или F2
</span>
</template>
<div>
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
</div>
<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: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
this.close();
}
close() {
this.$emit('paste-text-toggle');
}
keyHook(event) {
if (event.type == 'keydown') {
switch (event.code) {
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

@@ -94,6 +94,6 @@ class ProgressPage extends Vue {
</style>
<style>
.el-progress__text {
color: lightgreen;
color: lightgreen !important;
}
</style>

View File

@@ -1,42 +1,45 @@
<template>
<el-container>
<el-header v-show="toolBarActive" height='50px'>
<div class="header">
<div ref="header" class="header">
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
</el-tooltip>
<div>
<el-tooltip content="Действие назад" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
</el-tooltip>
<el-tooltip content="Действие вперед" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
</el-tooltip>
<div class="space"></div>
<el-tooltip content="На весь экран" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
</el-tooltip>
<el-tooltip content="Плавный скроллинг" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
</el-tooltip>
<el-tooltip content="Перелистнуть" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
</el-tooltip>
<el-tooltip content="Найти в тексте" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
</el-tooltip>
<el-tooltip content="Скопировать текст со страницы" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
</el-tooltip>
<el-tooltip content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
<el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
</el-button>
</el-tooltip>
<div class="space"></div>
<el-tooltip content="Открыть недавние" :open-delay="1000" effect="light">
<el-button ref="history" class="tool-button" :class="buttonActiveClass('history')" @click="buttonClick('history')"><i class="el-icon-document"></i></el-button>
<el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
</el-tooltip>
<el-tooltip v-show="showToolButton['offlineMode']" content="Автономный режим (без интернета)" :open-delay="1000" effect="light">
<el-button ref="offlineMode" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"><i class="el-icon-connection"></i></el-button>
</el-tooltip>
</div>
@@ -68,12 +71,108 @@
@start-text-search="startTextSearch"
@stop-text-search="stopTextSearch">
</SearchPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
<HistoryPage v-if="historyActive" ref="historyPage" @load-book="loadBook" @history-toggle="historyToggle"></HistoryPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<el-dialog
title="Что нового:"
:visible.sync="whatsNewVisible"
width="80%">
<div style="line-height: 20px" v-html="whatsNewContent"></div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer" class="dialog-footer">
<el-button @click="whatsNewDisable">Больше не показывать</el-button>
</span>
</el-dialog>
<el-dialog
title="Внимание!"
:visible.sync="migrationVisible1"
width="90%">
<div>
Появилась httpS-версия сайта по адресу <a href="https://omnireader.ru" target="_blank">https://omnireader.ru</a><br>
Работа по httpS-протоколу, помимо безопасности соединения, позволяет воспользоваться всеми возможностями
современных браузеров, а именно, применительно к нашему ресурсу:
<ul>
<li>возможность автономной работы с читалкой (без доступа к интернету), кеширование сайта через appcache</li>
<li>безопасная передача на сервер данных о настройках и читаемых книгах при включенной синхронизации; все данные шифруются на стороне
браузера ключом доступа и никто (в т.ч. администратор) не имеет возможности их прочитать
<li>использование встроенных в JS функций шифрования и других</li>
</ul>
Для того, чтобы перейти на новую версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
<ul>
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
после этого все данные будут автоматически сохранены на сервер
</span>
</li>
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
произойдет переход на https-версию читалки и откроется окно для ввода ключа
</span><br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
</span>
</li>
</ul>
Старая http-версия сайта будет доступна до конца 2019 года.<br>
Приносим извинения за доставленные неудобства.
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
</span>
</el-dialog>
<el-dialog
title="Внимание!"
:visible.sync="migrationVisible2"
width="90%">
<div>
Информация для пользователей старой версии читалки по адресу <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a><br>
Для того, чтобы перейти на новую httpS-версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
<ul>
<li>перейти на старую версию ресурса <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a></li>
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
после этого все данные будут автоматически сохранены на сервер
</span>
</li>
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
произойдет переход на https-версию читалки и откроется окно для ввода ключа
</span><br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
</span>
</li>
</ul>
Старая http-версия сайта будет доступна до конца 2019 года.<br>
Приносим извинения за доставленные неудобства.
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
</span>
</el-dialog>
</el-main>
</el-container>
</template>
@@ -81,6 +180,9 @@
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
import {Buffer} from 'safe-buffer';
import LoaderPage from './LoaderPage/LoaderPage.vue';
import TextPage from './TextPage/TextPage.vue';
import ProgressPage from './ProgressPage/ProgressPage.vue';
@@ -88,16 +190,16 @@ import ProgressPage from './ProgressPage/ProgressPage.vue';
import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
import SearchPage from './SearchPage/SearchPage.vue';
import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
import HistoryPage from './HistoryPage/HistoryPage.vue';
import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
import SettingsPage from './SettingsPage/SettingsPage.vue';
import HelpPage from './HelpPage/HelpPage.vue';
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
import ServerStorage from './ServerStorage/ServerStorage.vue';
import bookManager from './share/bookManager';
import readerApi from '../../api/reader';
import _ from 'lodash';
import {sleep} from '../../share/utils';
import restoreOldSettings from './share/restoreOldSettings';
import * as utils from '../../share/utils';
import {versionHistory} from './versionHistory';
export default @Component({
components: {
@@ -108,10 +210,11 @@ export default @Component({
SetPositionPage,
SearchPage,
CopyTextPage,
HistoryPage,
RecentBooksPage,
SettingsPage,
HelpPage,
ClickMapPage,
ServerStorage,
},
watch: {
bookPos: function(newValue) {
@@ -138,10 +241,12 @@ export default @Component({
this.updateRoute();
},
loaderActive: function(newValue) {
const recent = this.mostRecentBook();
if (!newValue && !this.loading && recent && !bookManager.hasBookParsed(recent)) {
this.loadBook(recent);
}
(async() => {
const recent = this.mostRecentBook();
if (!newValue && !this.loading && recent && !await bookManager.hasBookParsed(recent)) {
this.loadBook(recent);
}
})();
},
},
})
@@ -154,7 +259,8 @@ class Reader extends Vue {
setPositionActive = false;
searchActive = false;
copyTextActive = false;
historyActive = false;
recentBooksActive = false;
offlineModeActive = false;
settingsActive = false;
helpActive = false;
clickMapActive = false;
@@ -163,9 +269,16 @@ class Reader extends Vue {
allowUrlParamBookPos = false;
showRefreshIcon = true;
mostRecentBookReactive = null;
showToolButton = {};
actionList = [];
actionCur = -1;
hidden = false;
whatsNewVisible = false;
whatsNewContent = '';
migrationVisible1 = false;
migrationVisible2 = false;
created() {
this.loading = true;
@@ -197,21 +310,39 @@ class Reader extends Vue {
});
this.loadSettings();
//TODO: убрать в будущем
if (this.showToolButton['history']) {
const newShowToolButton = Object.assign({}, this.showToolButton);
newShowToolButton['recentBooks'] = true;
delete newShowToolButton['history'];
const newSettings = Object.assign({}, this.settings, { showToolButton: newShowToolButton });
this.commit('reader/setSettings', newSettings);
}
}
mounted() {
this.updateHeaderMinWidth();
(async() => {
await bookManager.init(this.settings);
await restoreOldSettings(this.settings, bookManager, this.commit);
bookManager.addEventListener(this.bookManagerEvent);
if (this.$root.rootRoute == '/reader') {
if (this.routeParamUrl) {
this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos});
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
} else {
this.loaderActive = true;
}
}
this.checkSetStorageAccessKey();
this.checkActivateDonateHelpPage();
this.loading = false;
await this.$refs.serverStorage.init();
await this.showWhatsNew();
await this.showMigration();
})();
}
@@ -222,6 +353,108 @@ class Reader extends Vue {
this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showMigrationDialog = settings.showMigrationDialog;
this.showToolButton = settings.showToolButton;
this.updateHeaderMinWidth();
}
updateHeaderMinWidth() {
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
if (this.$refs.header)
this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
}
checkSetStorageAccessKey() {
const q = this.$route.query;
if (q['setStorageAccessKey']) {
this.$router.replace(`/reader`);
this.settingsToggle();
this.$nextTick(() => {
this.$refs.settingsPage.enterServerStorageKey(
Buffer.from(utils.fromBase58(q['setStorageAccessKey'])).toString()
);
});
}
}
checkActivateDonateHelpPage() {
const q = this.$route.query;
if (q['donate']) {
this.$router.replace(`/reader`);
this.helpToggle();
this.$nextTick(() => {
this.$refs.helpPage.activateDonateHelpPage();
});
}
}
checkBookPosPercent() {
const q = this.$route.query;
if (q['__pp']) {
let pp = q['__pp'];
if (pp) {
pp = parseFloat(pp) || 0;
const recent = this.mostRecentBook();
(async() => {
await utils.sleep(100);
this.bookPos = Math.floor(recent.textLength*pp/100);
})();
}
}
}
async showWhatsNew() {
await utils.sleep(2000);
const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) {
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
this.whatsNewVisible = true;
}
}
async showMigration() {
await utils.sleep(3000);
if (!this.settingsActive &&
this.mode == 'omnireader' && this.showMigrationDialog && this.migrationRemindDate != utils.formatDate(new Date(), 'coDate')) {
if (window.location.protocol == 'http:') {
this.migrationVisible1 = true;
} else if (window.location.protocol == 'https:') {
this.migrationVisible2 = true;
}
}
}
migrationDialogDisable() {
this.migrationVisible1 = false;
this.migrationVisible2 = false;
if (this.showMigrationDialog) {
const newSettings = Object.assign({}, this.settings, { showMigrationDialog: false });
this.commit('reader/setSettings', newSettings);
}
}
migrationDialogRemind() {
this.migrationVisible1 = false;
this.migrationVisible2 = false;
this.commit('reader/setMigrationRemindDate', utils.formatDate(new Date(), 'coDate'));
}
openVersionHistory() {
this.whatsNewVisible = false;
this.versionHistoryToggle();
}
whatsNewDisable() {
this.whatsNewVisible = false;
const whatsNew = versionHistory[0];
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
}
get routeParamPos() {
@@ -237,16 +470,22 @@ class Reader extends Vue {
}
updateRoute(isNewRoute) {
if (this.loading)
return;
const recent = this.mostRecentBook();
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
const url = (recent ? `url=${recent.url}` : '');
if (isNewRoute)
this.$router.push(`/reader?${pos}${url}`);
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
else
this.$router.replace(`/reader?${pos}${url}`);
this.$router.replace(`/reader?${pos}${url}`).catch(() => {});
}
get mode() {
return this.$store.state.config.mode;
}
get routeParamUrl() {
let result = '';
const path = this.$route.fullPath;
@@ -258,6 +497,11 @@ class Reader extends Vue {
return decodeURIComponent(result);
}
get routeParamRefresh() {
const q = this.$route.query;
return !!q['__refresh'];
}
bookPosChanged(event) {
if (event.bookPosSeen !== undefined)
this.bookPosSeen = event.bookPosSeen;
@@ -265,6 +509,32 @@ class Reader extends Vue {
this.debouncedUpdateRoute();
}
async bookManagerEvent(eventName) {
if (eventName == 'recent-changed') {
if (this.recentBooksActive) {
await this.$refs.recentBooksPage.updateTableData();
}
}
if (eventName == 'set-recent' || eventName == 'recent-deleted') {
const oldBook = this.mostRecentBookReactive;
const newBook = bookManager.mostRecentBook();
if (oldBook && newBook) {
if (oldBook.key != newBook.key) {
this.loadingBook = true;
try {
await this.loadBook(newBook);
} finally {
this.loadingBook = false;
}
} else if (oldBook.bookPos != newBook.bookPos) {
while (this.loadingBook) await utils.sleep(100);
this.bookPosChanged({bookPos: newBook.bookPos});
}
}
}
}
get toolBarActive() {
return this.reader.toolBarActive;
}
@@ -279,6 +549,14 @@ class Reader extends Vue {
return this.$store.state.reader.settings;
}
get whatsNewContentHash() {
return this.$store.state.reader.whatsNewContentHash;
}
get migrationRemindDate() {
return this.$store.state.reader.migrationRemindDate;
}
addAction(pos) {
let a = this.actionList;
if (!a.length || a[a.length - 1] != pos) {
@@ -319,7 +597,7 @@ class Reader extends Vue {
closeAllTextPages() {
this.setPositionActive = false;
this.copyTextActive = false;
this.historyActive = false;
this.recentBooksActive = false;
this.settingsActive = false;
this.stopScrolling();
this.stopSearch();
@@ -340,8 +618,8 @@ class Reader extends Vue {
this.setPositionActive = true;
this.$nextTick(() => {
this.$refs.setPositionPage.sliderMax = this.mostRecentBook().textLength - 1;
this.$refs.setPositionPage.sliderValue = this.mostRecentBook().bookPos;
const recent = this.mostRecentBook();
this.$refs.setPositionPage.init(recent.bookPos, recent.textLength - 1);
});
} else {
this.setPositionActive = false;
@@ -411,21 +689,31 @@ class Reader extends Vue {
}
}
historyToggle() {
this.historyActive = !this.historyActive;
if (this.historyActive) {
recentBooksToggle() {
this.recentBooksActive = !this.recentBooksActive;
if (this.recentBooksActive) {
this.closeAllTextPages();
this.historyActive = true;
this.$refs.recentBooksPage.init();
this.recentBooksActive = true;
} else {
this.historyActive = false;
this.recentBooksActive = false;
}
}
offlineModeToggle() {
this.offlineModeActive = !this.offlineModeActive;
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
}
settingsToggle() {
this.settingsActive = !this.settingsActive;
if (this.settingsActive) {
this.closeAllTextPages();
this.settingsActive = true;
this.$nextTick(() => {
this.$refs.settingsPage.init();
});
} else {
this.settingsActive = false;
}
@@ -448,6 +736,15 @@ class Reader extends Vue {
}
}
versionHistoryToggle() {
this.helpToggle();
if (this.helpActive) {
this.$nextTick(() => {
this.$refs.helpPage.activateVersionHistoryHelpPage();
});
}
}
refreshBook() {
if (this.mostRecentBook()) {
this.loadBook({url: this.mostRecentBook().url, force: true});
@@ -493,12 +790,15 @@ class Reader extends Vue {
case 'copyText':
this.copyTextToggle();
break;
case 'history':
this.historyToggle();
break;
case 'refresh':
this.refreshBook();
break;
case 'recentBooks':
this.recentBooksToggle();
break;
case 'offlineMode':
this.offlineModeToggle();
break;
case 'settings':
this.settingsToggle();
break;
@@ -517,7 +817,8 @@ class Reader extends Vue {
case 'scrolling':
case 'search':
case 'copyText':
case 'history':
case 'recentBooks':
case 'offlineMode':
case 'settings':
if (this[`${button}Active`])
classResult = classActive;
@@ -535,7 +836,7 @@ class Reader extends Vue {
break;
}
if (this.activePage == 'LoaderPage' || !this.mostRecentBook()) {
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
switch (button) {
case 'undoAction':
case 'redoAction':
@@ -545,9 +846,9 @@ class Reader extends Vue {
case 'copyText':
classResult = classDisabled;
break;
case 'history':
case 'recentBooks':
case 'refresh':
if (!this.mostRecentBook())
if (!this.mostRecentBookReactive)
classResult = classDisabled;
break;
}
@@ -583,11 +884,17 @@ class Reader extends Vue {
this.$root.$emit('set-app-title');
}
// на LoaderPage всегда показываем toolBar
if (result == 'LoaderPage' && !this.toolBarActive) {
this.toolBarToggle();
}
if (this.lastActivePage != result && result == 'TextPage') {
//акивируем страницу с текстом
this.$nextTick(async() => {
const last = this.mostRecentBookReactive;
const isParsed = bookManager.hasBookParsed(last);
const isParsed = await bookManager.hasBookParsed(last);
if (!isParsed) {
this.$root.$emit('set-app-title');
return;
@@ -608,7 +915,7 @@ class Reader extends Vue {
return result;
}
loadBook(opts) {
async loadBook(opts) {
if (!opts || !opts.url) {
this.mostRecentBook();
return;
@@ -621,125 +928,127 @@ class Reader extends Vue {
// уже просматривается сейчас
const lastBook = (this.$refs.page ? this.$refs.page.lastBook : null);
if (!opts.force && lastBook && lastBook.url == url && bookManager.hasBookParsed(lastBook)) {
if (!opts.force && lastBook && lastBook.url == url && await bookManager.hasBookParsed(lastBook)) {
this.loaderActive = false;
return;
}
this.progressActive = true;
this.$nextTick(async() => {
const progress = this.$refs.page;
this.actionList = [];
this.actionCur = -1;
await this.$nextTick();
try {
progress.show();
progress.setState({state: 'parse'});
const progress = this.$refs.page;
// есть ли среди недавних
const key = bookManager.keyFromUrl(url);
let wasOpened = await bookManager.getRecentBook({key});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
const bookPosPercent = wasOpened.bookPosPercent;
this.actionList = [];
this.actionCur = -1;
let book = null;
try {
progress.show();
progress.setState({state: 'parse'});
if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url}, (prog) => {
progress.setState({progress: prog});
});
// есть ли среди недавних
const key = bookManager.keyFromUrl(url);
let wasOpened = await bookManager.getRecentBook({key});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
// если есть в локальном кэше
if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, bookPosPercent}, bookManager.metaOnly(bookParsed)));
this.mostRecentBook();
this.addAction(bookPos);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage();
let book = null;
await this.activateClickMapPage();
return;
}
// иначе идем на сервер
// пытаемся загрузить готовый файл с сервера
if (wasOpened.path) {
try {
const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => {
progress.setState(state);
});
book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) {
//молчим
}
}
}
progress.setState({totalSteps: 5});
// не удалось, скачиваем книгу полностью с конвертацией
let loadCached = true;
if (!book) {
book = await readerApi.loadBook(url, (state) => {
progress.setState(state);
});
loadCached = false;
}
// добавляем в bookManager
progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (prog) => {
if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url}, (prog) => {
progress.setState({progress: prog});
});
// добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, bookPosPercent}, bookManager.metaOnly(addedBook)));
this.mostRecentBook();
this.addAction(bookPos);
this.updateRoute(true);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
if (loadCached) {
// если есть в локальном кэше
if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
this.mostRecentBook();
this.addAction(bookPos);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage();
} else
this.stopBlink = true;
await this.activateClickMapPage();
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$alert(e.message, 'Ошибка', {type: 'error'});
this.checkBookPosPercent();
await this.activateClickMapPage();
return;
}
// иначе идем на сервер
// пытаемся загрузить готовый файл с сервера
if (wasOpened.path) {
try {
const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => {
progress.setState(state);
});
book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) {
//молчим
}
}
}
});
}
loadFile(opts) {
this.progressActive = true;
this.$nextTick(async() => {
const progress = this.$refs.page;
try {
progress.show();
progress.setState({state: 'upload'});
const url = await readerApi.uploadFile(opts.file, this.config.maxUploadFileSize, (state) => {
progress.setState({totalSteps: 5});
// не удалось, скачиваем книгу полностью с конвертацией
let loadCached = true;
if (!book) {
book = await readerApi.loadBook(url, (state) => {
progress.setState(state);
});
progress.hide(); this.progressActive = false;
this.loadBook({url});
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$alert(e.message, 'Ошибка', {type: 'error'});
loadCached = false;
}
});
// добавляем в bookManager
progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (prog) => {
progress.setState({progress: prog});
});
// добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook));
this.mostRecentBook();
this.addAction(bookPos);
this.updateRoute(true);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
if (loadCached) {
this.blinkCachedLoadMessage();
} else
this.stopBlink = true;
this.checkBookPosPercent();
await this.activateClickMapPage();
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$alert(e.message, 'Ошибка', {type: 'error'});
}
}
async loadFile(opts) {
this.progressActive = true;
await this.$nextTick();
const progress = this.$refs.page;
try {
progress.show();
progress.setState({state: 'upload'});
const url = await readerApi.uploadFile(opts.file, this.config.maxUploadFileSize, (state) => {
progress.setState(state);
});
progress.hide(); this.progressActive = false;
await this.loadBook({url});
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
this.$alert(e.message, 'Ошибка', {type: 'error'});
}
}
blinkCachedLoadMessage() {
@@ -756,7 +1065,7 @@ class Reader extends Vue {
this.showRefreshIcon = !this.showRefreshIcon;
if (page.blinkCachedLoadMessage)
page.blinkCachedLoadMessage(this.showRefreshIcon);
await sleep(500);
await utils.sleep(500);
if (this.stopBlink)
break;
this.blinkCount--;
@@ -779,8 +1088,8 @@ class Reader extends Vue {
if (!handled && this.settingsActive)
handled = this.$refs.settingsPage.keyHook(event);
if (!handled && this.historyActive)
handled = this.$refs.historyPage.keyHook(event);
if (!handled && this.recentBooksActive)
handled = this.$refs.recentBooksPage.keyHook(event);
if (!handled && this.setPositionActive)
handled = this.$refs.setPositionPage.keyHook(event);
@@ -830,7 +1139,12 @@ class Reader extends Vue {
this.refreshBook();
break;
case 'KeyX':
this.historyToggle();
this.recentBooksToggle();
event.preventDefault();
event.stopPropagation();
break;
case 'KeyO':
this.offlineModeToggle();
break;
case 'KeyS':
this.settingsToggle();
@@ -859,11 +1173,10 @@ class Reader extends Vue {
overflow-x: auto;
overflow-y: hidden;
}
.header {
display: flex;
justify-content: space-between;
min-width: 550px;
}
.el-main {
@@ -887,6 +1200,10 @@ class Reader extends Vue {
box-shadow: 3px 3px 5px black;
}
.tool-button + .tool-button {
margin: 0 2px 0 2px;
}
.tool-button:hover {
background-color: white;
}
@@ -927,4 +1244,10 @@ i {
.clear {
color: rgba(0,0,0,0);
}
</style>
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,320 @@
<template>
<Window width="600px" ref="window" @close="close">
<template slot="header">
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
<span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
</template>
<el-table
:data="tableData"
style="width: 570px"
size="mini"
height="1px"
stripe
border
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
:header-cell-style = "headerCellStyle"
:row-key = "rowKey"
>
<el-table-column
type="index"
width="35px"
>
</el-table-column>
<el-table-column
prop="touchDateTime"
min-width="85px"
sortable
>
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<span style="font-size: 90%">Время<br>просм.</span>
</template>
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<div class="desc" @click="loadBook(scope.row.url)">
{{ scope.row.touchDate }}<br>
{{ scope.row.touchTime }}
</div>
</template>
</el-table-column>
<el-table-column
>
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
<!--el-input ref="input"
:value="search" @input="search = $event"
size="mini"
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
placeholder="Найти"/-->
<div class="el-input el-input--mini">
<input class="el-input__inner"
ref="input"
placeholder="Найти"
style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
:value="search" @input="search = $event.target.value"
/>
</div>
</template>
<el-table-column
min-width="280px"
>
<template slot-scope="scope">
<div class="desc" @click="loadBook(scope.row.url)">
<span style="color: green">{{ scope.row.desc.author }}</span><br>
<span>{{ scope.row.desc.title }}</span>
</div>
</template>
</el-table-column>
<el-table-column
min-width="90px"
>
<template slot-scope="scope">
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
<a :href="scope.row.path" :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>
</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';
export default @Component({
components: {
Window,
},
watch: {
search: function() {
this.updateTableData();
}
},
})
class RecentBooksPage extends Vue {
loading = false;
search = null;
tableData = [];
created() {
}
init() {
this.$refs.window.init();
this.$nextTick(() => {
//this.$refs.input.focus();
});
(async() => {//отбражение подгрузки списка, иначе тормозит
if (this.initing)
return;
this.initing = true;
await this.updateTableData(3);
await utils.sleep(200);
if (bookManager.loaded) {
const t = Date.now();
await this.updateTableData(10);
if (bookManager.getSortedRecent().length > 10)
await utils.sleep(10*(Date.now() - t));
} else {
let i = 0;
let j = 5;
while (i < 500 && !bookManager.loaded) {
if (i % j == 0) {
bookManager.sortedRecentCached = null;
await this.updateTableData(100);
j *= 2;
}
await utils.sleep(100);
i++;
}
}
await this.updateTableData();
this.initing = false;
})();
}
rowKey(row) {
return row.key;
}
async updateTableData(limit) {
while (this.updating) await utils.sleep(100);
this.updating = true;
let result = [];
this.loading = !!limit;
const sorted = bookManager.getSortedRecent();
for (let i = 0; i < sorted.length; i++) {
const book = sorted[i];
if (book.deleted)
continue;
if (limit && result.length >= limit)
break;
let d = new Date();
d.setTime(book.touchTime);
const t = utils.formatDate(d).split(' ');
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
textLen = ` ${Math.round(book.textLength/1000)}k`;
}
const fb2 = (book.fb2 ? book.fb2 : {});
let title = fb2.bookTitle;
if (title)
title = `"${title}"`;
else
title = '';
let author = '';
if (fb2.author) {
const authorNames = fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
author = authorNames.join(', ');
} else {
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,
});
if (result.length >= 100)
break;
}
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())
});
/*for (let i = 0; i < result.length; i++) {
if (!_.isEqual(this.tableData[i], result[i])) {
this.$set(this.tableData, i, result[i]);
await utils.sleep(10);
}
}
if (this.tableData.length > result.length)
this.tableData.splice(result.length);*/
this.tableData = result;
this.updating = false;
}
headerCellStyle(cell) {
let result = {margin: 0, padding: 0};
if (cell.columnIndex > 0) {
result['border-bottom'] = 0;
}
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();
if (!bookManager.mostRecentBook())
this.close();
}
loadBook(url) {
this.$emit('load-book', {url});
this.close();
}
isUrl(url) {
if (url)
return (url.indexOf('file://') != 0);
else
return false;
}
close() {
this.$emit('recent-books-toggle');
}
keyHook(event) {
if (event.type == 'keydown' && event.code == 'Escape') {
this.close();
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.desc {
cursor: pointer;
}
</style>

View File

@@ -1,28 +1,24 @@
<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" 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>
</div>
</Window>
</template>
<script>
@@ -61,6 +57,8 @@ class SearchPage extends Vue {
}
async init(parsed) {
this.$refs.window.init();
if (this.parsed != parsed) {
this.initStep = true;
this.stopInit = false;
@@ -178,32 +176,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 {

View File

@@ -0,0 +1,632 @@
<template>
<div></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';
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);
},
},
})
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.debouncedSaveRecent = _.debounce((itemKey) => {
this.saveRecent(itemKey);
}, 1000);
this.debouncedNotifySuccess = _.debounce(() => {
this.success('Данные синхронизированы с сервером');
}, 1000);
this.oldProfiles = {};
this.oldSettings = {};
}
async init() {
try {
if (!this.serverStorageKey) {
//генерируем новый ключ
await this.generateNewServerStorageKey();
} else {
await this.serverStorageKeyChanged();
}
bookManager.addEventListener(this.bookManagerEvent);
} finally {
this.inited = true;
}
}
async bookManagerEvent(eventName, itemKey) {
if (!this.serverSyncEnabled)
return;
if (eventName == 'recent-changed') {
if (itemKey) {
if (!this.recentDeltaInited) {
await this.loadRecent();
this.warning('Функции сохранения на сервер пока недоступны');
return;
}
if (!this.recentDelta)
this.recentDelta = {};
this.recentDelta[itemKey] = _.cloneDeep(bookManager.recent[itemKey]);
this.debouncedSaveRecent(itemKey);
}
}
}
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.loadRecent();
if (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;
}
checkCurrentProfile() {
if (!this.profiles[this.currentProfile]) {
this.commit('reader/setCurrentProfile', '');
}
}
success(message) {
if (this.showServerStorageMessages)
this.$notify.success({message});
}
warning(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$notify.warning({message});
}
error(message) {
if (this.showServerStorageMessages && !this.offlineModeActive)
this.$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 initRecentDelta() {
let recentDelta = null;
try {
recentDelta = await this.storageGet({recentDelta: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (recentDelta.state == 'success') {
recentDelta = recentDelta.items.recentDelta;
if (recentDelta.rev == 0)
recentDelta.data = {};
this.recentDelta = recentDelta.data;
this.recentDeltaInited = true;
} else {
this.warning(`Неверный ответ сервера: ${recentDelta.state}`);
}
}
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
return;
this.loadingRecent = true;
try {
const oldRecentRev = bookManager.recentRev;
const oldRecentDeltaRev = bookManager.recentDeltaRev;
//проверим ревизию на сервере
let revs = null;
if (!skipRevCheck) {
try {
revs = await this.storageCheck({recent: {}, recentDelta: {}});
if (revs.state == 'success' && revs.items.recent.rev == oldRecentRev &&
revs.items.recentDelta.rev == oldRecentDeltaRev) {
if (!this.recentDeltaInited)
await this.initRecentDelta();
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let recent = null;
try {
recent = await this.storageGet({recent: {}, recentDelta: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (recent.state == 'success') {
let recentDelta = recent.items.recentDelta;
recent = recent.items.recent;
if (recent.rev == 0)
recent.data = {};
let newRecent = {};
if (recentDelta && recentDelta.data) {
if (recentDelta.data.diff) {
newRecent = recent.data;
const key = recentDelta.data.diff.key;
if (newRecent[key])
newRecent[key] = utils.applyObjDiff(newRecent[key], recentDelta.data.diff);
} else {
newRecent = Object.assign(recent.data, recentDelta.data);
}
this.recentDelta = recentDelta.data;
} else {
newRecent = recent.data;
this.recentDelta = {};
}
this.recentDeltaInited = true;
if (!bookManager.loaded) {
this.warning('Ожидание загрузки списка книг перед синхронизацией');
while (!bookManager.loaded) await utils.sleep(100);
}
await bookManager.setRecent(newRecent);
await bookManager.setRecentRev(recent.rev);
await bookManager.setRecentDeltaRev(recentDelta.rev);
} else {
this.warning(`Неверный ответ сервера: ${recent.state}`);
}
if (doNotifySuccess)
this.debouncedNotifySuccess();
} finally {
this.loadingRecent = false;
}
}
async saveRecent(itemKey, recurse) {
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
return;
const bm = bookManager;
//вычисление критерия сохранения целиком
if (!this.sameKeyCount)
this.sameKeyCount = 0;
if (this.prevItemKey == itemKey) {
this.sameKeyCount++;
} else {
this.sameKeyCount = 0;
}
const l = Object.keys(this.recentDelta).length - (1*(!!this.recentDelta.diff));
this.makeDeltaDiff = (l == 1 && this.prevItemKey == itemKey ? this.makeDeltaDiff : false);
const forceSaveRecent = l > 10 || (this.sameKeyCount > 5 && (l > 1)) || (l == 1 && this.sameKeyCount > 10 && !this.makeDeltaDiff);
this.sameKeyCount = (!forceSaveRecent ? this.sameKeyCount : 0);
this.prevItemKey = itemKey;
//дифф от дельты для уменьшения размера передаваемых данных в частном случае
if (this.makeDeltaDiff) {
this.recentDelta.diff = utils.getObjDiff(this.prevSavedItem, bm.recent[itemKey]);
this.recentDelta.diff.key = itemKey;
delete this.recentDelta[itemKey];
} else if (this.recentDelta.diff) {
const key = this.recentDelta.diff.key;
if (!this.prevSavedItem && bm.recent[key])
this.prevSavedItem = _.cloneDeep(bm.recent[key]);
if (this.prevSavedItem) {
this.recentDelta[key] = utils.applyObjDiff(this.prevSavedItem, this.recentDelta.diff);
}
delete this.recentDelta.diff;
}
//сохранение
this.savingRecent = true;
try {
if (forceSaveRecent) {//сохраняем recent целиком
let result = {state: ''};
try {
result = await this.storageSet({recent: {rev: bm.recentRev + 1, data: bm.recent}, recentDelta: {rev: bm.recentDeltaRev + 1, data: {}}});
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
await this.loadRecent(true, false);
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse) {
this.savingRecent = false;
this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
this.saveRecent(itemKey, true);
return;
}
} else if (result.state == 'success') {
this.makeDeltaDiff = true;
this.prevSavedItem = _.cloneDeep(bm.recent[itemKey]);
this.recentDelta = {};
await bm.setRecentRev(bm.recentRev + 1);
await bm.setRecentDeltaRev(bm.recentDeltaRev + 1);
}
} else {//сохраняем только дифф
let result = {state: ''};
try {
result = await this.storageSet({recentDelta: {rev: bm.recentDeltaRev + 1, data: this.recentDelta}});
} catch(e) {
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
}
if (result.state == 'reject') {
await this.loadRecent(true, false);
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse) {
this.savingRecent = false;
this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
this.saveRecent(itemKey, true);
return;
}
} else if (result.state == 'success') {
await bm.setRecentDeltaRev(bm.recentDeltaRev + 1);
}
}
} 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,19 @@
<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 class="slider">
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
</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 +23,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,6 +35,15 @@ class SetPositionPage extends Vue {
created() {
this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
this.initialized = false;
}
init(sliderValue, sliderMax) {
this.$refs.window.init();
this.sliderMax = sliderMax;
this.sliderValue = sliderValue;
this.initialized = true;
}
formatTooltip(val) {
@@ -63,26 +68,6 @@ 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;

File diff suppressed because it is too large Load Diff

View File

@@ -23,17 +23,18 @@ export default class DrawHelper {
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
return '';
const spaceWidth = this.measureText(' ', {});
const font = this.fontByStyle({});
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
let out = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
` color: ${this.textColor}">`;
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);
let y = this.fontSize*this.textShift;
for (let i = 0; i < len; i++) {
const line = lines[i];
/* line:
@@ -43,81 +44,110 @@ export default class DrawHelper {
first: Boolean,
last: Boolean,
parts: array of {
style: {bold: Boolean, italic: Boolean, center: Boolean}
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;
}
let indent = line.first ? this.p : 0;
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 centerStyle = {};
let space = 0;
let j = 0;
//формируем строку
for (const part of line.parts) {
lineText += part.text;
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;
if (part.style.center)
centerStyle = part.style;
}
space = (part.style.space > space ? part.style.space : space);
let filled = false;
// если выравнивание по ширине включено
if (this.textAlignJustify && !line.last && !center) {
const words = lineText.split(' ');
//избражения
//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`;
}
if (words.length > 1) {
const spaceCount = words.length - 1;
const space = (this.w - line.width + spaceWidth*spaceCount)/spaceCount;
let x = indent;
for (const part of line.parts) {
const font = this.fontByStyle(part.style);
let partWords = part.text.split(' ');
for (let j = 0; j < partWords.length; j++) {
let f = font;
let style = part.style;
let word = partWords[j];
if (i == 0 && this.searching && word.toLowerCase().indexOf(this.needle) >= 0) {
style = Object.assign({}, part.style, {bold: true});
f = this.fontByStyle(style);
}
out += this.fillText(word, x, y, f);
x += this.measureText(word, style) + (j < partWords.length - 1 ? space : 0);
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}"/>`;
}
}
filled = true;
imageDrawn.add(img.paraIndex);
}
}
// просто выводим текст
if (!filled) {
let x = indent;
x = (center ? (this.w - this.measureText(lineText, centerStyle))/2 : x);
for (const part of line.parts) {
let font = this.fontByStyle(part.style);
if (i == 0 && this.searching) {//для поиска, разбивка по словам
let partWords = part.text.split(' ');
for (let j = 0; j < partWords.length; j++) {
let f = font;
let style = part.style;
let word = partWords[j];
if (word.toLowerCase().indexOf(this.needle) >= 0) {
style = Object.assign({}, part.style, {bold: true});
f = this.fontByStyle(style);
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`;
}
out += this.fillText(word, x, y, f);
x += this.measureText(word, style) + (j < partWords.length - 1 ? spaceWidth : 0);
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
}
} else {
out += this.fillText(part.text, x, y, font);
x += this.measureText(part.text, part.style);
//
}
}
}
y += this.lineHeight;
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>';
@@ -287,4 +317,56 @@ export default class DrawHelper {
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 = '';
}
}

View File

@@ -1,16 +1,16 @@
<template>
<div ref="main" class="main">
<div class="layout back">
<div class="layout back" @wheel.prevent.stop="onMouseWheel">
<div v-html="background"></div>
<!-- img -->
</div>
<div ref="scrollBox1" class="layout" style="overflow: hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div v-html="page1"></div>
</div>
</div>
<div ref="scrollBox2" class="layout" style="overflow: hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div v-html="page2"></div>
</div>
</div>
@@ -23,7 +23,6 @@
oncontextmenu="return false;">
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div>
<div v-show="fontsLoading" ref="fontsLoading"></div>
</div>
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div>
@@ -77,7 +76,6 @@ class TextPage extends Vue {
page2 = null;
statusBar = null;
statusBarClickable = null;
fontsLoading = null;
lastBook = null;
bookPos = 0;
@@ -133,7 +131,6 @@ class TextPage extends Vue {
}, 10);
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
}
mounted() {
@@ -157,9 +154,10 @@ class TextPage extends Vue {
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
this.w = this.realWidth - 2*this.indentLR;
this.h = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0) - 2*this.indentTB;
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
this.h = this.scrollHeight - 2*this.indentTB;
this.lineHeight = this.fontSize + this.lineInterval;
this.pageLineCount = 1 + Math.floor((this.h - this.fontSize)/this.lineHeight);
this.pageLineCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
this.$refs.scrollingPage1.style.width = this.w + 'px';
this.$refs.scrollingPage2.style.width = this.w + 'px';
@@ -170,6 +168,12 @@ class TextPage extends Vue {
this.fontShift = this.fontVertShift/100;
this.textShift = this.textVertShift/100 + this.fontShift;
//statusBar
this.$refs.statusBar.style.left = '0px';
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
//drawHelper
this.drawHelper.realWidth = this.realWidth;
this.drawHelper.realHeight = this.realHeight;
@@ -195,49 +199,56 @@ class TextPage extends Vue {
this.drawHelper.lineHeight = this.lineHeight;
this.drawHelper.context = this.context;
//сообщение "Загрузка шрифтов..."
const flText = 'Загрузка шрифта...';
this.$refs.fontsLoading.innerHTML = flText;
const fontsLoadingStyle = this.$refs.fontsLoading.style;
fontsLoadingStyle.position = 'absolute';
fontsLoadingStyle.fontSize = this.fontSize + 'px';
fontsLoadingStyle.top = (this.realHeight/2 - 2*this.fontSize) + 'px';
fontsLoadingStyle.left = (this.realWidth - this.drawHelper.measureText(flText, {}))/2 + 'px';
//statusBar
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
//parsed
if (this.parsed) {
this.parsed.p = this.p;
this.parsed.w = this.w;// px, ширина текста
this.parsed.font = this.font;
this.parsed.fontSize = this.fontSize;
this.parsed.wordWrap = this.wordWrap;
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
let t = '';
while (this.drawHelper.measureText(t, {}) < this.w) t += 'Щ';
this.parsed.maxWordLength = t.length - 1;
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
this.parsed.lineHeight = this.lineHeight;
this.parsed.showImages = this.showImages;
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
this.parsed.imageHeightLines = this.imageHeightLines;
this.parsed.imageFitWidth = this.imageFitWidth;
this.parsed.compactTextPerc = this.compactTextPerc;
}
//statusBar
this.$refs.statusBar.style.left = '0px';
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
//scrolling page
const pageDelta = this.h - (this.pageLineCount*this.lineHeight - this.lineInterval);
let y = this.indentTB + pageDelta/2;
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
let y = pageSpace/2;
if (this.showStatusBar)
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
const page1 = this.$refs.scrollBox1;
const page2 = this.$refs.scrollBox2;
page1.style.width = this.w + 'px';
page2.style.width = this.w + 'px';
page1.style.height = (this.h - pageDelta) + 'px';
page2.style.height = (this.h - pageDelta) + 'px';
let page1 = this.$refs.scrollBox1;
let page2 = this.$refs.scrollBox2;
page1.style.perspective = '3072px';
page2.style.perspective = '3072px';
page1.style.width = this.w + this.indentLR + 'px';
page2.style.width = this.w + this.indentLR + 'px';
page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
page2.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
page1.style.top = y + 'px';
page2.style.top = y + 'px';
page1.style.left = this.indentLR + 'px';
page2.style.left = this.indentLR + 'px';
page1 = this.$refs.scrollingPage1;
page2 = this.$refs.scrollingPage2;
page1.style.width = this.w + this.indentLR + 'px';
page2.style.width = this.w + this.indentLR + 'px';
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
}
async checkLoadedFonts() {
@@ -252,6 +263,18 @@ class TextPage extends Vue {
async loadFonts() {
this.fontsLoading = true;
let inst = null;
(async() => {
await sleep(500);
if (this.fontsLoading)
inst = this.$notify({
title: '',
dangerouslyUseHTMLString: true,
message: 'Загрузка шрифта &nbsp;<i class="el-icon-loading"></i>',
duration: 0
});
})();
if (!this.fontsLoaded)
this.fontsLoaded = {};
//загрузка дин.шрифта
@@ -282,6 +305,8 @@ class TextPage extends Vue {
}
this.fontsLoading = false;
if (inst)
inst.close();
}
getSettings() {
@@ -361,13 +386,17 @@ class TextPage extends Vue {
this.meta = bookManager.metaOnly(this.book);
this.fb2 = this.meta.fb2;
const authorName = _.compact([
this.fb2.lastName,
this.fb2.firstName,
this.fb2.middleName
]).join(' ');
let authorNames = [];
if (this.fb2.author) {
authorNames = this.fb2.author.map(a => _.compact([
a.lastName,
a.firstName,
a.middleName
]).join(' '));
}
this.title = _.compact([
authorName,
authorNames.join(', '),
this.fb2.bookTitle
]).join(' - ');
@@ -436,15 +465,7 @@ class TextPage extends Vue {
}
startSearch(needle) {
this.drawHelper.needle = '';
const words = needle.split(' ');
for (const word of words) {
if (word != '') {
this.drawHelper.needle = word;
break;
}
}
this.drawHelper.needle = needle;
this.drawHelper.searching = true;
this.draw();
}
@@ -613,7 +634,7 @@ class TextPage extends Vue {
const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation');
const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation');
//const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100));
let page1 = this.$refs.scrollingPage1;
@@ -633,8 +654,22 @@ class TextPage extends Vue {
duration, this.pageChangeDirectionDown, transition1Finish);
break;
case 'downShift':
page1.style.height = this.scrollHeight + 'px';
page2.style.height = this.scrollHeight + 'px';
await this.drawHelper.doPageAnimationDownShift(page1, page2,
duration, this.pageChangeDirectionDown, transition1Finish);
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
break;
case 'rotate':
await this.drawHelper.doPageAnimationRotate(page1, page2,
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish);
break;
case 'flip':
await this.drawHelper.doPageAnimationFlip(page1, page2,
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish, this.backgroundColor);
break;
}
@@ -936,6 +971,7 @@ class TextPage extends Vue {
this.$emit('full-screen-toogle');
break;
case 'Tab':
case 'KeyQ':
this.doToolBarToggle();
event.preventDefault();
event.stopPropagation();
@@ -970,7 +1006,7 @@ class TextPage extends Vue {
}
onTouchStart(event) {
if (!this.mobile)
if (!this.$isMobileDevice)
return;
this.endClickRepeat();
if (event.touches.length == 1) {
@@ -986,19 +1022,19 @@ class TextPage extends Vue {
}
onTouchEnd() {
if (!this.mobile)
if (!this.$isMobileDevice)
return;
this.endClickRepeat();
}
onTouchCancel() {
if (!this.mobile)
if (!this.$isMobileDevice)
return;
this.endClickRepeat();
}
onMouseDown(event) {
if (this.mobile)
if (this.$isMobileDevice)
return;
this.endClickRepeat();
if (event.button == 0) {
@@ -1014,13 +1050,13 @@ class TextPage extends Vue {
}
onMouseUp() {
if (this.mobile)
if (this.$isMobileDevice)
return;
this.endClickRepeat();
}
onMouseWheel(event) {
if (this.mobile)
if (this.$isMobileDevice)
return;
if (event.deltaY > 0) {
this.doDown();
@@ -1097,6 +1133,14 @@ class TextPage extends Vue {
z-index: 10;
}
.over-hidden {
overflow: hidden;
}
.on-top {
z-index: 100;
}
.back {
z-index: 5;
}
@@ -1162,4 +1206,5 @@ class TextPage extends Vue {
0% { opacity: 1; }
100% { opacity: 0; }
}
</style>

View File

@@ -2,8 +2,14 @@ import he from 'he';
import sax from '../../../../server/core/BookConverter/sax';
import {sleep} 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) {
@@ -43,6 +43,16 @@ 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 = [];
let paraIndex = -1;
let paraOffset = 0;
@@ -51,23 +61,70 @@ export default class BookParser {
index: Number,
offset: Number, //сумма всех length до этого параграфа
length: Number, //длина text без тегов
text: String //текст параграфа (или title или epigraph и т.д) с вложенными тегами
text: String, //текст параграфа с вложенными тегами
cut: Boolean, //параграф - кандидат на сокрытие (cutEmptyParagraphs)
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 (async(resolve, reject) => {
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 = (e) => {
reject(e);
};
i.src = `data:${binaryType};base64,${data}`;
await sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
});
};
const getExternalImageDimensions = (src) => {
return new Promise (async(resolve, reject) => {
const i = new Image();
let resolved = false;
i.onload = () => {
resolved = true;
this.binary[src] = {
w: i.width,
h: i.height,
};
resolve();
};
i.onerror = (e) => {
reject(e);
};
i.src = src;
await sleep(30*1000);
if (!resolved)
reject('Не удалось получить размер изображения');
});
};
const newParagraph = (text, len, addIndex) => {
paraIndex++;
let p = {
index: paraIndex,
offset: paraOffset,
length: len,
text: text,
cut: (!addIndex && (len == 1 && text[0] == ' ')),
addIndex: (addIndex ? addIndex : 0),
};
para[paraIndex] = p;
@@ -82,66 +139,116 @@ export default class BookParser {
}
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];
//добавление пустых (addEmptyParagraphs) параграфов
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
paraIndex--;
paraOffset -= p.length;
for (let i = 0; i < 2; i++) {
newParagraph(' ', 1, i + 1);
}
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;
paraOffset += p.length;
}
paraOffset -= p.length;
//параграф оказался непустой
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
p.length = 0;
p.text = p.text.substr(1);
p.cut = (len == 1 && text[0] == ' ');
}
p.length += len;
p.text += 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;
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 (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
if (!fb2.author)
fb2.author = [];
fb2.author.push({});
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
if (!isFirstBody)
newParagraph(' ', 1);
isFirstBody = false;
}
if (tag == 'title') {
newParagraph(' ', 1);
isFirstTitlePara = true;
bold = true;
center = true;
}
if (tag == 'section') {
if (!isFirstSection)
newParagraph(' ', 1);
isFirstSection = false;
}
if (tag == 'emphasis' || tag == 'strong') {
growParagraph(`<${tag}>`, 0);
}
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;
}
if (tag == 'epigraph') {
italic = true;
space += 1;
}
if (tag == 'poem') {
@@ -149,15 +256,21 @@ 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;
}
@@ -166,17 +279,27 @@ export default class BookParser {
growParagraph(`</${tag}>`, 0);
}
if (tag == 'p') {
inPara = false;
}
if (tag == 'subtitle') {
isFirstTitlePara = false;
bold = false;
}
if (tag == 'epigraph') {
italic = false;
space -= 1;
}
if (tag == 'stanza') {
newParagraph(' ', 1);
}
if (tag == 'text-author') {
space -= 1;
}
}
path = path.substr(0, path.length - tag.length - 1);
@@ -194,23 +317,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,7 +365,9 @@ 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>' : '');
@@ -247,13 +376,11 @@ export default class BookParser {
}
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);
}
growParagraph(`${tOpen}${text}${tClose}`, text.length);
}
if (binaryId) {
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
}
};
@@ -266,6 +393,14 @@ export default class BookParser {
onStartNode, onEndNode, onTextNode, onProgress
});
if (dimPromises.length) {
try {
await Promise.all(dimPromises);
} catch (e) {
//
}
}
this.fb2 = fb2;
this.para = para;
@@ -301,19 +436,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 +462,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 +512,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 +527,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);
}
@@ -432,7 +615,13 @@ export default class BookParser {
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;
@@ -442,6 +631,16 @@ export default class BookParser {
wordWrap: this.wordWrap,
maxWordLength: this.maxWordLength,
font: this.font,
cutEmptyParagraphs: this.cutEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs,
showImages: this.showImages,
imageHeightLines: this.imageHeightLines,
imageFitWidth: this.imageFitWidth,
compactTextPerc: this.compactTextPerc,
visible: !(
(this.cutEmptyParagraphs && para.cut) ||
(para.addIndex > this.addEmptyParagraphs)
)
};
@@ -453,6 +652,7 @@ 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,
}
}*/
@@ -463,16 +663,80 @@ export default class BookParser {
let str = '';//измеряемая строка
let prevStr = '';//строка без крайнего слова
let prevW = 0;
let j = 0;//номер строки
let style = {};
let ofs = 0;//смещение от начала параграфа para.offset
// тут начинается самый замес, перенос по слогам и стилизация
let imgW = 0;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) {
const words = part.text.split(' ');
style = part.style;
//изображения
if (part.image.id && !part.image.inline) {
parsed.visible = this.showImages;
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 = '';
for (let i = 0; i < words.length; i++) {
@@ -484,10 +748,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 +765,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 +775,6 @@ export default class BookParser {
}
if (pw) {
prevW = pw;
partText += ss + (ss[ss.length - 1] == '-' ? '' : '-');
wordTail = slogi.join('');
}
@@ -524,7 +788,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 +795,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 +803,7 @@ export default class BookParser {
partText = '';
sp2 = '';
str = wordTail;
imgW = 0;
j++;
}
@@ -548,7 +811,6 @@ export default class BookParser {
partText += sp2 + wordTail;
sp1 = ' ';
sp2 = ' ';
prevW = w;
}
if (partText != '')
@@ -560,14 +822,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);
@@ -614,16 +874,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 +894,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 +917,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,9 +1,10 @@
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 = 300*1024*1024;//compressed bytes
const bmMetaStore = localForage.createInstance({
name: 'bmMetaStore'
@@ -19,32 +20,87 @@ const bmRecentStore = localForage.createInstance({
class BookManager {
async init(settings) {
this.loaded = false;
this.settings = settings;
this.eventListeners = [];
this.books = {};
this.recent = {};
this.recentChanged1 = true;
this.recentChanged2 = true;
this.recentLast = await bmRecentStore.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
this.recentRev = await bmRecentStore.getItem('recent-rev') || 0;
this.recentDeltaRev = await bmRecentStore.getItem('recent-delta-rev') || 0;
this.recentChanged = true;
this.loadStored();//no await
}
//Долгая асинхронная загрузка из хранилища.
//Хранение в отдельных записях дает относительно
//нормальное поведение при нескольких вкладках с читалкой в браузере.
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);
}
}
}
let key = null;
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;
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStore.key(i);
if (key) {
let r = await bmRecentStore.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStore.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await this.cleanBooks();
await this.cleanRecentBooks();
this.recentChanged = true;
this.loaded = true;
this.emit('load-stored-finish');
}
async cleanBooks() {
@@ -54,7 +110,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;
@@ -70,49 +127,168 @@ 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 bmDataStore.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 bmDataStore.getItem(`bmMeta-${meta.key}`);
if (result)
this.books[meta.key] = result;
}
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;
}
@@ -120,9 +296,6 @@ class BookManager {
}
async delBook(meta) {
if (!this.books)
await this.init();
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
await bmDataStore.removeItem(`bmData-${meta.key}`);
@@ -130,15 +303,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
});
@@ -147,7 +317,7 @@ class BookManager {
metaOnly(book) {
let result = Object.assign({}, book);
delete result.data;
delete result.data;//можно будет убрать эту строку со временем
delete result.parsed;
return result;
}
@@ -156,85 +326,98 @@ 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 setRecentBook(value) {
const result = this.metaOnly(value);
result.touchTime = Date.now();
result.deleted = 0;
if (result.textLength && !result.bookPos && result.bookPosPercent)
result.bookPos = Math.round(result.bookPosPercent*result.textLength);
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;
}
this.recent[result.key] = result;
await bmRecentStore.setItem(result.key, result);
await this.cleanRecentBooks();
this.recentChanged1 = true;
this.recentChanged2 = true;
this.recentLast = result;
await bmRecentStore.setItem('recent-last', this.recentLast);
this.recentChanged = true;
this.emit('recent-changed', result.key);
return result;
}
async getRecentBook(value) {
if (!this.recent)
await this.init();
return this.recent[value.key];
let result = this.recent[value.key];
if (!result) {
result = await bmRecentStore.getItem(value.key);
if (result)
this.recent[value.key] = result;
}
return result;
}
async delRecentBook(value) {
if (!this.recent)
await this.init();
this.recent[value.key].deleted = 1;
await bmRecentStore.setItem(value.key, this.recent[value.key]);
await bmRecentStore.removeItem(value.key);
delete this.recent[value.key];
this.recentChanged1 = true;
this.recentChanged2 = true;
if (this.recentLast.key == value.key) {
this.recentLast = null;
await bmRecentStore.setItem('recent-last', this.recentLast);
}
this.emit('recent-deleted', value.key);
this.emit('recent-changed', value.key);
}
async cleanRecentBooks() {
if (!this.recent)
await this.init();
const sorted = this.getSortedRecent();
if (Object.keys(this.recent).length > 1000) {
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++) {
await bmRecentStore.removeItem(sorted[i].key);
delete this.recent[sorted[i].key];
await bmRecentStore.removeItem(sorted[i].key);
isDel = true;
}
this.sortedRecentCached = null;
if (isDel)
this.emit('recent-changed');
return isDel;
}
mostRecentBook() {
if (!this.recentChanged1 && this.mostRecentCached) {
return this.mostRecentCached;
if (this.recentLast) {
return this.recentLast;
}
const oldRecentLast = this.recentLast;
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;
this.recentChanged1 = false;
this.recentLast = result;
bmRecentStore.setItem('recent-last', this.recentLast);//no await
if (this.recentLast !== oldRecentLast)
this.emit('recent-changed');
return result;
}
getSortedRecent() {
if (!this.recentChanged2 && this.sortedRecentCached) {
if (!this.recentChanged && this.sortedRecentCached) {
return this.sortedRecentCached;
}
@@ -243,10 +426,64 @@ class BookManager {
result.sort((a, b) => b.touchTime - a.touchTime);
this.sortedRecentCached = result;
this.recentChanged2 = false;
this.recentChanged = false;
return result;
}
async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent);
Object.assign(mergedRecent, value);
//"ленивое" обновление хранилища
(async() => {
for (const rec of Object.values(mergedRecent)) {
if (rec.key) {
await bmRecentStore.setItem(rec.key, rec);
await utils.sleep(1);
}
}
})();
this.recent = mergedRecent;
this.recentLast = null;
await bmRecentStore.setItem('recent-last', this.recentLast);
this.recentChanged = true;
this.emit('set-recent');
this.emit('recent-changed');
}
async setRecentRev(value) {
await bmRecentStore.setItem('recent-rev', value);
this.recentRev = value;
}
async setRecentDeltaRev(value) {
await bmRecentStore.setItem('recent-delta-rev', value);
this.recentDeltaRev = value;
}
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);
}
}
}
}

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,153 @@
export const versionHistory = [
{
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,10 +1,15 @@
<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" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
<div ref="windowBox" class="windowBox" @click.stop>
<div class="window">
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
<span class="header-text"><slot name="header"></slot></span>
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
</div>
<slot></slot>
</div>
</div>
<slot></slot>
</div>
</template>
@@ -14,17 +19,116 @@ 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 },
}
})
class Window extends Vue {
close() {
this.$emit('close');
init() {
this.$nextTick(() => {
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';
});
}
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>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 50;
}
.windowBox {
position: absolute;
display: flex;
height: 100%;
width: 100%;
}
.window {
flex: 1;
display: flex;
@@ -39,9 +143,9 @@ class Window extends Vue {
.header {
display: flex;
justify-content: flex-end;
background-color: #e5e7ea;
background-color: #59B04F;
align-items: center;
height: 40px;
height: 30px;
}
.header-text {
@@ -54,8 +158,12 @@ class Window extends Vue {
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

@@ -86,6 +86,9 @@ import './theme/form-item.css';
import ElColorPicker from 'element-ui/lib/color-picker';
import './theme/color-picker.css';
import ElDialog from 'element-ui/lib/dialog';
import './theme/dialog.css';
import Notification from 'element-ui/lib/notification';
import './theme/notification.css';
@@ -95,12 +98,15 @@ import './theme/loading.css';
import MessageBox from 'element-ui/lib/message-box';
import './theme/message-box.css';
//import Message from 'element-ui/lib/message';
//import './theme/message.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,
ElColorPicker, ElDialog,
};
for (let name in components) {

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html manifest="/app/manifest.appcache">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
@@ -9,5 +9,6 @@
</head>
<body>
<div id="app"></div>
<script src="https://yastatic.net/share2/share.js" async="async"></script>
</body>
</html>

View File

@@ -1,11 +1,12 @@
import Vue from 'vue';
import App from './components/App.vue';
import router from './router';
import store from './store';
import './element';
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,

View File

@@ -2,21 +2,25 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import _ from 'lodash';
import App from './components/App.vue';
//немедленная загрузка
import CardIndex from './components/CardIndex/CardIndex.vue';
//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
const Search = () => import('./components/CardIndex/Search/Search.vue');
const Card = () => import('./components/CardIndex/Card/Card.vue');
const Book = () => import('./components/CardIndex/Book/Book.vue');
const History = () => import('./components/CardIndex/History/History.vue');
const Reader = () => import('./components/Reader/Reader.vue');
//немедленная загрузка
//const Reader = () => import('./components/Reader/Reader.vue');
import Reader from './components/Reader/Reader.vue';
//const Forum = () => import('./components/Forum/Forum.vue');
const Income = () => import('./components/Income/Income.vue');
const Sources = () => import('./components/Sources/Sources.vue');
const Settings = () => import('./components/Settings/Settings.vue');
const Help = () => import('./components/Help/Help.vue');
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
const myRoutes = [
['/', null, null, '/cardindex'],

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()}`;
}
}
@@ -62,4 +79,118 @@ 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) {
const result = {__isDiff: true, change: {}, add: {}, del: []};
for (const key of Object.keys(oldObj)) {
if (newObj.hasOwnProperty(key)) {
if (!_.isEqual(oldObj[key], newObj[key])) {
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
} else {
result.change[key] = _.cloneDeep(newObj[key]);
}
}
} else {
result.del.push(key);
}
}
for (const key of Object.keys(newObj)) {
if (!oldObj.hasOwnProperty(key)) {
result.add[key] = _.cloneDeep(newObj[key]);
}
}
return result;
}
export function isObjDiff(diff) {
return (_.isObject(diff) && diff.__isDiff);
}
export function isEmptyObjDiff(diff) {
return (!_.isObject(diff) || !diff.__isDiff ||
(!Object.keys(diff.change).length &&
!Object.keys(diff.add).length &&
!diff.del.length
)
);
}
export function applyObjDiff(obj, diff, isAddChanged) {
const 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], isAddChanged);
} else {
result[key] = _.cloneDeep(change[key]);
}
} else if (isAddChanged) {
result[key] = _.cloneDeep(change[key]);
}
}
for (const key of Object.keys(diff.add)) {
result[key] = _.cloneDeep(diff.add[key]);
}
for (const key of diff.del) {
delete result[key];
}
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;
}

View File

@@ -1,4 +1,16 @@
import Vue from 'vue';
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
const toolButtons = [
{name: 'undoAction', show: true, text: 'Действие назад'},
{name: 'redoAction', show: true, text: 'Действие вперед'},
{name: 'fullScreen', show: true, text: 'На весь экран'},
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
{name: 'setPosition', show: true, text: 'На страницу'},
{name: 'search', show: true, text: 'Найти в тексте'},
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
];
const fonts = [
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
@@ -6,7 +18,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},
];
@@ -125,57 +137,77 @@ 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,
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
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание
pageChangeAnimationSpeed: 80, //0-100%
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
pageChangeAnimationSpeed: 80, //0-100%
allowUrlParamBookPos: false,
lazyParseEnabled: false,
copyFullText: false,
showClickMapPage: true,
clickControl: true,
cutEmptyParagraphs: false,
addEmptyParagraphs: 0,
blinkCachedLoad: true,
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,
showMigrationDialog: true,
fontShifts: {},
fontShifts: {},
showToolButton: {},
};
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;
// initial state
const state = {
toolBarActive: true,
serverSyncEnabled: false,
serverStorageKey: '',
profiles: {},
profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '',
migrationRemindDate: '',
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settingsRev: {},
};
// getters
@@ -189,12 +221,40 @@ 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;
},
setMigrationRemindDate(state, value) {
state.migrationRemindDate = value;
},
setCurrentProfile(state, value) {
state.currentProfile = value;
},
setSettings(state, value) {
state.settings = Object.assign({}, state.settings, value);
}
},
setSettingsRev(state, value) {
state.settingsRev = Object.assign({}, state.settingsRev, value);
},
};
export default {
toolButtons,
fonts,
webFonts,
settingDefaults,

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
#RewriteEngine On
#RewriteCond %{HTTP_HOST} ^www.bookpauk.ru$ [NC]
#RewriteRule ^(.*)$ http://bookpauk.ru/$1 [R=301,L]
Options None
Options +ExecCGI

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,2 @@
siteroot = 'http://old.omnireader.ru/';
doRedirect = '';

View File

@@ -0,0 +1,5 @@
<?php
$siteroot = 'http://old.omnireader.ru/';
$use_gzip = false;
$tmp_dir = '/tmp';
?>

447
docs/omnireader/old/f.php Normal file
View File

@@ -0,0 +1,447 @@
<?php
date_default_timezone_set('Europe/Moscow');
require_once 'config/config.php';
require_once 'parser.php';
define('LOWERCASE',3);
define('UPPERCASE',1);
function getParam($param, $defaultValue = '') {
$paramValue = (isset($_REQUEST[$param]) ? $_REQUEST[$param] : $defaultValue);
return $paramValue;
}
function getEncoding($str, $check_utf = FALSE) {
if (!$check_utf) {
$result = getEncoding(mb_convert_encoding($str, 'cp1251', 'UTF-8'), TRUE);
if ($result == 'w')
return 'u';
}
$charsets = Array(
'k' => 0,
'w' => 0,
'd' => 0,
'i' => 0,
'm' => 0
);
$length = strlen($str);
$block_size = ($length > 5*3000) ? 3000 : $length;
$counter = 0;
for ( $i = 0; $i < $length; $i++ ) {
$char = ord($str[$i]);
//non-russian characters
if ($char < 128 || $char > 256)
continue;
//CP866
if (($char > 159 && $char < 176) || ($char > 223 && $char < 242)) $charsets['d']+=LOWERCASE;
if (($char > 127 && $char < 160)) $charsets['d']+=UPPERCASE;
//KOI8-R
if (($char > 191 && $char < 223)) $charsets['k']+=LOWERCASE;
if (($char > 222 && $char < 256)) $charsets['k']+=UPPERCASE;
//WIN-1251
if ($char > 223 && $char < 256) $charsets['w']+=LOWERCASE;
if ($char > 191 && $char < 224) $charsets['w']+=UPPERCASE;
//MAC
if ($char > 221 && $char < 255) $charsets['m']+=LOWERCASE;
if ($char > 127 && $char < 160) $charsets['m']+=UPPERCASE;
//ISO-8859-5
if ($char > 207 && $char < 240) $charsets['i']+=LOWERCASE;
if ($char > 175 && $char < 208) $charsets['i']+=UPPERCASE;
$counter++;
if ($counter > $block_size) {
$counter = 0;
$i += (int)($length/2 - 2*$block_size);
}
}
arsort($charsets);
if (preg_match('//u', $str))
return 'u';
else
return key($charsets);
}
function getTag($tagName, $book) {
$from_tag = '<' . $tagName . '>';
$to_tag = '</' . $tagName . '>';
$from = strpos($book, $from_tag);
$to = strpos($book, $to_tag);
if ($from === FALSE || $to === FALSE)
return '';
$from += strlen($from_tag);
return trim(substr($book, $from, $to - $from));
}
function getMetaInfoAndFilter($book, &$meta_info) {
$meta_info['author'] = '';
$meta_info['title'] = getTag('title', $book);
$out = $book;
//fb2 ??? ---------------------
if (strpos($meta_info['title'], '<p>') !== FALSE) {
$s = str_replace('</p>', '', $meta_info['title']);
$a = explode('<p>', $s);
$meta_info['author'] = parseHtml($a[1], TRUE);
$meta_info['title'] = parseHtml($a[2], TRUE);
if ($meta_info['title'] === NULL || $meta_info['title'] === '') {
$s = parseHtml($s, TRUE);
$meta_info['author'] = '';
$meta_info['title'] = $s;
}
}
//samlib ----------------------
$samlib_start_sign = '<!----------- Ñîáñòâåííî ïðîèçâåäåíèå --------------->';
$samlib_book_idx = strpos($book, $samlib_start_sign);
if ($samlib_book_idx !== FALSE) {
$samlib_author = getTag('h3', $book);
$meta_info['author'] = substr($samlib_author, 0, strpos($samlib_author, ': <small>'));
$meta_info['title'] = getTag('h2', $book);;
$samlib_book_idx += strlen($samlib_start_sign);
$samlib_book_end_idx = strpos($book, '<!---- Áëîê îïèñàíèÿ ïðîèçâåäåíèÿ (ñëåâà âíèçó) ----------------------->');
$samlib_book_end_idx = ($samlib_book_end_idx === FALSE ? strlen($book) : $samlib_book_end_idx);
$out = '<dd>' . $meta_info['author'] . '<dd>' . $meta_info['title'] . '<empty-line/>' .
substr($book, $samlib_book_idx, $samlib_book_end_idx - $samlib_book_idx);
$out = preg_replace("/<dd>&nbsp;&nbsp[;]*\s*[\r\n]/", '<empty-line/>', $out);
}
return $out;
}
function filterTextAndGzip($meta_info, $txtin) {
global $use_gzip;
if (strpos($txtin, '<P>') === FALSE) {
$len = strlen($txtin);
$counts = array();
$flag = 0;
$c = 0;
for ($i = 0; $i < $len; $i++) {
if ($txtin[$i] == chr(10) || $i == 0) {
$counts[$c]++;
if ($c > 0)
$counts[0]++;
$c = 0;
$flag = 1;
} else
if ($txtin[$i] != ' ')
$flag = 0;
else
if ($flag)
$c++;
}
arsort($counts);
$key = 0;
if (count($counts) > 1) {
next($counts);
$key = key($counts);
}
//$txtout .= print_r($counts, TRUE);
//$txtout .= $key;
$txtout = '';
$flag = 0;
$c = 0;
for ($i = 0; $i < $len; $i++) {
if ($txtin[$i] == chr(10) || $i == 0) {
$c = 0;
$flag = 1;
} else
if ($txtin[$i] != ' ') {
if ($c >= $key && $flag)
$txtout .= '<p>';
$flag = 0;
}
else
if ($flag)
$c++;
$txtout .= $txtin[$i];
}
} else
$txtout = $txtin;
$txtout = 'no_file' . '|' . $meta_info['author'] . '|' . $meta_info['title'] .
'<<<bpr5A432688AB0467AA396E5A144830248Abpr>>>' . $txtout;
$supportsGzip = strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false;
if ($use_gzip && $supportsGzip && getParam('meta') == '' && getParam('curl') == '') {
$txtout = gzencode($txtout, 9);
header('Content-Encoding: gzip');
}
return $txtout;
}
function myErrorHandler($errno, $errstr, $errfile, $errline)
{
if (!(error_reporting() & $errno)) {
// Ýòîò êîä îøèáêè íå âêëþ÷åí â error_reporting
return;
}
if ($errno == 8 /*|| $errno == 2*/)
return;
//throw new Exception("[$errno]: ($errstr) at $errfile line $errline");
throw new Exception("$errstr");
// Íå çàïóñêàåì âíóòðåííèé îáðàáîò÷èê îøèáîê PHP
return TRUE; // ñþäà õîäà íåò, íî ïóñòü áóäåò êàê øàáëîí
}
function unzip($filein) {
$zip = new ZipArchive;
$result = '';
if ($zip->open($filein) === TRUE) {
$filename = '';
$max_size = -1;
for($i = 0; $i < $zip->numFiles; $i++) {
$stat = $zip->statIndex($i);
$size = $stat['size'];
if ($size > $max_size) {
$max_size = $size;
$filename = $zip->getNameIndex($i);
$fp = $zip->getStream($filename);
if (!$fp)
throw new Exception("zip->getStream failed");
$result = stream_get_contents($fp);
fclose($fp);
}
}
$zip->close();
} else
throw new Exception("zip->open failed");
return $result;
}
function create_guid($namespace = '') {
$uid = md5(uniqid("", true));
$data = $namespace;
$data .= $_SERVER['REQUEST_TIME'];
$data .= $_SERVER['HTTP_USER_AGENT'];
$data .= $_SERVER['LOCAL_ADDR'];
$data .= $_SERVER['LOCAL_PORT'];
$data .= $_SERVER['REMOTE_ADDR'];
$data .= $_SERVER['REMOTE_PORT'];
$hash = strtoupper(hash('ripemd128', $uid . $guid . md5($data)));
return $hash;
}
function microtime_float()
{
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
function curlExec(/* Array */$curlOptions='', /* Array */$curlHeaders='', /* Array */$postFields='')
{
$newUrl = '';
$maxRedirection = 10;
do
{
if ($maxRedirection<1) die('Error: reached the limit of redirections');
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
if (!empty($curlOptions)) curl_setopt_array($ch, $curlOptions);
if (!empty($curlHeaders)) curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
if (!empty($postFields))
{
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
}
if (!empty($newUrl)) curl_setopt($ch, CURLOPT_URL, $newUrl); // redirect needed
curl_setopt($ch, CURLOPT_HEADER, 1);
$response = curl_exec($ch);
// Then, after your curl_exec call:
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$curlResult = substr($response, $header_size);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$info = curl_getinfo($ch);
if (getParam('curl') != '') {;
throw new Exception("<br>" . str_replace("[", "<br>[", print_r($info, TRUE)) . "<br>$header<br>END");
}
if ($code == 301 || $code == 302 || $code == 303 || $code == 307)
{
if (array_key_exists('redirect_url', $info) && !empty($info['redirect_url'])) {
$newUrl = trim($info['redirect_url']);
} else {
preg_match('/Location:(.*?)\n/', $header, $matches);
$newUrl = trim(array_pop($matches));
}
curl_close($ch);
$maxRedirection--;
continue;
}
else // no more redirection
{
if ($curlResult === FALSE || $info['http_code'] != 200) {
$curlResult = "ERROR ". $info['http_code'];
if (curl_error($ch))
$curlResult .= "<br>". curl_error($ch);
throw new Exception($curlResult);
} else {
$code = 0; //OK
curl_close($ch);
}
}
}
while($code);
return $curlResult;
}
{
set_error_handler("myErrorHandler");
// set_time_limit(300);
$url = getParam('url');
try {
$body = '';
if ($url == '')
throw new Exception("íå çàäàí àäðåñ êíèãè");
$meta_info = array();
$time_start = $time = microtime_float();
$pid = create_guid();
$dir = 'txt/';
$encoding = getParam('encoding');
if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0)
$url = 'http://' . $url;
$url = str_replace('"', '', $url);
$url = str_replace('\'', '', $url);
$url = str_replace(']', '%5D', str_replace('[', '%5B', $url));
$options = array(
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_TIMEOUT => 300,
CURLOPT_URL => $url,
CURLOPT_BUFFERSIZE => 1024*128,
CURLOPT_NOPROGRESS => FALSE,
CURLOPT_USERAGENT => "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6",
CURLOPT_PROGRESSFUNCTION => function(
$DownloadSize, $Downloaded, $UploadSize, $Uploaded
) {
// If $Downloaded exceeds, returning non-0 breaks the connection!
return ($Downloaded > (50 * 1024 * 1024)) ? 1 : 0;
}
);
$out = curlExec($options);
$meta_info['time_curl'] = microtime_float() - $time;
$time = microtime_float();
//zip
if ($out[0] == chr(0x50) && $out[1] == chr(0x4B) && $out[2] == chr(0x03) && $out[3] == chr(0x04)) {
$zipped_file = $tmp_dir . "/{$pid}-temp.zip";
file_put_contents($zipped_file, $out);
$out = unzip($zipped_file);
if (file_exists($zipped_file)) unlink($zipped_file);
}
//pdf
/* if ($out[0] == chr(0x25) && $out[1] == chr(0x50) && $out[2] == chr(0x44) && $out[3] == chr(0x46)) {
$a = new PDF2Text();
$a->reset();
$a->decodePDF($out);
$out = $a->output();
file_put_contents('/tmp/1', $out);
}*/
$meta_info['time_unzip'] = microtime_float() - $time;
$time = microtime_float();
//decoding and parsing
if ($out !== FALSE) {
if ($encoding == '')
$encoding = getEncoding($out);
switch ($encoding) {
case 'k':
$out = mb_convert_encoding($out, 'cp1251', 'KOI8-R');
break;
case 'w':
break;
case 'd':
$out = mb_convert_encoding($out, 'cp1251', 'cp866');
break;
case 'i':
$out = mb_convert_encoding($out, 'cp1251', 'ISO-8859-5');
break;
case 'm':
$out = mb_convert_encoding($out, 'cp1251', 'MACINTOSH');
break;
case 'u':
$out = mb_convert_encoding($out, 'cp1251', 'UTF-8');
break;
}
//$out = $encoding . '===' . $out;
//file_put_contents('/tmp/bpr1', $out);
$meta_info['time_decodepage'] = microtime_float() - $time;
$time = microtime_float();
$out = getMetaInfoAndFilter($out, $meta_info);
$meta_info['time_metainfo'] = microtime_float() - $time;
$time = microtime_float();
$out = parseHtml($out);
$meta_info['time_parsehtml'] = microtime_float() - $time;
$time = microtime_float();
$out = filterTextAndGzip($meta_info, $out);
$meta_info['time_filter_gzip'] = microtime_float() - $time;
$meta_info['time_total'] = microtime_float() - $time_start;
$meta = getParam('meta');
if ($meta != '') {
$info = '';
foreach ($meta_info as $key => $value) {
if (strpos($key, 'time') !== FALSE)
$info .= sprintf("%06.3f", $value) . " $key <br>";
else
$info .= "$key: $value<br>";
}
throw new Exception("<br>" . $info);
}
header('Content-Type: text/plain; charset=windows-1251');
echo $out;
//file_put_contents('/tmp/bpr2', $out);
return;
} else
throw new Exception("îøèáêà çàãðóçêè ôàéëà. Ïîïðîáóéòå åùå ðàç.");
} catch (Exception $e) {
header('Content-Type: text/html; charset=windows-1251');
$err = $e->getMessage();
if (strpos($err, 'ERROR 404') !== FALSE)
$err = 'ñòðàíèöà íå íàéäåíà';
$body = "Îøèáêà çàãðóçêè êíèãè: " . ($url == '' ? '' : "($url) ") . $err;
}
echo $body;
}
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,279 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<meta http-equiv="Content-Language" content="ru">
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi" />
<meta name="description" content="áðàóçåðíàÿ îíëàéí-÷èòàëêà êíèã èç èíòåðíåòà">
<meta name="keywords" content="îíëàéí,÷èòàëêà,êíèãè,÷èòàòü,áðàóçåð,èíòåðíåò">
<title>Omni Reader - áðàóçåðíàÿ îíëàéí-÷èòàëêà</title>
<link rel="icon" type="image/gif" href="js/bpricon.gif">
<link rel="shortcut icon" type="image/gif" href="js/bpricon.gif">
<link rel="stylesheet" type="text/css" media="screen" href="js/stylex.css">
<script src="config/config.js" type="text/javascript"></script>
<script src="js/bpr318.js" type="text/javascript"></script>
<meta name="yandex-verification" content="be58752dfe93d304" />
</head>
<body onload="onLoa();">
<div style="display: none;" id="loading">
<img style="float:left;padding: 0; margin: 0" src="js/load.gif"></img>
</div>
<noscript>
<div class="over">JavaScript disabled
</div>
</noscript>
<hr id="fhr">
<div id="win">
<div id="header" class="header">
</div>
<!-- MAIN DIV -->
<div id="main">
</div>
</div>
<div class="podk" id="comm">
<div style="background-color: rgb(34, 34, 0); color: rgb(235, 226, 201);" class="menuCross" id="mcross" onclick="menu();return false;">x
</div>
<br>
<div style="text-decoration: underline; text-align: center"><span>ÌÅÍÞ</span></div>
<div class="melem">
<br>
<a href="/?#" onclick="goTo();return false;">Íà ñòðàíèöó:&nbsp;</a>
<b onmousedown="gpa=1;pageGoto(1);" onmouseup="gpa=0;" onmouseout="gpa=0;">&lt;</b>
<input value="1" style="color: rgb(34, 34, 0);" id="pageGoto" size="3" maxlength="4" type="text" onkeydown="if (event.keyCode == 13) {goTo();return false;}">
<b onmousedown="gpa=1;pageGoto(0);" onmouseup="gpa=0;" onmouseout="gpa=0;">&gt;</b>
&nbsp;
<a href="/?#" onclick="goTo();return false;">Ok</a>
<br>
<a href="/?#" onclick="return statusPanel();">Ïàíåëü ñòàòóñà(px):&nbsp;</a>
<input style="color: rgb(34, 34, 0);" id="id_sp_size" size="3" maxlength="3" type="text" onkeydown="if (event.keyCode == 13) return statusPanel();">
<a href="/?#" onclick="return statusPanel();">Ok</a>
<br>
<a href="/?#" onclick="return statusPanel();">Ñêðîëëèíã(ms):&nbsp;</a>
<input style="color: rgb(34, 34, 0);" id="id_sc_int" size="3" maxlength="3" type="text" onkeydown="if (event.keyCode == 13) return scrollInterval();">
<a href="/?#" onclick="return scrollInterval();">Ok</a>
<br>
<label>Ïîëîñà ïðîêðóòêè<input id="id_scroll" type="checkbox" onclick="return showScroll()"></label>
<br>
<label>Óïðàâëåíèå êëèêîì<input id="id_by_click" type="checkbox" onclick="return byClick()"></label>
<br><br>
<a href="/?#Íàñòðîèòü öâåò è øðèôò" onclick="colorMenu();return false;">Íàñòðîèòü öâåò è øðèôò</a>
<br>
<a href="/?#Ñïðàâêà" onclick="legend(1);return false;">Ñïðàâêà</a>
<br>
</div>
</div>
<div style="top: 172px; left: 423px" id="colorSelect">
<div style="background-color: rgb(34, 34, 0); color: rgb(235, 226, 201);" class="colorCross" id="cross" onclick="colorClose(); return false;">x
</div>
<div class="colorImage" onclick="colorSet(); return false;">
</div>
<div style="border-color: rgb(136, 132, 100);" class="colorMenu">
<div style="position:absolute;top:180px;left:0px;">
<div onclick="colorFontTemp=1; setSelectedFontItem(); return false;" onmouseover="this.style.backgroundColor='#0FA';" onmouseout="this.style.background='none';">
<span id="fontItemText">&nbsp;Òåêñò</span>
</div>
<input onchange="colorChange(1,this.value);" id="fcolor" size="8" maxlength="7" type="text" onkeydown="if (event.keyCode == 13) {colorChange(1,this.value);return false;}">
<br>
<div onclick="colorFontTemp=0; setSelectedFontItem(); return false;" onmouseover="this.style.backgroundColor='#0FA';" onmouseout="this.style.background='none';">
<span id="fontItemBack">&nbsp;Ôîí</span>
</div>
<input onchange="colorChange(2,this.value);" id="bcolor" size="8" maxlength="7" type="text" onkeydown="if (event.keyCode == 13) {colorChange(2,this.value);return false;}">
<br>
<div>
&nbsp;Ðàçìåð
</div>
<input onchange="colorChange(3,this.value);" id="fsize" size="8" maxlength="3" type="text" onkeydown="if (event.keyCode == 13) {colorChange(3,this.value);return false;}">
<br>
<div>
&nbsp;Øðèôò
</div>
<select id="ffamily" onchange="gdb('ffamily2').value='';colorChange(4,this.value);">
<option>Trebuchet Ms</option>
<option>Serif</option>
<option>Arial</option>
<option>Times New Roman</option>
<option>Sans-Serif</option>
<option>Tahoma</option>
<option>Verdana</option>
<option>Lucida Sans Unicode</option>
</select>
<br>
<div>
&nbsp;Ñâîé
</div>
<input onchange="colorChange(4,this.value);" id="ffamily2" size="8" maxlength="70" type="text" onkeydown="if (event.keyCode == 13) {colorChange(4,this.value);return false;}">
</div>
<div class="colorExample" style="cursor: pointer; height: 24px; bottom: 0px; background-color: rgb(204, 238, 255); border-top: 2px solid rgb(136, 132, 100); line-height: 26px; border-color: rgb(136, 132, 100);" onmouseover="this.style.backgroundColor='#CFC';" onmouseout="this.style.backgroundColor='#CEF';" onclick="colorSubmit();">
OK
</div>
<div id="clrdiv" class="colorExample" style="top: 78px; border-bottom: 2px solid rgb(136, 132, 100); color: rgb(34, 34, 0); background-color: rgb(235, 226, 201); border-color: rgb(136, 132, 100);overflow: hidden;">
Ïðèìåð.
</div>
<div id="clrdiv2" class="colorExample" style="top: 0px; font-size: 21px; color: rgb(34, 34, 0); background-color: rgb(235, 226, 201);overflow: hidden;">
Ðåçóëüòàò.
</div>
</div>
</div>
<div id="dop" style="display:none">
<center>
<br><h4><strong>Omni Reader - áðàóçåðíàÿ îíëàéí-÷èòàëêà.</strong></h4>
<br>Äîáðî ïîæàëîâàòü!
<br><br><br>
<div class="addr">
<div align="left" style="text-indent: 0">Àäðåñ êíèãè (URL):
</div>
<input class="book" id="book" style="width:70%" type="text" onkeydown="if (event.keyCode == 13) gdb('btnOk').click()">
<input class="book" id="btnOk" value="ÎÊ" onclick="location.href=siteroot + '?url=' + gdb('book').value;" type="button">
<div align="left" style="text-indent: 0;font-size: 12px;line-height:16px">
<b><a href="http://samlib.ru/comment/b/bookpauk/bookpauk_reader" target="_blank">Êîììåíòèðîâàòü</a></b>
</div>
</div>
<div id="id_add"></div>
</center>
<br><br><strong>Âîçìîæíîñòè ÷èòàëêè:</strong>
<p>- çàãðóçêà ëþáîé ñòðàíèöû èíòåðíåòà
<p>- ïîäêëþ÷åíèå ê èíòåðíåòó íå îáÿçàòåëüíî äëÿ ÷òåíèÿ êíèãè ïîñëå åå çàãðóçêè
<p>- âîçìîæíîñòü èçìåíèòü öâåò ôîíà, òåêñòà, ðàçìåð è òèï øðèôòà, ðàçìåð ïàíåëè ñòàòóñà
<p>- çàïîìèíàíèå òåêóùåé ïîçèöèè è íàñòðîåê â áðàóçåðå
<p>- ïåðåõîä íà çàäàííóþ ñòðàíèöó
<p>- ïëàâíûé ñêðîëëèíã òåêñòà
<p>- óïðàâëåíèå êëèêîì äëÿ ñåíñîðíûõ ýêðàíîâ
<p>- ðåãèñòðàöèÿ íå òðåáóåòñÿ
<br><br> êà÷åñòâå URL ìîæíî çàäàâàòü html-ñòðàíè÷êó ñ êíèãîé, ëèáî ïðÿìóþ ññûëêó íà ôàéë èç îíëàéí-áèáëèîòåêè (íàïðèìåð, ñêîïèðîâàâ àäðåñ ññûëêè èëè êíîïêè "ñêà÷àòü fb2").
Ïîääåðæèâàåìûå ôîðìàòû: <strong>html, txt, fb2, fb2.zip</strong>
<br><br>Âû ìîæåòå äîáàâèòü â ñâîé áðàóçåð çàêëàäêó, óêàçàâ â åå ñâîéñòâàõ âìåñòî àäðåñà ñëåäóþùèé êîä:
<br><p><strong>javascript:location.href='http://old.omnireader.ru/?url='+location.href;</strong>
<br>Òîãäà, íàæàâ íà ïîëó÷èâøóþñÿ êíîïêó íà ëþáîé ñòðàíèöå èíòåðíåòà, âû àâòîìàòè÷åñêè îòêðîåòå åå â Omni Reader.
<br><br>Äëÿ Chrome íà Android ìîæíî âûçûâàòü çàêëàäêó ïî åå èìåíè (èìÿ ñòîèò ñäåëàòü ïîïðîùå) â àäðåñíîé ñòðîêå áðàóçåðà, ïîñêîëüêó ñòàíäàðòíûé âûçîâ òàêîé çàêëàäêè íå ðàáîòàåò.
<br><br>Êîëè÷åñòâî è íóìåðàöèÿ ñòðàíèö â ÷èòàëêå çàâèñèò îò ðàçìåðà îêíà áðàóçåðà.
<br><br><strong>Ïîïóëÿðíûå ðåñóðñû ñ êíèãàìè è ýëåêòðîííûå áèáëèîòåêè:</strong>
<br><p><a href="http://samlib.ru" target="_blank">samlib.ru</a>
<br><p><a href="http://flibusta.is" target="_blank">flibusta.is</a>
<br><p><a href="http://fantasy-worlds.org" target="_blank">fantasy-worlds.org</a>
<br><p><a href="http://www.litmir.co" target="_blank">www.litmir.co</a>
<br><p><a href="http://lib.ru" target="_blank">lib.ru</a>
<br><br><strong>Ïðè íåîáõîäèìîñòè çàãðóçèòü ôàéë ñ ëîêàëüíîãî äèñêà â ÷èòàëêó</strong>, ìîæíî âîñïîëüçîâàòüñÿ îäíèì èç ôàéëîîáìåííûõ
ñåðâèñîâ. Ðåêîìåíäóåìûå ôàéëîîáìåííèêè:
<br><p><a href="http://fayloobmennik.cloud" target="_blank">fayloobmennik.cloud</a>
<br><p><a href="http://zaix.ru" target="_blank">zaix.ru</a>
<br><br>Ïîñëå çàãðóçêè ôàéëà íà îáìåííèê, ïðÿìóþ ññûëêó íà ôàéë íåîáõîäèìî ñêîïèðîâàòü â ïîëå "Àäðåñ êíèãè" (ñì. âûøå).
<br><br><strong>Èçâåñòíûå ïðîáëåìû:</strong>
<p>- ïðè èçìåíåíèè/ïðèìåíåíèè íàñòðîåê áûâàþò ñáîè èç-çà ìàñøòàáèðîâàíèÿ ñòðàíèöû. Ïîìîãàåò îáíîâëåíèå ñòðàíèöû â áðàóçåðå.
</div>
<div id="leg" onclick="legend();">
<br>
<b>Óïðàâëåíèå:</b>
<p>M - ÌÅÍÞ
<p>PgUp, Left, Shift+Space, Backspace - ñòðàíèöó íàçàä
<p>PgDown, Right, Space, Enter - ñòðàíèöó âïåð¸ä
<p>Home - â íà÷àëî êíèãè
<p>End - â êîíåö êíèãè
<p>Up - ñòðî÷êó íàçàä
<p>Down - ñòðî÷êó âïåð¸ä
<p>A, Shift+A - èçìåíèòü ðàçìåð øðèôòà
<p>S - ïîêàçàòü/ñêðûòü ïîëîñó ïðîêðóòêè
<p>T - âêëþ÷èòü/îòêëþ÷èòü óïðàâëåíèå êëèêîì
<p>F, F11, ` (àïîñòðîô) - âêë./âûêë. ïîëíûé ýêðàí
<p>Z, Shift+Down - ïëàâíûé ñêðîëëèíã òåêñòà
<p>Shift+Left/Shift+Right - óâåëè÷èòü/óìåíüøèòü èíòåðâàë ñêðîëëèíãà
<p>R - ïðèíóäèòåëüíî îáíîâèòü êíèãó â îáõîä êýøà
<br><br><strong>Óïðàâëåíèå íà ñåíñîðíûõ ýêðàíàõ (êëèêîì):</strong>
<p>PgUp-------Up------PgUp
<p>PgDown---Menu---PgDown
<p>PgDown---Down---PgDown
<div id="top100">
<div id="id_add2"></div>
<br>
<!-- Yandex.Metrika informer --><a href="https://metrika.yandex.ru/stat/?id=31398038&amp;from=informer"target="_blank" rel="nofollow"><img src="https://mc.yandex.ru/informer/31398038/3_1_FFFFFFFF_EFEFEFFF_0_pageviews"style="width:88px; height:31px; border:0;" alt="ßíäåêñ.Ìåòðèêà" title="ßíäåêñ.Ìåòðèêà: äàííûå çà ñåãîäíÿ (ïðîñìîòðû, âèçèòû è óíèêàëüíûå ïîñåòèòåëè)" onclick="try{Ya.Metrika.informer({i:this,id:31398038,lang:'ru'});return false}catch(e){}" /></a><!-- /Yandex.Metrika informer --> <!-- Yandex.Metrika counter --><script type="text/javascript"> (function (d, w, c) { (w[c] = w[c] || []).push(function() { try { w.yaCounter31398038 = new Ya.Metrika({ id:31398038, clickmap:true, trackLinks:true, accurateTrackBounce:true }); } catch(e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks");</script><noscript><div><img src="https://mc.yandex.ru/watch/31398038" style="position:absolute; left:-9999px;" alt="" /></div></noscript><!-- /Yandex.Metrika counter -->
</div>
</div>
<div id="footer" class="footer">
<div id="id_lp" style="position: absolute; left: 0">
<div id="bmain" class="page">
<a href="/"><span title="Âåðíóòüñÿ íà ãëàâíóþ">&nbsp;&nbsp;&nbsp;&lt;&lt;&lt;&nbsp;&nbsp;</span></a>
</div>
<div class="page">&nbsp;&nbsp;&nbsp;
</div>
<div class="cpage" onclick="menu();return false;">
<span title="M - ïîêàçàòü/ñêðûòü ìåíþ">ÌÅÍÞ</span>
</div>
<div class="page">&nbsp;&nbsp;&nbsp;
</div>
<div class="cpage" onclick="toggleFullScreen();return false;">
<span title="F, F11, ` (àïîñòðîô) - âêë./âûêë. ïîëíûé ýêðàí">&lt;---&gt;</span>
</div>
<div class="page">&nbsp;&nbsp;&nbsp;
</div>
<div class="cpage" onclick="incFont(1);return false;">
<span title="A - óâåëè÷èòü ðàçìåð øðèôòà">A+</span>
</div>
<div class="page">&nbsp;
</div>
<div class="cpage" onclick="incFont(-1);return false;">
<span title="Shift+A - óìåíüøèòü ðàçìåð øðèôòà">A-</span>
</div>
<div class="page">&nbsp;&nbsp;
</div>
<div class="cpage" onclick="return toggleScroll();">
<span title="Z, Shift+Down - âêë./âûêë. ïëàâíûé ñêðîëëèíã òåêñòà">$</span>
</div>
<div class="page">&nbsp;&nbsp;
</div>
<div id="reload" class="cpage" onclick="return reloadBook();">
<span title="R - ïðèíóäèòåëüíî îáíîâèòü êíèãó â îáõîä êýøà">@</span>
</div>
<div class="page" style="width: 20px">&nbsp;
</div>
</div>
<div id="orig" class="page">
<a id="orig_href" href="" target="_blank">&nbsp;</a>
</div>
<div id="id_rp" style="position: absolute; right: 0">
<div id="pageCount" class="page">
<div onmousedown="pageMove(1);" onmouseout="pageMoveClear();" onmouseup="pageMoveClear();">&lt;&nbsp;
</div>
<div onclick="pageMoveShow();" style="visibility:visible;">1/1
</div>
<div onmousedown="pageMove(0); " onmouseout="pageMoveClear();" onmouseup="pageMoveClear();">&nbsp;&gt;
</div>
</div>
<div id="pagePercent" class="page">
</div>
<div id="tm" class="page">
</div>
<div class="page">&nbsp;&nbsp;&nbsp;
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
omnireader.ru

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,438 @@
*
{
margin:0px;
padding:0px;
}
body
{
font: 21px/26px 'Comic Sans Ms', Fantasy;
color: #FFFFFF;
padding: 0;
margin: 0;
background-color: 0;
}
hr {
position:absolute;
left:1%;
width: 98%;
height: 1px;
bottom: 22px;
color:#000000;
border: none;
z-index:6;
}
#win {
display:none;
width: 100%;
}
#main {
width: 94.9%;
padding-left: 2.5%;
padding-right: 2.5%;
font: 21px/26px 'Trebuchet Ms', Sans-Serif;
color: #220;
float:left;
position:relative;
top:0px;
text-align: justify;
overflow:hidden;
overflow-y:auto;
/*border:1px solid #000;*/
}
#main span
{
display:block;
text-indent:7%;
clear:both;
padding:0px;
}
#main p
{
display:block;
text-indent:7%;
clear:both;
padding:0px;
}
#main dd
{
display:block;
text-indent:7%;
clear:both;
padding:0px;
}
#main div
{
text-indent:7%;
clear:both;
padding:0px;
}
#main span p
{
text-indent:0;
}
#main span div
{
text-indent:0;
}
div#main img
{
border:0px;
vertical-align:top;float:left;
padding:0px;
line-height:0px;
font-size:0px;
}
div.podk
{
position:absolute;
border:3px solid #220;
display:none;
z-index:9;
height:280px;
width:264px;
font: bold 16px 'Trebuchet Ms', Sans-Serif;
display:none;
}
div.podk span
{
text-align:center;
font: 21px/21px 'Trebuchet Ms', Sans-Serif;
}
div.melem {
padding-left: 20px;
line-height: 25px;
}
div.melem label {
cursor: pointer;
}
div.melem label input {
cursor: pointer;
position: relative;
border:0;
left: 10px;
top: 5px;
}
div.melem input
{
background-color:transparent;
border:0px solid #fff;
font: bold 14px 'Trebuchet Ms', Sans-Serif;
text-align:center;
height: 18px;
border:1px solid #000;
}
div.melem b
{
cursor:pointer;
}
.over
{
display:block;
color:#500;
background-color:black;
text-align:center;
vertical-align:middle;
width:100%;
height:10050px;
position:absolute;
right:0px;
bottom:0px;
z-index:15;
}
div#loading
{
background-color:black;
font: 40px 'Trebuchet Ms', Sans-Serif;
align:center;
color:#FFFFFF;
position:absolute;
left:48%;
bottom:50%;
z-index:16;
}
a
{
text-decoration:none;
}
a:hover
{
text-decoration:underline;
}
div#footer div a:hover, div#comm div a:hover
{
text-decoration:none;
}
#tl
{
width:14px;
float:left;
}
#tr
{
width:14px;
float:right;
}
.header
{
height: 0px;
}
.footer {
display: none;
font: 18px/21px 'Trebuchet Ms', Sans-Serif;
width:100%;
height:22px;
position:absolute;
bottom:0px;
overflow: hidden;
white-space: nowrap;
}
.page, .cpage
{
position:relative;
float: left;
vertical-align: bottom;
border: 0px solid yellow;
overflow: hidden;
white-space: nowrap;
}
.cpage
{
cursor:pointer;
}
#tm
{
}
#pageCount
{
}
#pagePercent
{
}
#bmain
{
}
div#pageCount div
{
overflow:hidden;
float:left;
cursor:pointer;
text-align:center;
visibility:hidden;
}
.l
{
left:17px;
float:left;
margin-left:0;
}
.t
{
top:4px;
}
.b
{
bottom:4px;
}
.r
{
right:17px;
float:right;
}
.a
{
position:absolute;
line-height:0px;
}
.addr {
width:80%;
margin-left: 21%;
}
#book
{
border-right:0px solid #fff !important;
}
.book
{
background-color:transparent;
border:2px groove #000;
font: bold 16px 'Trebuchet Ms', Sans-Serif;
height: 26px;
padding:0;
margin:0;
display:block;
float:left;
}
#btnOk
{
height: 30px;
}
#book:hover
{
border-left:2px ridge #000;
}
.book:hover
{
border:2px ridge #000;
}
#leg
{
font: bold 12px Verdana, Sans-Serif;
line-height:12px;
width:60%;
height:60%;
background-color:#fff;
color:#000;
border:4px double black;
padding:20px;
text-align: justify;
z-index:16;
position:absolute;
overflow:scroll;
top:0;
left:0;
display:none;
}
#leg b
{
color: #AA2222;
}
/*êåëâ àüþìî÷ ôàåï÷ START*/
div#colorSelect
{
width:758px;
height:600px;
z-index:20;
display:none;
position:absolute;
color:#220;
}
div.colorImage
{
width:600px;
height:600px;
background:url(/js/colo58.png);
float:right;
cursor:crosshair;
}
div.colorMenu
{
overflow:hidden;
width:150px;
height:330px;
border: 3px solid #000;
position:relative;
background-color:white;
font: 20px/22px 'Trebuchet Ms', Sans-Serif;
top:150px;
}
div.colorMenu div div
{
cursor:pointer;
float:left;
width:76px;
}
div.colorMenu div input
{
float:right;
height:18px;
width:70px;
border: 1px solid #000;
margin:1px 0px;
display:block;
}
div.colorMenu div select
{
width: 70px;
}
div.colorExample
{
position:absolute;
width:150px;
height:78px;
font: 21px/78px 'Trebuchet Ms', Sans-Serif;
text-align:center;
}
div.colorCross
{
position:absolute;
top:143px;
left:147px;
width:14px;
height:14px;
z-index:21;
cursor:pointer;
line-height:7px;
}
div.menuCross
{
font: 21px/26px 'Comic Sans Ms', Fantasy;
position:absolute;
top:-8px;
left: 258px;
width:14px;
height:14px;
z-index:21;
cursor:pointer;
line-height:7px;
}
/*êåëâ àüþìî÷ ôàåï÷ END*/
.test {
border: 1px solid #FFFFFF;
}
#cbscroll {
padding-top: 3px;
padding-left: 30px;
}

View File

@@ -0,0 +1,96 @@
<?php
function parseHtml($data, $remove_tags = FALSE) {
$substs = array(
//html
'TD' => chr(9),
'TH' => chr(9),
'TR' => chr(13) . chr(10) . '<P>',
'BR' => chr(13) . chr(10) . '<P>',
'BR/' => chr(13) . chr(10) . '<P>',
'DD' => chr(13) . chr(10) . '<P>',
'P' => chr(13) . chr(10) . '<P>',
'HR' => chr(13) . chr(10),
'LI' => chr(13) . chr(10),
'OL' => chr(13) . chr(10),
'/OL' => chr(13) . chr(10),
'TABLE' => chr(13) . chr(10),
'/TABLE' => chr(13) . chr(10),
'TITLE' => '<br>&nbsp;',
'/TITLE' => '<br>&nbsp;',
'UL' => chr(13) . chr(10) . ' ',
'/UL' => chr(13) . chr(10),
// fb2
'EMPTY-LINE/' => '<P>&nbsp;',
'STANZA' => '<P>&nbsp;',
'V' => '<P>',
'/POEM' => '<P>&nbsp;',
'SUBTITLE' => '<br>&nbsp;<P>',
'/SUBTITLE' => '<br>&nbsp;',
);
$inner_cut = array(
'HEAD' => 1,
'SCRIPT' => 1,
'STYLE' => 1,
//fb2
'BINARY' => 1,
'DESCRIPTION' => 1,
);
if ($remove_tags)
$substs = $inner_cut = array();
$data = str_replace('&nbsp;', ' ', $data);
$i = 0;
$len = strlen($data);
$out = '';
$cut_counter = 0;
$cut_tag = '';
while ($i < $len) {
$left = strpos($data, '<', $i);
if ($left !== FALSE) {
$right = strpos($data, '>', $left + 1);
if ($right !== FALSE) {
$tag = trim(substr($data, $left + 1, $right - $left - 1));
$first_space = strpos($tag, ' ');
if ($first_space !== FALSE)
$tag = substr($tag, 0, $first_space);
$tag = strtoupper($tag);
if (!$cut_counter) {
$out .= substr($data, $i, $left - $i);
if (isset($substs[$tag]))
$out .= $substs[$tag];
}
if (isset($inner_cut[$tag]) && (!$cut_counter || $cut_tag == $tag))
{
if (!$cut_counter)
$cut_tag = $tag;
$cut_counter++;
}
if ($tag != '' && $tag[0] == '/' && $cut_tag == substr($tag, 1)) {
$cut_counter = ($cut_counter > 0) ? $cut_counter - 1 : 0;
if (!$cut_counter)
$cut_tag = '';
}
//$close_tag = substr($tag, 1);
//$out .= "<br>$cut_counter, $cut_tag == $close_tag";
$i = $right + 1;
} else
break;
}
else
break;
}
if ($i < $len && !$cut_counter)
$out .= substr($data, $i, $len - $i);
return $out;
}
?>

View File

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

View File

@@ -0,0 +1,4 @@
<?php
header('Content-Type: text/html; charset=windows-1251');
echo getcwd();
?>

View File

@@ -0,0 +1,2 @@
AddType 'text/plain; charset=windows-1251' .txz .txt
AddEncoding gzip .txz

View File

@@ -1,3 +1,34 @@
server {
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/omnireader.ru/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/omnireader.ru/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name omnireader.ru;
client_max_body_size 50m;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location /api {
proxy_pass http://localhost:44081;
}
location /tmp {
root /home/liberama/public;
add_header Content-Type text/xml;
add_header Content-Encoding gzip;
}
location / {
root /home/liberama/public;
}
}
server {
listen 80;
server_name omnireader.ru;
@@ -23,3 +54,26 @@ server {
root /home/liberama/public;
}
}
server {
listen 80;
server_name old.omnireader.ru;
client_max_body_size 50m;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
root /home/oldreader;
index index.html;
# Обработка php файлов с помощью fpm
location ~ \.php$ {
try_files $uri =404;
include /etc/nginx/fastcgi.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
}

View File

@@ -1,8 +1,30 @@
sudo bash
mkdir /home/liberama
chown www-data /home/liberama
chgrp www-data /home/liberama
chown www-data.www-data /home/liberama
### oldreader
# ubuntu 18
apt install php7.2 php7.2-curl php7.2-mbstring php7.2-fpm
service php7.2-fpm restart
mkdir /home/oldreader
chown www-data /home/oldreader
chgrp www-data /home/oldreader
sudo -u www-data cp -r ./old/* /home/oldreader
###
### external converter
# calibre releases https://download.calibre-ebook.com/
# download, unpack to data/calibre
# 3.39.1
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
apt install libreoffice
apt install poppler-utils
###
apt install nginx
@@ -11,5 +33,7 @@ ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
rm /etc/nginx/sites-enabled/default
service nginx reload
chown -R www-data.www-data /var/www
exit

View File

@@ -1 +1 @@
sudo -u www-data /home/liberama/liberama
sudo -H -u www-data sh -c "cd /var/www; /home/liberama/liberama"

5231
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.2.2",
"version": "0.7.1",
"engines": {
"node": ">=10.0.0"
},
@@ -19,65 +19,70 @@
},
"devDependencies": {
"babel-core": "^6.22.1",
"babel-eslint": "^10.0.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-component": "^1.1.1",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-preset-env": "^1.3.2",
"clean-webpack-plugin": "^1.0.0",
"clean-webpack-plugin": "^1.0.1",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^1.0.0",
"decompress-targz": "^4.1.1",
"disable-output-webpack-plugin": "^1.0.1",
"element-theme-chalk": "^2.4.11",
"eslint": "^5.11.1",
"eslint-plugin-html": "^5.0.0",
"element-theme-chalk": "^2.12.0",
"eslint": "^5.16.0",
"eslint-plugin-html": "^5.0.5",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-vue": "^5.0.0",
"event-hooks-webpack-plugin": "^2.1.1",
"eslint-plugin-vue": "^5.2.3",
"event-hooks-webpack-plugin": "^2.1.4",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.5.0",
"null-loader": "^0.1.1",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"pkg": "^4.3.7",
"terser-webpack-plugin": "^1.2.1",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"pkg": "4.3.7",
"terser-webpack-plugin": "^1.4.1",
"url-loader": "^1.1.2",
"vue-class-component": "^6.3.2",
"vue-loader": "^15.4.2",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.21",
"webpack": "^4.28.2",
"webpack-cli": "^3.1.2",
"webpack-dev-middleware": "^3.4.0",
"webpack-hot-middleware": "^2.24.3",
"webpack-merge": "^4.1.5"
"vue-template-compiler": "^2.6.10",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"webpack-dev-middleware": "^3.7.1",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^4.2.2"
},
"dependencies": {
"axios": "^0.18.0",
"appcache-webpack-plugin": "^1.4.0",
"axios": "^0.18.1",
"base-x": "^3.0.6",
"chardet": "^0.7.0",
"compression": "^1.7.3",
"decompress": "^4.2.0",
"detect-file-type": "^0.2.0",
"element-ui": "^2.4.11",
"express": "^4.16.4",
"compression": "^1.7.4",
"decompress-zip": "^0.2.2",
"element-ui": "^2.12.0",
"express": "^4.17.1",
"fg-loadcss": "^2.1.0",
"fs-extra": "^7.0.1",
"got": "^9.5.1",
"got": "^9.6.0",
"he": "^1.2.0",
"iconv-lite": "^0.4.24",
"localforage": "^1.7.3",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"minimist": "^1.2.0",
"multer": "^1.4.1",
"multer": "^1.4.2",
"pako": "^1.0.10",
"path-browserify": "^1.0.0",
"safe-buffer": "^5.2.0",
"sjcl": "^1.0.8",
"sql-template-strings": "^2.2.2",
"sqlite": "^3.0.0",
"vue": "^2.5.21",
"vue-router": "^3.0.2",
"vuex": "^3.0.1",
"sqlite": "3.0.0",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.1",
"vuex-persistedstate": "^2.5.4"
}
}

View File

@@ -14,13 +14,27 @@ module.exports = {
logDir: `${dataDir}/log`,
publicDir: `${execDir}/public`,
uploadDir: `${execDir}/public/upload`,
dbFileName: 'db.sqlite',
loggingEnabled: true,
maxUploadFileSize: 50*1024*1024,//50Мб
maxTempPublicDirSize: 512*1024*1024,//512Мб
maxUploadPublicDirSize: 200*1024*1024,//100Мб
useExternalBookConverter: false,
db: [
{
poolName: 'app',
connCount: 20,
fileName: 'app.sqlite',
},
{
poolName: 'readerStorage',
connCount: 20,
fileName: 'reader-storage.sqlite',
}
],
servers: [
{
serverName: '1',

View File

@@ -5,7 +5,8 @@ const propsToSave = [
'maxUploadFileSize',
'maxTempPublicDirSize',
'maxUploadPublicDirSize',
'useExternalBookConverter',
'servers',
];

View File

@@ -1,6 +1,5 @@
class BaseController {
constructor(connPool, config) {
this.connPool = connPool;
constructor(config) {
this.config = config;
}
}

View File

@@ -1,12 +1,11 @@
const BaseController = require('./BaseController');
const ReaderWorker = require('../core/ReaderWorker');
const workerState = require('../core/workerState');
//const log = require('../core/getLogger').getLog();
//const _ = require('lodash');
const ReaderWorker = require('../core/ReaderWorker');
const readerStorage = require('../core/readerStorage');
const workerState = require('../core/workerState');
class ReaderController extends BaseController {
constructor(connPool, config) {
super(connPool, config);
constructor(config) {
super(config);
this.readerWorker = new ReaderWorker(config);
}
@@ -27,6 +26,24 @@ class ReaderController extends BaseController {
return false;
}
async storage(req, res) {
const request = req.body;
let error = '';
try {
if (!request.action)
throw new Error(`key 'action' is empty`);
if (!request.items || Array.isArray(request.data))
throw new Error(`key 'items' is empty`);
return await readerStorage.doAction(request);
} catch (e) {
error = e.message;
}
//error
res.status(500).send({error});
return false;
}
async uploadFile(req, res) {
const file = req.file;
let error = '';

View File

@@ -0,0 +1,147 @@
const fs = require('fs-extra');
const iconv = require('iconv-lite');
const chardet = require('chardet');
const he = require('he');
const textUtils = require('./textUtils');
const utils = require('../utils');
let execConverterCounter = 0;
class ConvertBase {
constructor(config) {
this.config = config;
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
this.sofficePath = '/usr/bin/soffice';
this.pdfToHtmlPath = '/usr/bin/pdftohtml';
}
async run(data, opts) {// eslint-disable-line no-unused-vars
//override
}
async checkExternalConverterPresent() {
if (!await fs.pathExists(this.calibrePath))
throw new Error('Внешний конвертер calibre не найден');
if (!await fs.pathExists(this.sofficePath))
throw new Error('Внешний конвертер LibreOffice не найден');
if (!await fs.pathExists(this.pdfToHtmlPath))
throw new Error('Внешний конвертер pdftohtml не найден');
}
async execConverter(path, args, onData) {
execConverterCounter++;
try {
if (execConverterCounter > 10)
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
const result = await utils.spawnProcess(path, {args, onData});
if (result.code != 0) {
let error = result.code;
if (this.config.branch == 'development')
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
}
} catch(e) {
if (e.status == 'killed') {
throw new Error('Слишком долгое ожидание конвертера');
} else if (e.status == 'error') {
throw new Error(e.error);
} else {
throw new Error(e);
}
} finally {
execConverterCounter--;
}
}
decode(data) {
let selected = textUtils.getEncoding(data);
if (selected == 'ISO-8859-5') {
const charsetAll = chardet.detectAll(data.slice(0, 20000));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {
selected = charset.name;
break;
}
}
}
if (selected.toLowerCase() != 'utf-8')
return iconv.decode(data, selected);
else
return data;
}
repSpaces(text) {
return text.replace(/&nbsp;|[\t\n\r]/g, ' ');
}
escapeEntities(text) {
return he.escape(he.decode(text));
}
formatFb2(fb2) {
let out = '<?xml version="1.0" encoding="utf-8"?>';
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">';
out += this.formatFb2Node(fb2);
out += '</FictionBook>';
return out;
}
formatFb2Node(node, name) {
let out = '';
if (Array.isArray(node)) {
for (const n of node) {
out += this.formatFb2Node(n);
}
} else if (typeof node == 'string') {
if (name)
out += `<${name}>${this.repSpaces(node)}</${name}>`;
else
out += this.repSpaces(node);
} else {
if (node._n)
name = node._n;
let attrs = '';
if (node._attrs) {
for (let attrName in node._attrs) {
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
}
}
let tOpen = '';
let tBody = '';
let tClose = '';
if (name)
tOpen += `<${name}${attrs}>`;
if (node.hasOwnProperty('_t'))
tBody += this.repSpaces(node._t);
for (let nodeName in node) {
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
continue;
const n = node[nodeName];
tBody += this.formatFb2Node(n, nodeName);
}
if (name)
tClose += `</${name}>`;
if (attrs == '' && name == 'p' && tBody.trim() == '')
out += '<empty-line/>'
else
out += `${tOpen}${tBody}${tClose}`;
}
return out;
}
}
module.exports = ConvertBase;

View File

@@ -0,0 +1,33 @@
const fs = require('fs-extra');
const path = require('path');
const ConvertDocX = require('./ConvertDocX');
class ConvertDoc extends ConvertDocX {
check(data, opts) {
const {inputFiles} = opts;
return this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'msi';
}
async run(data, opts) {
if (!this.check(data, opts))
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const docFile = `${outFile}.doc`;
const docxFile = `${outFile}.docx`;
const fb2File = `${outFile}.fb2`;
await fs.copy(inputFiles.sourceFile, docFile);
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, docFile]);
return await super.convert(docxFile, fb2File, callback);
}
}
module.exports = ConvertDoc;

View File

@@ -0,0 +1,49 @@
const fs = require('fs-extra');
const path = require('path');
const ConvertBase = require('./ConvertBase');
class ConvertDocX extends ConvertBase {
check(data, opts) {
const {inputFiles} = opts;
if (this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') {
//ищем файл '[Content_Types].xml'
for (const file of inputFiles.files) {
if (file.path == '[Content_Types].xml') {
return true;
}
}
}
return false;
}
async convert(docxFile, fb2File, callback) {
let perc = 0;
await this.execConverter(this.calibrePath, [docxFile, fb2File], () => {
perc = (perc < 100 ? perc + 5 : 50);
callback(perc);
});
return await fs.readFile(fb2File);
}
async run(data, opts) {
if (!this.check(data, opts))
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const docxFile = `${outFile}.docx`;
const fb2File = `${outFile}.fb2`;
await fs.copy(inputFiles.sourceFile, docxFile);
return await this.convert(docxFile, fb2File, callback);
}
}
module.exports = ConvertDocX;

View File

@@ -0,0 +1,49 @@
const fs = require('fs-extra');
const path = require('path');
const ConvertBase = require('./ConvertBase');
class ConvertEpub extends ConvertBase {
async check(data, opts) {
const {inputFiles} = opts;
if (this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'zip') {
//ищем файл 'mimetype'
for (const file of inputFiles.files) {
if (file.path == 'mimetype') {
const mt = await fs.readFile(`${inputFiles.filesDir}/${file.path}`);
if (mt.toString().trim() == 'application/epub+zip')
return true;
break;
}
}
}
return false;
}
async run(data, opts) {
if (!await this.check(data, opts))
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const epubFile = `${outFile}.epub`;
const fb2File = `${outFile}.fb2`;
await fs.copy(inputFiles.sourceFile, epubFile);
let perc = 0;
await this.execConverter(this.calibrePath, [epubFile, fb2File], () => {
perc = (perc < 100 ? perc + 5 : 50);
callback(perc);
});
return await fs.readFile(fb2File);
}
}
module.exports = ConvertEpub;

View File

@@ -0,0 +1,41 @@
const ConvertBase = require('./ConvertBase');
const iconv = require('iconv-lite');
class ConvertFb2 extends ConvertBase {
check(data, opts) {
const {dataType} = opts;
return (dataType && dataType.ext == 'xml' && data.toString().indexOf('<FictionBook') >= 0);
}
async run(data, opts) {
if (!this.check(data, opts))
return false;
return this.checkEncoding(data);
}
checkEncoding(data) {
let result = data;
const left = data.indexOf('<?xml version="1.0"');
if (left >= 0) {
const right = data.indexOf('?>', left);
if (right >= 0) {
const head = data.slice(left, right + 2).toString();
const m = head.match(/encoding="(.*)"/);
if (m) {
let encoding = m[1].toLowerCase();
if (encoding != 'utf-8') {
result = iconv.decode(data, encoding);
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
}
}
}
}
return result;
}
}
module.exports = ConvertFb2;

View File

@@ -0,0 +1,299 @@
const ConvertBase = require('./ConvertBase');
const sax = require('./sax');
const textUtils = require('./textUtils');
class ConvertHtml extends ConvertBase {
check(data, opts) {
const {dataType} = opts;
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
return {isText: false};
//может это чистый текст?
if (textUtils.checkIfText(data)) {
return {isText: true};
}
return false;
}
async run(data, opts) {
let isText = false;
if (!opts.skipCheck) {
const checkResult = this.check(data, opts);
if (!checkResult)
return false;
isText = checkResult.isText;
} else {
isText = opts.isText;
}
let {cutTitle} = opts;
let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo};
let pars = [];
let body = {_n: 'body', section: {_a: []}};
let binary = [];
let fb2 = [desc, body, binary];
let title = '';
let inTitle = false;
let inImage = false;
let image = {};
let bold = false;
let italic = false;
let spaceCounter = [];
const repCrLfTab = (text) => text.replace(/[\n\r]/g, '').replace(/\t/g, ' ');
const newParagraph = () => {
pars.push({_n: 'p', _t: ''});
};
const growParagraph = (text) => {
if (!pars.length)
newParagraph();
const l = pars.length;
pars[l - 1]._t += text;
//посчитаем отступы у текста, чтобы выделить потом параграфы
const lines = text.split('\n');
for (let line of lines) {
if (line.trim() == '')
continue;
line = repCrLfTab(line);
let l = 0;
while (l < line.length && line[l] == ' ') {
l++;
}
if (!spaceCounter[l])
spaceCounter[l] = 0;
spaceCounter[l]++;
}
};
const newPara = new Set(['tr', '/table', 'hr', 'br', 'br/', 'li', 'dt', 'dd', 'p', 'title', '/title', 'h1', 'h2', 'h3', '/h1', '/h2', '/h3']);
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
text = this.escapeEntities(text);
if (!cutCounter && !(cutTitle && inTitle)) {
let tOpen = (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
let tClose = (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
growParagraph(`${tOpen}${text}${tClose}`);
}
if (inTitle && !title)
title = text;
if (inImage) {
image._t = text;
binary.push(image);
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
newParagraph();
}
};
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter) {
if (newPara.has(tag))
newParagraph();
switch (tag) {
case 'i':
case 'em':
italic = true;
break;
case 'b':
case 'strong':
case 'h1':
case 'h2':
case 'h3':
bold = true;
break;
}
}
if (tag == 'title' || tag == 'cut-title') {
inTitle = true;
if (tag == 'cut-title')
cutTitle = true;
}
if (tag == 'fb2-image') {
inImage = true;
const attrs = sax.getAttrsSync(tail);
image = {_n: 'binary', _attrs: {id: attrs.name.value, 'content-type': attrs.type.value}, _t: ''};
}
};
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter) {
if (newPara.has('/' + tag))
newParagraph();
switch (tag) {
case 'i':
case 'em':
italic = false;
break;
case 'b':
case 'strong':
case 'h1':
case 'h2':
case 'h3':
bold = false;
break;
}
}
if (tag == 'title' || tag == 'cut-title')
inTitle = false;
if (tag == 'fb2-image')
inImage = false;
};
let buf = this.decode(data).toString();
sax.parseSync(buf, {
onStartNode, onEndNode, onTextNode,
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image'])
});
titleInfo['book-title'] = title;
//подозрение на чистый текст, надо разбить на параграфы
if (isText || pars.length < buf.length/2000) {
let total = 0;
let count = 1;
for (let i = 0; i < spaceCounter.length; i++) {
const sc = (spaceCounter[i] ? spaceCounter[i] : 0);
if (sc) count++;
total += sc;
}
let d = 0;
const mid = total/count;
for (let i = 0; i < spaceCounter.length; i++) {
const sc = (spaceCounter[i] ? spaceCounter[i] : 0);
if (sc > mid) d++;
}
let i = 0;
//если разброс не слишком большой, выделяем параграфы
if (d < 10 && spaceCounter.length) {
total /= 20;
i = spaceCounter.length - 1;
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
}
const parIndent = (i > 0 ? i : 0);
let newPars = [];
const newPar = () => {
newPars.push({_n: 'p', _t: ''});
};
const growPar = (text) => {
if (!newPars.length)
newPar();
const l = newPars.length;
newPars[l - 1]._t += text;
}
i = 0;
for (const par of pars) {
if (par._n != 'p') {
newPars.push(par);
continue;
}
if (i > 0)
newPar();
i++;
let j = 0;
const lines = par._t.split('\n');
for (let line of lines) {
line = repCrLfTab(line);
let l = 0;
while (l < line.length && line[l] == ' ') {
l++;
}
if (l >= parIndent) {
if (j > 0)
newPar();
j++;
}
growPar(line.trim() + ' ');
}
}
body.section._a[0] = newPars;
} else {
body.section._a[0] = pars;
}
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
bold = false;
italic = false;
pars = body.section._a[0];
for (let i = 0; i < pars.length; i++) {
if (pars[i]._n != 'p')
continue;
pars[i]._t = this.repSpaces(pars[i]._t).trim();
if (pars[i]._t.indexOf('<') >= 0) {
const t = pars[i]._t;
let a = [];
const onTextNode = (text) => {
let tOpen = (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
let tClose = (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
a.push(`${tOpen}${text}${tClose}`);
}
const onStartNode = (tag) => {
if (tag == 'strong')
bold = true;
if (tag == 'emphasis')
italic = true;
}
const onEndNode = (tag) => {
if (tag == 'strong')
bold = false;
if (tag == 'emphasis')
italic = false;
}
sax.parseSync(t, { onStartNode, onEndNode, onTextNode });
pars[i]._t = '';
pars[i]._a = a;
}
}
return this.formatFb2(fb2);
}
}
module.exports = ConvertHtml;

View File

@@ -0,0 +1,37 @@
const fs = require('fs-extra');
const path = require('path');
const ConvertBase = require('./ConvertBase');
class ConvertMobi extends ConvertBase {
async check(data, opts) {
const {inputFiles} = opts;
return (this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'mobi');
}
async run(data, opts) {
if (!await this.check(data, opts))
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const mobiFile = `${outFile}.mobi`;
const fb2File = `${outFile}.fb2`;
await fs.copy(inputFiles.sourceFile, mobiFile);
let perc = 0;
await this.execConverter(this.calibrePath, [mobiFile, fb2File], () => {
perc = (perc < 100 ? perc + 5 : 50);
callback(perc);
});
return await fs.readFile(fb2File);
}
}
module.exports = ConvertMobi;

View File

@@ -0,0 +1,219 @@
const fs = require('fs-extra');
const path = require('path');
const sax = require('./sax');
const utils = require('../utils');
const ConvertHtml = require('./ConvertHtml');
class ConvertPdf extends ConvertHtml {
check(data, opts) {
const {inputFiles} = opts;
return this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'pdf';
}
async run(notUsed, opts) {
if (!this.check(notUsed, opts))
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
//конвертируем в xml
let perc = 0;
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
perc = (perc < 80 ? perc + 10 : 40);
callback(perc);
});
callback(80);
const data = await fs.readFile(outFile);
callback(90);
//парсим xml
let lines = [];
let images = [];
let loading = [];
let inText = false;
let bold = false;
let italic = false;
let title = '';
let prevTop = 0;
let i = -1;
let titleCount = 0;
const loadImage = async(image) => {
const src = path.parse(image.src);
let type = 'unknown';
switch (src.ext) {
case '.jpg': type = 'image/jpeg'; break;
case '.png': type = 'image/png'; break;
}
if (type != 'unknown') {
image.data = (await fs.readFile(image.src)).toString('base64');
image.type = type;
image.name = src.base;
}
}
const putImage = (curTop) => {
if (!isNaN(curTop) && images.length) {
while (images.length && images[0].top < curTop) {
i++;
lines[i] = images[0];
images.shift();
}
}
}
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter && inText) {
let tOpen = (bold ? '<b>' : '');
tOpen += (italic ? '<i>' : '');
let tClose = (italic ? '</i>' : '');
tClose += (bold ? '</b>' : '');
lines[i].text += `${tOpen}${text}${tClose} `;
if (titleCount < 2 && text.trim() != '') {
title += text + (titleCount ? '' : ' - ');
titleCount++;
}
}
};
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter) {
if (inText) {
switch (tag) {
case 'i':
italic = true;
break;
case 'b':
bold = true;
break;
}
}
if (tag == 'text' && !inText) {
let attrs = sax.getAttrsSync(tail);
const line = {
text: '',
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10),
left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10),
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
};
if (line.width != 0 || line.height != 0) {
inText = true;
if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) {
putImage(line.top);
i++;
lines[i] = line;
}
prevTop = line.top;
}
}
if (tag == 'image') {
const attrs = sax.getAttrsSync(tail);
const src = (attrs.src && attrs.src.value ? attrs.src.value : '');
if (src) {
const image = {
isImage: true,
src,
data: '',
type: '',
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
};
loading.push(loadImage(image));
images.push(image);
images.sort((a, b) => a.top - b.top)
}
}
if (tag == 'page') {
putImage(100000);
}
}
};
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (inText) {
switch (tag) {
case 'i':
italic = false;
break;
case 'b':
bold = false;
break;
}
}
if (tag == 'text')
inText = false;
};
let buf = this.decode(data).toString();
sax.parseSync(buf, {
onStartNode, onEndNode, onTextNode
});
putImage(100000);
await Promise.all(loading);
//найдем параграфы и отступы
const indents = [];
for (const line of lines) {
if (line.isImage)
continue;
if (!isNaN(line.left)) {
indents[line.left] = 1;
}
}
let j = 0;
for (let i = 0; i < indents.length; i++) {
if (indents[i]) {
j++;
indents[i] = j;
}
}
indents[0] = 0;
//формируем текст
let text = `<title>${title}</title>`;
let concat = '';
let sp = '';
for (const line of lines) {
if (line.isImage) {
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
continue;
}
if (concat == '') {
const left = line.left || 0;
sp = ' '.repeat(indents[left]);
}
let t = line.text.trim();
if (t.substr(-1) == '-') {
t = t.substr(0, t.length - 1);
concat += t;
} else {
text += sp + concat + t + "\n";
concat = '';
}
}
if (concat)
text += sp + concat + "\n";
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
}
}
module.exports = ConvertPdf;

View File

@@ -0,0 +1,33 @@
const fs = require('fs-extra');
const path = require('path');
const ConvertDocX = require('./ConvertDocX');
class ConvertRtf extends ConvertDocX {
check(data, opts) {
const {inputFiles} = opts;
return this.config.useExternalBookConverter &&
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'rtf';
}
async run(data, opts) {
if (!this.check(data, opts))
return false;
await this.checkExternalConverterPresent();
const {inputFiles, callback} = opts;
const outFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}`;
const rtfFile = `${outFile}.rtf`;
const docxFile = `${outFile}.docx`;
const fb2File = `${outFile}.fb2`;
await fs.copy(inputFiles.sourceFile, rtfFile);
await this.execConverter(this.sofficePath, ['--headless', '--convert-to', 'docx', '--outdir', inputFiles.filesDir, rtfFile]);
return await super.convert(docxFile, fb2File, callback);
}
}
module.exports = ConvertRtf;

View File

@@ -0,0 +1,278 @@
const _ = require('lodash');
const URL = require('url').URL;
const sax = require('./sax');
const ConvertBase = require('./ConvertBase');
class ConvertSamlib extends ConvertBase {
check(data, opts) {
const {url, dataType} = opts;
const parsedUrl = new URL(url);
if (dataType && dataType.ext == 'html' &&
(parsedUrl.hostname == 'samlib.ru' ||
parsedUrl.hostname == 'budclub.ru' ||
parsedUrl.hostname == 'zhurnal.lib.ru')) {
return {hostname: parsedUrl.hostname};
}
return false;
}
async run(data, opts) {
const checkResult = this.check(data, opts);
if (!checkResult)
return false;
const {hostname} = checkResult;
let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo};
let pars = [];
let body = {_n: 'body', section: {_a: pars}};
let fb2 = [desc, body];
let inSubtitle = false;
let inJustify = true;
let inImage = false;
let isFirstPara = false;
let path = '';
let tag = '';// eslint-disable-line no-unused-vars
let inText = false;
let textFound = false;
let node = {_a: pars};
let inPara = false;
let italic = false;
let bold = false;
const openTag = (name, attrs) => {
if (name == 'p')
inPara = true;
let n = {_n: name, _attrs: attrs, _a: [], _p: node};
node._a.push(n);
node = n;
};
const closeTag = (name) => {
if (name == 'p')
inPara = false;
if (node._p) {
const exact = (node._n == name);
node = node._p;
if (!exact)
closeTag(name);
}
};
const growParagraph = (text) => {
if (!node._p) {
if (text.trim() != '')
openTag('p');
else
return;
}
if (node._n == 'p' && node._a.length == 0)
text = text.trimLeft();
node._a.push({_t: text});
};
const onStartNode = (elemName, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (elemName == '')
return;
if (!inText) {
path += '/' + elemName;
tag = elemName;
} else {
switch (elemName) {
case 'li':
case 'p':
case 'dd':
case 'br':
if (!(inSubtitle && isFirstPara)) {
if (inPara)
closeTag('p');
openTag('p');
}
isFirstPara = false;
break;
case 'h1':
case 'h2':
case 'h3':
if (inPara)
closeTag('p');
openTag('p');
bold = true;
break;
case 'i':
case 'em':
italic = true;
break;
case 'b':
case 'strong':
bold = true;
break;
case 'div':
if (inPara)
closeTag('p');
if (tail.indexOf('align="center"') >= 0) {
openTag('subtitle');
inSubtitle = true;
isFirstPara = true;
}
if (tail.indexOf('align="justify"') >= 0) {
openTag('p');
inJustify = true;
}
break;
case 'img': {
if (inPara)
closeTag('p');
const attrs = sax.getAttrsSync(tail);
if (attrs.src && attrs.src.value) {
let href = attrs.src.value;
if (href[0] == '/')
href = `http://${hostname}${href}`;
openTag('image', {'l:href': href});
inImage = true;
}
break;
}
}
}
};
const onEndNode = (elemName, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!inText) {
const oldPath = path;
let t = '';
do {
let i = path.lastIndexOf('/');
t = path.substr(i + 1);
path = path.substr(0, i);
} while (t != elemName && path);
if (t != elemName) {
path = oldPath;
}
let i = path.lastIndexOf('/');
tag = path.substr(i + 1);
} else {
switch (elemName) {
case 'li':
case 'p':
case 'dd':
closeTag('p');
break;
case 'h1':
case 'h2':
case 'h3':
closeTag('p');
bold = false;
break;
case 'i':
case 'em':
italic = false;
break;
case 'b':
case 'strong':
bold = false;
break;
case 'div':
if (inSubtitle) {
closeTag('subtitle');
inSubtitle = false;
isFirstPara = false;
}
if (inJustify) {
closeTag('p');
inJustify = false;
}
break;
case 'img':
if (inImage)
closeTag('image');
inImage = false;
break;
}
}
};
const onComment = (text) => {// eslint-disable-line no-unused-vars
if (text == '--------- Собственно произведение -------------') {
inText = true;
textFound = true;
}
if (text == '-----------------------------------------------')
inText = false;
};
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
if (text && text.trim() == '')
text = (text.indexOf(' ') >= 0 ? ' ' : '');
if (!text)
return;
text = this.escapeEntities(text);
switch (path) {
case '/html/body/center/h2':
titleInfo['book-title'] = text;
return;
case '/html/body/div/h3':
if (!titleInfo.author)
titleInfo.author = {};
text = text.replace(':', '').trim().split(' ');
if (text[0])
titleInfo.author['last-name'] = text[0];
if (text[1])
titleInfo.author['first-name'] = text[1];
if (text[2])
titleInfo.author['middle-name'] = text[2];
return;
}
let tOpen = (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
let tClose = (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
if (inText)
growParagraph(`${tOpen}${text}${tClose}`);
};
sax.parseSync(this.decode(data).toString().replace(/&nbsp;/g, ' '), {
onStartNode, onEndNode, onTextNode, onComment,
innerCut: new Set(['head', 'script', 'style'])
});
//текст не найден на странице, обработать корректно не получилось
if (!textFound)
return false;
const title = (titleInfo['book-title'] ? titleInfo['book-title'] : '');
let author = '';
if (titleInfo.author) {
author = _.compact([
(titleInfo.author['last-name'] ? titleInfo.author['last-name'] : ''),
(titleInfo.author['first-name'] ? titleInfo.author['first-name'] : ''),
(titleInfo.author['middle-name'] ? titleInfo.author['middle-name'] : ''),
]).join(' ');
}
pars.unshift({_n: 'title', _a: [
{_n: 'p', _t: author}, {_n: 'p', _t: ''},
{_n: 'p', _t: title}, {_n: 'p', _t: ''},
]})
return this.formatFb2(fb2);
}
}
module.exports = ConvertSamlib;

View File

@@ -1,466 +1,57 @@
const fs = require('fs-extra');
const URL = require('url').URL;
const iconv = require('iconv-lite');
const chardet = require('chardet');
const _ = require('lodash');
const sax = require('./sax');
const textUtils = require('./textUtils');
const FileDetector = require('../FileDetector');
const repSpaces = (text) => text.replace(/&nbsp;|[\t\n\r]/g, ' ');
//порядок важен
const convertClassFactory = [
require('./ConvertEpub'),
require('./ConvertPdf'),
require('./ConvertRtf'),
require('./ConvertDocX'),
require('./ConvertDoc'),
require('./ConvertMobi'),
require('./ConvertFb2'),
require('./ConvertSamlib'),
require('./ConvertHtml'),
];
class BookConverter {
constructor() {
constructor(config) {
this.detector = new FileDetector();
}
async convertToFb2(inputFile, outputFile, url, callback) {
const fileType = await this.detector.detectFile(inputFile);
const data = await fs.readFile(inputFile);
callback(100);
if (fileType && (fileType.ext == 'html' || fileType.ext == 'xml')) {
if (data.toString().indexOf('<FictionBook') >= 0) {
await fs.writeFile(outputFile, this.checkEncoding(data));
return;
}
const parsedUrl = new URL(url);
if (parsedUrl.hostname == 'samlib.ru' ||
parsedUrl.hostname == 'budclub.ru' ||
parsedUrl.hostname == 'zhurnal.lib.ru') {
await fs.writeFile(outputFile, this.convertSamlib(data));
return;
}
await fs.writeFile(outputFile, this.convertHtml(data));
return;
} else {
if (fileType)
throw new Error(`Этот формат файла не поддерживается: ${fileType.mime}`);
else {
//может это чистый текст?
if (textUtils.checkIfText(data)) {
await fs.writeFile(outputFile, this.convertHtml(data, true));
return;
}
throw new Error(`Не удалось определить формат файла: ${url}`);
}
this.convertFactory = [];
for (const convertClass of convertClassFactory) {
this.convertFactory.push(new convertClass(config));
}
}
decode(data) {
const charsetAll = chardet.detectAll(data.slice(0, 20000));
async convertToFb2(inputFiles, outputFile, url, callback) {
const selectedFileType = await this.detector.detectFile(inputFiles.selectedFile);
const data = await fs.readFile(inputFiles.selectedFile);
let selected = 'ISO-8859-5';
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {
selected = charset.name;
let result = false;
for (const convert of this.convertFactory) {
result = await convert.run(data, {inputFiles, url, callback, dataType: selectedFileType});
if (result) {
await fs.writeFile(outputFile, result);
break;
}
}
if (selected == 'ISO-8859-5') {
selected = textUtils.getEncoding(data);
if (!result && inputFiles.nesting) {
result = await this.convertToFb2(inputFiles.nesting, outputFile, url, callback);
}
return iconv.decode(data, selected);
}
checkEncoding(data) {
let result = data;
const left = data.indexOf('<?xml version="1.0"');
if (left >= 0) {
const right = data.indexOf('?>', left);
if (right >= 0) {
const head = data.slice(left, right + 2).toString();
const m = head.match(/encoding="(.*)"/);
if (m) {
let encoding = m[1].toLowerCase();
if (encoding != 'utf-8') {
result = iconv.decode(data, encoding);
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
}
}
if (!result) {
if (selectedFileType)
throw new Error(`Этот формат файла не поддерживается: ${selectedFileType.mime}`);
else {
throw new Error(`Не удалось определить формат файла: ${url}`);
}
}
callback(100);
return result;
}
convertHtml(data, isText) {
let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo};
let pars = [];
let body = {_n: 'body', section: {_a: []}};
let fb2 = [desc, body];
let title = '';
let inTitle = false;
let spaceCounter = [];
const newParagraph = () => {
pars.push({_n: 'p', _t: ''});
};
const growParagraph = (text) => {
const l = pars.length;
if (l) {
if (pars[l - 1]._t == '')
text = text.trimLeft();
pars[l - 1]._t += text;
}
//посчитаем отступы у текста, чтобы выделить потом параграфы
const lines = text.split('\n');
for (const line of lines) {
const sp = line.split(' ');
let l = 0;
while (l < sp.length && sp[l].trim() == '') {
l++;
}
if (!spaceCounter[l])
spaceCounter[l] = 0;
spaceCounter[l]++;
}
};
newParagraph();
const newPara = new Set(['tr', 'br', 'br/', 'dd', 'p', 'title', '/title', 'h1', 'h2', 'h3', '/h1', '/h2', '/h3']);
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter) {
growParagraph(text);
}
if (inTitle && !title)
title = text;
};
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!cutCounter) {
if (newPara.has(tag))
newParagraph();
}
if (tag == 'title')
inTitle = true;
};
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (tag == 'title')
inTitle = false;
};
let buf = this.decode(data).toString();
sax.parseSync(buf, {
onStartNode, onEndNode, onTextNode,
innerCut: new Set(['head', 'script', 'style'])
});
titleInfo['book-title'] = title;
//подозрение на чистый текст, надо разбить на параграфы
if (isText || pars.length < buf.length/2000) {
let total = 0;
for (let i = 0; i < spaceCounter.length; i++) {
total += (spaceCounter[i] ? spaceCounter[i] : 0);
}
total /= 10;
let i = spaceCounter.length - 1;
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
const parIndent = (i > 0 ? i : 0);
let newPars = [];
const newPar = () => {
newPars.push({_n: 'p', _t: ''});
};
const growPar = (text) => {
const l = newPars.length;
if (l) {
newPars[l - 1]._t += text;
}
}
for (const par of pars) {
newPar();
const lines = par._t.split('\n');
for (const line of lines) {
const sp = line.split(' ');
let l = 0;
while (l < sp.length && sp[l].trim() == '') {
l++;
}
if (l >= parIndent)
newPar();
growPar(line.trim() + ' ');
}
}
body.section._a[0] = newPars;
} else {
body.section._a[0] = pars;
}
//убираем лишнее
for (let i = 0; i < pars.length; i++)
pars[i]._t = repSpaces(pars[i]._t).trim();
return this.formatFb2(fb2);
}
convertSamlib(data) {
let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo};
let pars = [];
let body = {_n: 'body', section: {_a: pars}};
let fb2 = [desc, body];
let inSubtitle = false;
let inJustify = true;
let path = '';
let tag = '';// eslint-disable-line no-unused-vars
let inText = false;
let node = {_a: pars};
let inPara = false;
let italic = false;
let bold = false;
const openTag = (name) => {
if (name == 'p')
inPara = true;
let n = {_n: name, _a: [], _p: node};
node._a.push(n);
node = n;
};
const closeTag = (name) => {
if (name == 'p')
inPara = false;
if (node._p) {
const exact = (node._n == name);
node = node._p;
if (!exact)
closeTag(name);
}
};
const growParagraph = (text) => {
if (node._n == 'p' && node._a.length == 0)
text = text.trimLeft();
node._a.push({_t: text});
};
openTag('p');
const onStartNode = (elemName, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (elemName == '')
return;
if (!inText) {
path += '/' + elemName;
tag = elemName;
} else {
if (inPara && elemName != 'i' && elemName != 'b')
closeTag('p');
switch (elemName) {
case 'li':
case 'p':
case 'dd':
case 'h1':
case 'h2':
case 'h3':
openTag('p');
break;
case 'i':
openTag('emphasis');
italic = true;
break;
case 'b':
openTag('strong');
bold = true;
break;
case 'div':
if (tail.indexOf('align="center"') >= 0) {
openTag('subtitle');
inSubtitle = true;
}
if (tail.indexOf('align="justify"') >= 0) {
openTag('p');
inJustify = true;
}
break;
}
}
};
const onEndNode = (elemName, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
if (!inText) {
const oldPath = path;
let t = '';
do {
let i = path.lastIndexOf('/');
t = path.substr(i + 1);
path = path.substr(0, i);
} while (t != elemName && path);
if (t != elemName) {
path = oldPath;
}
let i = path.lastIndexOf('/');
tag = path.substr(i + 1);
} else {
switch (elemName) {
case 'li':
case 'p':
case 'dd':
case 'h1':
case 'h2':
case 'h3':
closeTag('p');
break;
case 'i':
closeTag('emphasis');
italic = false;
break;
case 'b':
closeTag('strong');
bold = false;
break;
case 'div':
if (inSubtitle) {
closeTag('subtitle');
inSubtitle = false;
}
if (inJustify) {
closeTag('p');
inJustify = false;
}
break;
}
}
};
const onComment = (text) => {// eslint-disable-line no-unused-vars
if (text == '--------- Собственно произведение -------------')
inText = true;
if (text == '-----------------------------------------------')
inText = false;
};
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
if (text != ' ' && text.trim() == '')
text = text.trim();
if (text == '')
return;
switch (path) {
case '/html/body/center/h2':
titleInfo['book-title'] = text;
return;
case '/html/body/div/h3':
if (!titleInfo.author)
titleInfo.author = {};
text = text.replace(':', '').trim().split(' ');
if (text[0])
titleInfo.author['last-name'] = text[0];
if (text[1])
titleInfo.author['first-name'] = text[1];
if (text[2])
titleInfo.author['middle-name'] = text[2];
return;
}
let tOpen = (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
let tClose = (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
if (inText)
growParagraph(`${tOpen}${text}${tClose}`);
};
sax.parseSync(repSpaces(this.decode(data).toString()), {
onStartNode, onEndNode, onTextNode, onComment,
innerCut: new Set(['head', 'script', 'style'])
});
const title = (titleInfo['book-title'] ? titleInfo['book-title'] : '');
let author = '';
if (titleInfo.author) {
author = _.compact([
(titleInfo.author['last-name'] ? titleInfo.author['last-name'] : ''),
(titleInfo.author['first-name'] ? titleInfo.author['first-name'] : ''),
(titleInfo.author['middle-name'] ? titleInfo.author['middle-name'] : ''),
]).join(' ');
}
pars.unshift({_n: 'title', _a: [
{_n: 'p', _t: author}, {_n: 'p', _t: ''},
{_n: 'p', _t: title}, {_n: 'p', _t: ''},
]})
return this.formatFb2(fb2);
}
formatFb2(fb2) {
let out = '<?xml version="1.0" encoding="utf-8"?>';
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">';
out += this.formatFb2Node(fb2);
out += '</FictionBook>';
return out;
}
formatFb2Node(node, name) {
let out = '';
if (Array.isArray(node)) {
for (const n of node) {
out += this.formatFb2Node(n);
}
} else if (typeof node == 'string') {
if (name)
out += `<${name}>${repSpaces(node)}</${name}>`;
else
out += repSpaces(node);
} else {
if (node._n)
name = node._n;
if (name)
out += `<${name}>`;
if (node.hasOwnProperty('_t'))
out += repSpaces(node._t);
for (let nodeName in node) {
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
continue;
const n = node[nodeName];
out += this.formatFb2Node(n, nodeName);
}
if (name)
out += `</${name}>`;
}
return out;
}
}
module.exports = BookConverter;

View File

@@ -11,7 +11,7 @@ function parseSync(xstr, options) {
let i = 0;
const len = xstr.length;
const progStep = len/10;
const progStep = len/20;
let nextProg = 0;
let cutCounter = 0;
@@ -151,7 +151,7 @@ async function parse(xstr, options) {
let i = 0;
const len = xstr.length;
const progStep = len/10;
const progStep = len/20;
let nextProg = 0;
let cutCounter = 0;
@@ -276,7 +276,84 @@ async function parse(xstr, options) {
await _onProgress(100);
}
function getAttrsSync(tail) {
let result = {};
let name = '';
let value = '';
let vOpen = '';
let inName = false;
let inValue = false;
let waitValue = false;
let waitEq = false;
const pushResult = () => {
if (name != '') {
let ns = '';
if (name.indexOf(':') >= 0) {
[ns, name] = name.split(':');
}
result[name] = {value, ns};
}
name = '';
value = '';
vOpen = '';
inName = false;
inValue = false;
waitValue = false;
waitEq = false;
};
tail = tail.replace(/[\t\n\r]/g, ' ');
for (let i = 0; i < tail.length; i++) {
const c = tail.charAt(i);
if (c == ' ') {
if (inValue) {
if (vOpen == '"')
value += c;
else
pushResult();
} else if (inName) {
waitEq = true;
inName = false;
}
} else if (!inValue && c == '=') {
waitEq = false;
waitValue = true;
inName = false;
} else if (c == '"') {
if (inValue) {
pushResult();
} else if (waitValue) {
inValue = true;
vOpen = '"';
}
} else if (inValue) {
value += c;
} else if (inName) {
name += c;
} else if (waitEq) {
pushResult();
inName = true;
name = c;
} else if (waitValue) {
waitValue = false;
inValue = true;
vOpen = ' ';
value = c;
} else {
inName = true;
name = c;
}
}
if (name != '')
pushResult();
return result;
}
module.exports = {
parseSync,
getAttrsSync,
parse
}

View File

@@ -1,4 +1,4 @@
function getEncoding(buf) {
function getEncoding(buf, returnAll) {
const lowerCase = 3;
const upperCase = 1;
@@ -8,6 +8,7 @@ function getEncoding(buf) {
'd': 'cp866',
'i': 'ISO-8859-5',
'm': 'maccyrillic',
'u': 'utf-8',
};
let charsets = {
@@ -15,38 +16,47 @@ function getEncoding(buf) {
'w': 0,
'd': 0,
'i': 0,
'm': 0
'm': 0,
'u': 0,
};
const len = buf.length;
const blockSize = (len > 5*3000 ? 3000 : len);
let counter = 0;
let i = 0;
let totalChecked = 0;
while (i < len) {
const char = buf[i];
const nextChar = (i < len - 1 ? buf[i + 1] : 0);
totalChecked++;
i++;
//non-russian characters
if (char < 128 || char > 256)
continue;
//CP866
if ((char > 159 && char < 176) || (char > 223 && char < 242)) charsets['d'] += lowerCase;
if ((char > 127 && char < 160)) charsets['d'] += upperCase;
//UTF-8
if ((char == 208 || char == 209) && nextChar >= 128 && nextChar <= 190)
charsets['u'] += lowerCase;
else {
//CP866
if ((char > 159 && char < 176) || (char > 223 && char < 242)) charsets['d'] += lowerCase;
if ((char > 127 && char < 160)) charsets['d'] += upperCase;
//KOI8-R
if ((char > 191 && char < 223)) charsets['k'] += lowerCase;
if ((char > 222 && char < 256)) charsets['k'] += upperCase;
//KOI8-R
if ((char > 191 && char < 223)) charsets['k'] += lowerCase;
if ((char > 222 && char < 256)) charsets['k'] += upperCase;
//WIN-1251
if (char > 223 && char < 256) charsets['w'] += lowerCase;
if (char > 191 && char < 224) charsets['w'] += upperCase;
//WIN-1251
if (char > 223 && char < 256) charsets['w'] += lowerCase;
if (char > 191 && char < 224) charsets['w'] += upperCase;
//MAC
if (char > 221 && char < 255) charsets['m'] += lowerCase;
if (char > 127 && char < 160) charsets['m'] += upperCase;
//MAC
if (char > 221 && char < 255) charsets['m'] += lowerCase;
if (char > 127 && char < 160) charsets['m'] += upperCase;
//ISO-8859-5
if (char > 207 && char < 240) charsets['i'] += lowerCase;
if (char > 175 && char < 208) charsets['i'] += upperCase;
//ISO-8859-5
if (char > 207 && char < 240) charsets['i'] += lowerCase;
if (char > 175 && char < 208) charsets['i'] += upperCase;
}
counter++;
@@ -57,18 +67,24 @@ function getEncoding(buf) {
}
let sorted = Object.keys(charsets).map(function(key) {
return { codePage: codePage[key], c: charsets[key] };
return { codePage: codePage[key], c: charsets[key], totalChecked };
});
sorted.sort((a, b) => b.c - a.c);
if (sorted[0].c > 0)
if (returnAll)
return sorted;
else if (sorted[0].c > 0)
return sorted[0].codePage;
else
return 'ISO-8859-5';
}
function checkIfText(buf) {
const enc = getEncoding(buf, true);
if (enc[0].c > enc[0].totalChecked*0.9)
return true;
let spaceCount = 0;
let crCount = 0;
let lfCount = 0;
@@ -85,7 +101,7 @@ function checkIfText(buf) {
const crFreq = crCount/(buf.length + 1);
const lfFreq = lfCount/(buf.length + 1);
return (spaceFreq > 0.1 || crFreq > 0.03 || lfFreq > 0.03);
return (buf.length < 1000 || spaceFreq > 0.1 || crFreq > 0.03 || lfFreq > 0.03);
}
module.exports = {

View File

@@ -1,8 +1,12 @@
const fs = require('fs-extra');
const zlib = require('zlib');
const crypto = require('crypto');
const path = require('path');
const unbzip2Stream = require('unbzip2-stream');
const tar = require('tar-fs');
const DecompressZip = require('decompress-zip');
const utils = require('./utils');
const decompress = require('decompress');
const FileDetector = require('./FileDetector');
class FileDecompressor {
@@ -10,29 +14,200 @@ class FileDecompressor {
this.detector = new FileDetector();
}
async decompressFile(filename, outputDir) {
async decompressNested(filename, outputDir) {
await fs.ensureDir(outputDir);
const fileType = await this.detector.detectFile(filename);
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2'))
return filename;
let result = {
sourceFile: filename,
sourceFileType: fileType,
selectedFile: filename,
filesDir: outputDir,
files: []
};
const files = await decompress(filename, outputDir);
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) {
return result;
}
let result = filename;
result.files = await this.decompressTarZZ(fileType.ext, filename, outputDir);
let sel = filename;
let max = 0;
if (files.length) {
if (result.files.length) {
//ищем файл с максимальным размером
for (let file of files) {
if (file.data.length > max) {
result = `${outputDir}/${file.path}`;
max = file.data.length;
for (let file of result.files) {
if (file.size > max) {
sel = `${outputDir}/${file.path}`;
max = file.size;
}
}
}
result.selectedFile = sel;
if (sel != filename) {
result.nesting = await this.decompressNested(sel, `${outputDir}/${utils.randomHexString(10)}`);
}
return result;
}
async unpack(filename, outputDir) {
const fileType = await this.detector.detectFile(filename);
if (!fileType)
throw new Error('Не удалось определить формат файла');
return await this.decompress(fileType.ext, filename, outputDir);
}
async unpackTarZZ(filename, outputDir) {
const fileType = await this.detector.detectFile(filename);
if (!fileType)
throw new Error('Не удалось определить формат файла');
return await this.decompressTarZZ(fileType.ext, filename, outputDir);
}
async decompressTarZZ(fileExt, filename, outputDir) {
const files = await this.decompress(fileExt, filename, outputDir);
if (fileExt == 'tar' || files.length != 1)
return files;
const tarFilename = `${outputDir}/${files[0].path}`;
const fileType = await this.detector.detectFile(tarFilename);
if (!fileType || fileType.ext != 'tar')
return files;
const newTarFilename = `${outputDir}/${utils.randomHexString(30)}`;
await fs.rename(tarFilename, newTarFilename);
const tarFiles = await this.decompress('tar', newTarFilename, outputDir);
await fs.remove(newTarFilename);
return tarFiles;
}
async decompress(fileExt, filename, outputDir) {
let files = [];
switch (fileExt) {
case 'zip':
files = await this.unZip(filename, outputDir);
break;
case 'bz2':
files = await this.unBz2(filename, outputDir);
break;
case 'gz':
files = await this.unGz(filename, outputDir);
break;
case 'tar':
files = await this.unTar(filename, outputDir);
break;
default:
throw new Error(`FileDecompressor: неизвестный формат файла '${fileExt}'`);
}
return files;
}
async unZip(filename, outputDir) {
return new Promise((resolve, reject) => {
const files = [];
const unzipper = new DecompressZip(filename);
unzipper.on('error', function(err) {
reject(err);
});
unzipper.on('extract', function() {
resolve(files);
});
unzipper.extract({
path: outputDir,
filter: function(file) {
if (file.type == 'File')
files.push({path: file.path, size: file.uncompressedSize});
return true;
}
});
});
}
unBz2(filename, outputDir) {
return this.decompressByStream(unbzip2Stream(), filename, outputDir);
}
unGz(filename, outputDir) {
return this.decompressByStream(zlib.createGunzip(), filename, outputDir);
}
unTar(filename, outputDir) {
return new Promise((resolve, reject) => {
const files = [];
const tarExtract = tar.extract(outputDir, {
map: (header) => {
files.push({path: header.name, size: header.size});
return header;
}
});
tarExtract.on('finish', () => {
resolve(files);
});
tarExtract.on('error', (err) => {
reject(err);
});
const inputStream = fs.createReadStream(filename);
inputStream.on('error', (err) => {
reject(err);
});
inputStream.pipe(tarExtract);
});
}
decompressByStream(stream, filename, outputDir) {
return new Promise(async(resolve, reject) => {
const file = {path: path.parse(filename).name};
let outFilename = `${outputDir}/${file.path}`;
if (await fs.pathExists(outFilename)) {
file.path = `${utils.randomHexString(10)}-${file.path}`;
outFilename = `${outputDir}/${file.path}`;
}
const inputStream = fs.createReadStream(filename);
const outputStream = fs.createWriteStream(outFilename);
outputStream.on('finish', async() => {
try {
file.size = (await fs.stat(outFilename)).size;
} catch (e) {
reject(e);
}
resolve([file]);
});
stream.on('error', (err) => {
reject(err);
});
inputStream.on('error', (err) => {
reject(err);
});
outputStream.on('error', (err) => {
reject(err);
});
inputStream.pipe(stream).pipe(outputStream);
});
}
async gzipBuffer(buf) {
return new Promise((resolve, reject) => {
zlib.gzip(buf, {level: 1}, (err, result) => {

View File

@@ -1,57 +0,0 @@
const detect = require('detect-file-type');
//html
detect.addSignature(
{
"type": "html",
"ext": "html",
"mime": "text/html",
"rules": [
{ "type": "or", "rules":
[
{ "type": "contains", "bytes": "3c68746d6c" },
{ "type": "contains", "bytes": "3c00680074006d006c00" },
{ "type": "contains", "bytes": "3c21646f6374797065" },
{ "type": "contains", "bytes": "3c626f6479" },
{ "type": "contains", "bytes": "3c68656164" },
{ "type": "contains", "bytes": "3c696672616d65" },
{ "type": "contains", "bytes": "3c696d67" },
{ "type": "contains", "bytes": "3c6f626a656374" },
{ "type": "contains", "bytes": "3c736372697074" },
{ "type": "contains", "bytes": "3c7461626c65" },
{ "type": "contains", "bytes": "3c7469746c65" },
]
}
]
}
);
//xml 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 2e 30 22
detect.addSignature(
{
"type": "xml",
"ext": "xml",
"mime": "application/xml",
"rules": [
{ "type": "or", "rules":
[
{ "type": "contains", "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
]
}
]
}
);
class FileDetector {
detectFile(filename) {
return new Promise((resolve, reject) => {
detect.fromFile(filename, (err, result) => {
if (err) reject(err);
resolve(result);
});
});
}
}
module.exports = FileDetector;

View File

@@ -0,0 +1,274 @@
const fs = require('fs');
const signatures = require('./signatures.json');
class FileDetector {
detectFile(filename) {
return new Promise((resolve, reject) => {
this.fromFile(filename, 2000, (err, result) => {
if (err) reject(err);
resolve(result);
});
});
}
//все, что ниже, взято здесь: https://github.com/dimapaloskin/detect-file-type
fromFile(filePath, bufferLength, callback) {
if (typeof bufferLength === 'function') {
callback = bufferLength;
bufferLength = undefined;
}
this.getFileSize(filePath, (err, fileSize) => {
if (err) {
return callback(err);
}
fs.open(filePath, 'r', (err, fd) => {
if (err) {
return callback(err);
}
let bufferSize = bufferLength;
if (!bufferSize) {
bufferSize = 500;
}
if (fileSize < bufferSize) {
bufferSize = fileSize;
}
const buffer = Buffer.alloc(bufferSize);
fs.read(fd, buffer, 0, bufferSize, 0, (err) => {
fs.close(fd);
if (err) {
return callback(err);
}
this.fromBuffer(buffer, callback);
});
});
});
}
fromBuffer(buffer, callback) {
let result = null;
const invalidSignaturesList = this.validateSigantures();
if (invalidSignaturesList.length) {
return callback(invalidSignaturesList);
}
signatures.every((signature) => {
if (this.detect(buffer, signature.rules)) {
result = {
ext: signature.ext,
mime: signature.mime
};
if (signature.iana)
result.iana = signature.iana;
return false;
}
return true;
});
callback(null, result);
}
detect(buffer, receivedRules, type) {
if (!type) {
type = 'and';
}
const rules = [...receivedRules];
let isDetected = true;
rules.every((rule) => {
if (rule.type === 'equal') {
const slicedHex = buffer.slice(rule.start || 0, rule.end || buffer.length).toString('hex');
isDetected = (slicedHex === rule.bytes);
return this.isReturnFalse(isDetected, type);
}
if (rule.type === 'notEqual') {
const slicedHex = buffer.slice(rule.start || 0, rule.end || buffer.length).toString('hex');
isDetected = !(slicedHex === rule.bytes);
return this.isReturnFalse(isDetected, type);
}
if (rule.type === 'contains') {
const slicedHex = buffer.slice(rule.start || 0, rule.end || buffer.length).toString('hex');
if (typeof rule.bytes === 'string') {
rule.bytes = [rule.bytes];
}
rule.bytes.every((bytes) => {
isDetected = (slicedHex.indexOf(bytes) !== -1);
return isDetected;
});
return this.isReturnFalse(isDetected, type);
}
if (rule.type === 'notContains') {
const slicedHex = buffer.slice(rule.start || 0, rule.end || buffer.length).toString('hex');
if (typeof rule.bytes === 'string') {
rule.bytes = [rule.bytes];
}
rule.bytes.every((bytes) => {
isDetected = (slicedHex.indexOf(bytes) === -1);
return isDetected;
});
return this.isReturnFalse(isDetected, type);
}
if (rule.type === 'or') {
isDetected = this.detect(buffer, rule.rules, 'or');
return this.isReturnFalse(isDetected, type);
}
if (rule.type === 'and') {
isDetected = this.detect(buffer, rule.rules, 'and');
return this.isReturnFalse(isDetected, type);
}
return true;
});
return isDetected;
}
isReturnFalse(isDetected, type) {
if (!isDetected && type === 'and') {
return false;
}
if (isDetected && type === 'or') {
return false;
}
return true;
}
validateRuleType(rule) {
const types = ['or', 'and', 'contains', 'notContains', 'equal', 'notEqual'];
return (types.indexOf(rule.type) !== -1);
}
validateSigantures() {
let invalidSignatures = signatures.map((signature) => {
return this.validateSignature(signature);
});
invalidSignatures = this.cleanArray(invalidSignatures);
if (invalidSignatures.length) {
return invalidSignatures;
}
return true;
}
validateSignature(signature) {
if (!('type' in signature)) {
return {
message: 'signature does not contain "type" field',
signature
};
}
if (!('ext' in signature)) {
return {
message: 'signature does not contain "ext" field',
signature
};
}
if (!('mime' in signature)) {
return {
message: 'signature does not contain "mime" field',
signature
};
}
if (!('rules' in signature)) {
return {
message: 'signature does not contain "rules" field',
signature
};
}
const invalidRules = this.validateRules(signature.rules);
if (invalidRules && invalidRules.length) {
return {
message: 'signature has invalid rule',
signature,
rules: invalidRules
}
}
}
validateRules(rules) {
let invalidRules = rules.map((rule) => {
let isRuleTypeValid = this.validateRuleType(rule);
if (!isRuleTypeValid) {
return {
message: 'rule type does not supported',
rule
};
}
if ((rule.type === 'or' || rule.type === 'and') && !('rules' in rule)) {
return {
message: 'rule should contains "rules" field',
rule
};
}
if (rule.type === 'or' || rule.type === 'and') {
return this.validateRules(rule.rules);
}
return false;
});
invalidRules = this.cleanArray(invalidRules);
if (invalidRules.length) {
return invalidRules;
}
}
cleanArray(actual) {
let newArray = new Array();
for (let i = 0; i < actual.length; i++) {
if (actual[i]) {
newArray.push(actual[i]);
}
}
return newArray;
}
addSignature(signature) {
signatures.push(signature);
}
getFileSize(filePath, callback) {
fs.stat(filePath, (err, stat) => {
if (err) {
return callback(err);
}
return callback(null, stat.size);
});
}
}
module.exports = FileDetector;

View File

@@ -0,0 +1,726 @@
[
{
"type": "jpg",
"ext": "jpg",
"mime": "image/jpeg",
"rules": [
{ "type": "equal", "start": 0, "end": 2, "bytes": "ffd8" }
]
},
{
"type": "png",
"ext": "png",
"mime": "image/png",
"rules": [
{ "type": "equal", "start": 0,"end": 4, "bytes": "89504e47" }
]
},
{
"type": "gif",
"ext": "gif",
"mime": "image/gif",
"rules": [
{ "type": "equal", "start": 0,"end": 3, "bytes": "474946" }
]
},
{
"type": "bmp",
"ext": "bmp",
"mime": "image/bmp",
"rules": [
{ "type": "equal", "start": 0,"end": 2, "bytes": "424d" }
]
},
{
"type": "webp",
"ext": "webp",
"mime": "image/webp",
"rules": [
{ "type": "equal", "start": 8,"end": 12, "bytes": "57454250" }
]
},
{
"type": "tif",
"ext": "tif",
"mime": "image/tiff",
"rules": [
{ "type": "and", "rules":
[
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 4, "bytes": "49492a00" },
{ "type": "equal", "start": 0, "end": 4, "bytes": "4d4d002a" }
]
},
{ "type": "notEqual", "start": 8, "end": 10, "bytes": "4352" }
]
}
]
},
{
"type": "cr2",
"ext": "cr2",
"mime": "image/x-canon-cr2",
"rules": [
{ "type": "and", "rules":
[
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 4, "bytes": "49492a00" },
{ "type": "equal", "start": 0, "end": 4, "bytes": "4d4d002a" }
]
},
{ "type": "equal", "start": 8, "end": 10, "bytes": "4352" }
]
}
]
},
{
"type": "jxr",
"ext": "jxr",
"mime": "image/vnd.ms-photo",
"rules": [
{ "type": "equal", "start": 0, "end": 3, "bytes": "4949bc" }
]
},
{
"type": "psd",
"ext": "psd",
"mime": "image/vnd.adobe.photoshop",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "38425053" }
]
},
{
"type": "flif",
"ext": "flif",
"mime": "image/flif",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "464c4946" }
]
},
{
"type": "zip",
"ext": "zip",
"mime": "application/zip",
"rules": [
{ "type": "equal", "start": 0, "end": 2, "bytes": "504b" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 2, "end": 3, "bytes": "03" },
{ "type": "equal", "start": 2, "end": 3, "bytes": "05" },
{ "type": "equal", "start": 2, "end": 3, "bytes": "07" }
]
},
{ "type": "or", "rules":
[
{ "type": "equal", "start": 3, "end": 4, "bytes": "04" },
{ "type": "equal", "start": 3, "end": 4, "bytes": "06" },
{ "type": "equal", "start": 3, "end": 4, "bytes": "08" }
]
},
{ "type": "notEqual", "start": 30, "end": 50, "bytes": "4d4554412d494e462f6d6f7a696c6c612e727361" }
]
},
{
"type": "xpi",
"ext": "xpi",
"mime": "application/x-xpinstall",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "504b0304" },
{ "type": "equal", "start": 30, "end": 50, "bytes": "4d4554412d494e462f6d6f7a696c6c612e727361" }
]
},
{
"type": "tar",
"ext": "tar",
"mime": "application/x-tar",
"rules": [
{ "type": "equal", "start": 257, "end": 262, "bytes": "7573746172" }
]
},
{
"type": "rar",
"ext": "rar",
"mime": "application/x-rar-compressed",
"rules": [
{ "type": "equal", "start": 0, "end": 6, "bytes": "526172211a07" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 6, "end": 7, "bytes": "00" },
{ "type": "equal", "start": 6, "end": 7, "bytes": "01" }
]
}
]
},
{
"type": "gz",
"ext": "gz",
"mime": "application/gzip",
"rules": [
{ "type": "equal", "start": 0, "end": 3, "bytes": "1f8b08" }
]
},
{
"type": "bz2",
"ext": "bz2",
"mime": "application/x-bzip2",
"rules": [
{ "type": "equal", "start": 0, "end": 3, "bytes": "425a68" }
]
},
{
"type": "7z",
"ext": "7z",
"mime": "application/x-7z-compressed",
"rules": [
{ "type": "equal", "start": 0, "end": 6, "bytes": "377abcaf271c" }
]
},
{
"type": "dmg",
"ext": "dmg",
"mime": "application/x-apple-diskimage",
"rules": [
{ "type": "equal", "start": 0, "end": 2, "bytes": "7801" }
]
},
{
"type": "mp4",
"ext": "mp4",
"mime": "video/mp4",
"rules": [
{ "type": "or", "rules":
[
{ "type": "and", "rules":
[
{ "type": "equal", "start": 0, "end": 3, "bytes": "000000" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 3, "end": 4, "bytes": "18" },
{ "type": "equal", "start": 3, "end": 4, "bytes": "20" }
]
},
{ "type": "equal", "start": 4, "end": 8, "bytes": "66747970" }
]
},
{ "type": "equal", "start": 0, "end": 4, "bytes": "33677035" },
{ "type": "and", "rules":
[
{ "type": "equal", "start": 0, "end": 11, "bytes": "0000001c667479706d7034" },
{ "type": "equal", "start": 16, "end": 28, "bytes": "6d7034316d70343269736f6d" }
]
},
{ "type": "equal", "start": 0, "end": 12, "bytes": "0000001c6674797069736f6d" },
{ "type": "equal", "start": 0, "end": 16, "bytes": "0000001c667479706d70343200000000" }
]
}
]
},
{
"type": "m4v",
"ext": "m4v",
"mime": "video/x-m4v",
"rules": [
{ "type": "equal", "start": 0, "end": 11, "bytes": "0000001c667479704d3456" }
]
},
{
"type": "mid",
"ext": "mid",
"mime": "audio/midi",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "4d546864" }
]
},
{
"type": "mkv",
"ext": "mkv",
"mime": "video/x-matroska",
"rules": [
{ "type": "equal", "start": 31, "end": 39, "bytes": "6d6174726f736b61" }
]
},
{
"type": "webm",
"ext": "webm",
"mime": "video/webm",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "1a45dfa3" },
{ "type": "notEqual", "start": 31, "end": 39, "bytes": "6d6174726f736b61" }
]
},
{
"type": "wmv",
"ext": "wmv",
"mime": "video/x-ms-wmv",
"rules": [
{ "type": "equal", "start": 0, "end": 10, "bytes": "3026b2758e66cf11a6d9" }
]
},
{
"type": "mpg",
"ext": "mpg",
"mime": "video/mpeg",
"rules": [
{ "type": "equal", "start": 0, "end": 3, "bytes": "000001" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 3, "end": 4, "bytes": "b0"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b1"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b2"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b3"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b4"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b5"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b6"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b7"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b8"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "b9"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "ba"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "bb"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "bc"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "bd"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "be"},
{ "type": "equal", "start": 3, "end": 4, "bytes": "bf"}
]
}
]
},
{
"type": "mp3",
"ext": "mp3",
"mime": "audio/mpeg",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 3, "bytes": "494433" },
{ "type": "equal", "start": 0, "end": 2, "bytes": "fffb" }
]
}
]
},
{
"type": "m4a",
"ext": "m4a",
"mime": "audio/m4a",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "start": 4, "end": 11, "bytes": "667479704d3441" },
{ "type": "equal", "start": 0, "end": 4, "bytes": "4d344120" }
]
}
]
},
{
"type": "opus",
"ext": "opus",
"mime": "audio/opus",
"rules": [
{ "type": "equal", "start": 28, "end": 36, "bytes": "4f70757348656164" }
]
},
{
"type": "ogg",
"ext": "ogg",
"mime": "audio/ogg",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "4f676753" },
{ "type": "notEqual", "start": 28, "end": 36, "bytes": "4f70757348656164" }
]
},
{
"type": "flac",
"ext": "flac",
"mime": "audio/x-flac",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "664c6143" }
]
},
{
"type": "wav",
"ext": "wav",
"mime": "audio/x-wav",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "52494646" },
{ "type": "equal", "start": 8, "end": 12, "bytes": "57415645" }
]
},
{
"type": "amr",
"ext": "amr",
"mime": "audio/amr",
"rules": [
{ "type": "equal", "start": 0, "end": 6, "bytes": "2321414d520a" }
]
},
{
"type": "pdf",
"ext": "pdf",
"mime": "application/pdf",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "25504446" }
]
},
{
"type": "exe",
"ext": "exe",
"mime": "application/x-msdownload",
"iana": "application/vnd.microsoft.portable-executable",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 2, "bytes": "4d5a" },
{ "type": "equal", "start": 0, "end": 2, "bytes": "4d7a" },
{ "type": "equal", "start": 0, "end": 2, "bytes": "6d7a" },
{ "type": "equal", "start": 0, "end": 2, "bytes": "6d5a" }
]
}
]
},
{
"type": "swf",
"ext": "swf",
"mime": "application/x-shockwave-flash",
"iana": "application/vnd.adobe.flash.movie",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 1, "bytes": "43" },
{ "type": "equal", "start": 0, "end": 1, "bytes": "46" }
]
},
{ "type": "equal", "start": 1, "end": 3, "bytes": "5753" }
]
},
{
"type": "rtf",
"ext": "rtf",
"mime": "application/rtf",
"rules": [
{ "type": "equal", "start": 0, "end": 5, "bytes": "7b5c727466" }
]
},
{
"type": "mov",
"ext": "mov",
"mime": "video/quicktime",
"rules": [
{ "type": "equal", "start": 0, "end": 8, "bytes": "0000001466747970" }
]
},
{
"type": "avi",
"ext": "avi",
"mime": "video/x-msvideo",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "52494646" },
{ "type": "equal", "start": 8, "end": 11, "bytes": "415649" }
]
},
{
"type": "woff",
"ext": "woff",
"mime": "application/font-woff",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "774f4646" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 4, "end": 8, "bytes": "00010000" },
{ "type": "equal", "start": 4, "end": 8, "bytes": "4f54544f" }
]
}
]
},
{
"type": "woff2",
"ext": "woff2",
"mime": "application/font-woff",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "774f4632" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 4, "end": 8, "bytes": "00010000" },
{ "type": "equal", "start": 4, "end": 8, "bytes": "4f54544f" }
]
}
]
},
{
"type": "eot",
"ext": "eot",
"mime": "application/octet-stream",
"rules": [
{ "type": "equal", "start": 34, "end": 36, "bytes": "4c50" },
{ "type": "or", "rules":
[
{ "type": "equal", "start": 8, "end": 11, "bytes": "000001" },
{ "type": "equal", "start": 8, "end": 11, "bytes": "010002" },
{ "type": "equal", "start": 8, "end": 11, "bytes": "020002" }
]
}
]
},
{
"type": "ttf",
"ext": "ttf",
"mime": "application/font-sfnt",
"rules": [
{ "type": "equal", "start": 0, "end": 5, "bytes": "0001000000" }
]
},
{
"type": "otf",
"ext": "otf",
"mime": "application/font-sfnt",
"rules": [
{ "type": "equal", "start": 0, "end": 5, "bytes": "4f54544f00" }
]
},
{
"type": "ico",
"ext": "ico",
"mime": "application/x-icon",
"iana": "image/vnd.microsoft.icon",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "00000100" }
]
},
{
"type": "flv",
"ext": "flv",
"mime": "application/x-flv",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "464c5601" }
]
},
{
"type": "ps",
"ext": "ps",
"mime": "application/postscript",
"rules": [
{ "type": "equal", "start": 0, "end": 2, "bytes": "2521" }
]
},
{
"type": "xz",
"ext": "xz",
"mime": "application/x-xz",
"rules": [
{ "type": "equal", "start": 0, "end": 6, "bytes": "fd377a585a00" }
]
},
{
"type": "sqlite",
"ext": "sqlite",
"mime": "application/x-sqlite3",
"iana": "application/vnd.sqlite3",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "53514c69" }
]
},
{
"type": "nes",
"ext": "nes",
"mime": "application/x-nintendo-nes-rom",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "4e45531a" }
]
},
{
"type": "crx",
"ext": "crx",
"mime": "application/x-google-chrome-extension",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "43723234" }
]
},
{
"type": "cab",
"ext": "cab",
"mime": "application/vnd.ms-cab-compressed",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 4, "bytes": "4d534346" },
{ "type": "equal", "start": 0, "end": 4, "bytes": "49536328" }
]
}
]
},
{
"type": "ar",
"ext": "ar",
"mime": "application/x-unix-archive",
"rules": [
{ "type": "equal", "start": 0, "end": 7, "bytes": "213c617263683e" },
{ "type": "notEqual", "start": 0, "end": 21, "bytes": "213c617263683e0a64656269616e2d62696e617279" }
]
},
{
"type": "deb",
"ext": "deb",
"mime": "application/x-deb",
"rules": [
{ "type": "equal", "start": 0, "end": 21, "bytes": "213c617263683e0a64656269616e2d62696e617279" }
]
},
{
"type": "rpm",
"ext": "rpm",
"mime": "application/x-rpm",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "edabeedb" }
]
},
{
"type": "Z",
"ext": "Z",
"mime": "application/x-compress",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "start": 0, "end": 2, "bytes": "1fa0" },
{ "type": "equal", "start": 0, "end": 2, "bytes": "1f9d" }
]
}
]
},
{
"type": "lz",
"ext": "lz",
"mime": "application/x-lzip",
"rules": [
{ "type": "equal", "start": 0, "end": 4, "bytes": "4c5a4950" }
]
},
{
"type": "msi",
"ext": "msi",
"mime": "application/x-msi",
"rules": [
{ "type": "equal", "start": 0, "end": 8, "bytes": "d0cf11e0a1b11ae1" }
]
},
{
"type": "svg",
"ext": "svg",
"mime": "image/svg+xml",
"rules": [
{ "type": "contains", "bytes": "3c737667" }
]
},
{
"type": "html",
"ext": "html",
"mime": "text/html",
"rules": [
{ "type": "or", "rules":
[
{ "type": "contains", "bytes": "3c68746d6c" },
{ "type": "contains", "bytes": "3c00680074006d006c00" },
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
]
}
]
},
{
"type": "docx",
"ext": "docx",
"mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"rules": [
{ "type": "or", "rules":
[
{ "type": "contains", "bytes": "6170706c69636174696f6e2f766e642e6f70656e786d6c666f726d6174732d6f6666696365646f63756d656e74" }
]
}
]
},
{
"type": "xml",
"ext": "xml",
"mime": "application/xml",
"rules": [
{ "type": "or", "rules":
[
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" }
]
}
]
},
{
"type": "mobi",
"ext": "mobi",
"mime": "application/x-mobipocket-ebook",
"rules": [
{ "type": "equal", "start": 64, "end": 68, "bytes": "4d4f4249" }
]
}
]

View File

@@ -8,8 +8,14 @@ class FileDownloader {
async load(url, callback) {
let errMes = '';
const options = {
encoding: null,
headers: {
'user-agent': 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0'
}
};
const response = await got(url, {method: 'HEAD'});
const response = await got(url, Object.assign({}, options, {method: 'HEAD'}));
let estSize = 0;
if (response.headers['content-length']) {
@@ -17,7 +23,7 @@ class FileDownloader {
}
let prevProg = 0;
const request = got(url, {encoding: null}).on('downloadProgress', progress => {
const request = got(url, options).on('downloadProgress', progress => {
if (progress.transferred > maxDownloadSize) {
errMes = 'file too big';
request.cancel();

View File

@@ -7,6 +7,7 @@ const FileDownloader = require('./FileDownloader');
const FileDecompressor = require('./FileDecompressor');
const BookConverter = require('./BookConverter');
const utils = require('./utils');
const log = require('./getLogger').getLog();
let singleCleanExecute = false;
@@ -22,7 +23,7 @@ class ReaderWorker {
this.down = new FileDownloader();
this.decomp = new FileDecompressor();
this.bookConverter = new BookConverter();
this.bookConverter = new BookConverter(this.config);
if (!singleCleanExecute) {
this.periodicCleanDir(this.config.tempPublicDir, this.config.maxTempPublicDirSize, 60*60*1000);//1 раз в час
@@ -63,17 +64,24 @@ class ReaderWorker {
//decompress
wState.set({state: 'decompress', step: 2, progress: 0});
decompDir = `${this.config.tempDownloadDir}/${decompDirname}`;
const decompFilename = await this.decomp.decompressFile(downloadedFilename, decompDir);
let decompFiles = {};
try {
decompFiles = await this.decomp.decompressNested(downloadedFilename, decompDir);
} catch (e) {
if (this.config.branch == 'development')
console.error(e);
throw new Error('Ошибка распаковки');
}
wState.set({progress: 100});
//parse book
//конвертирование в fb2
wState.set({state: 'convert', step: 3, progress: 0});
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
await this.bookConverter.convertToFb2(decompFilename, convertFilename, url, progress => {
await this.bookConverter.convertToFb2(decompFiles, convertFilename, url, progress => {
wState.set({progress});
});
//compress file to tmp dir, if not exists with the same hashname
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
const compFilename = await this.decomp.gzipFileIfNotExists(convertFilename, `${this.config.tempPublicDir}`);
wState.set({progress: 100});
@@ -83,8 +91,9 @@ class ReaderWorker {
wState.finish({path: `/tmp/${finishFilename}`});
} catch (e) {
if (this.config.branch == 'development')
console.error(e);
wState.set({state: 'error', error: (errMes ? errMes : e.message)});
} finally {
//clean
if (decompDir)
@@ -123,32 +132,40 @@ class ReaderWorker {
return `file://${hash}`;
}
async periodicCleanDir(dir, maxSize, timeout) {
const list = await fs.readdir(dir);
async periodicCleanDir(dir, maxSize, timeout) {
try {
log(`Start clean dir: ${dir}, maxSize=${maxSize}`);
const list = await fs.readdir(dir);
let size = 0;
let files = [];
for (const name of list) {
const stat = await fs.stat(`${dir}/${name}`);
if (!stat.isDirectory()) {
size += stat.size;
files.push({name, stat});
let size = 0;
let files = [];
for (const name of list) {
const stat = await fs.stat(`${dir}/${name}`);
if (!stat.isDirectory()) {
size += stat.size;
files.push({name, stat});
}
}
log(`found ${files.length} files in dir ${dir}`);
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
let i = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
log(`rm ${dir}/${file.name}`);
await fs.remove(`${dir}/${file.name}`);
size -= file.stat.size;
i++;
}
log(`removed ${i} files`);
} catch(e) {
log(LM_ERR, e.message);
} finally {
setTimeout(() => {
this.periodicCleanDir(dir, maxSize, timeout);
}, timeout);
}
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
let i = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
await fs.remove(`${dir}/${file.name}`);
size -= file.stat.size;
i++;
}
setTimeout(() => {
this.periodicCleanDir(dir, maxSize, timeout);
}, timeout);
}
}

View File

@@ -1,91 +0,0 @@
const utils = require('./utils');
const sqlite = require('sqlite');
const waitingDelay = 100; //ms
class SqliteConnectionPool {
constructor(connCount, config) {
this.connCount = connCount;
this.config = config;
}
async init() {
const dbFileName = this.config.dataDir + '/' + this.config.dbFileName;
this.connections = [];
this.taken = new Set();
this.freed = new Set();
for (let i = 0; i < this.connCount; i++) {
let client = await sqlite.open(dbFileName);
client.configure('busyTimeout', 10000); //ms
client.ret = () => {
this.taken.delete(i);
this.freed.add(i);
};
this.freed.add(i);
this.connections[i] = client;
}
}
_setImmediate() {
return new Promise((resolve) => {
setImmediate(() => {
return resolve();
});
});
}
async get() {
if (this.closed)
return;
let freeConnIndex = this.freed.values().next().value;
if (freeConnIndex == null) {
if (waitingDelay)
await utils.sleep(waitingDelay);
return await this._setImmediate().then(() => this.get());
}
this.freed.delete(freeConnIndex);
this.taken.add(freeConnIndex);
return this.connections[freeConnIndex];
}
async run(query) {
const dbh = await this.get();
try {
let result = await dbh.run(query);
dbh.ret();
return result;
} catch (e) {
dbh.ret();
throw e;
}
}
async all(query) {
const dbh = await this.get();
try {
let result = await dbh.all(query);
dbh.ret();
return result;
} catch (e) {
dbh.ret();
throw e;
}
}
async close() {
for (let i = 0; i < this.connections.length; i++) {
await this.connections[i].close();
}
this.closed = true;
}
}
module.exports = SqliteConnectionPool;

View File

@@ -0,0 +1,118 @@
const SQL = require('sql-template-strings');
const _ = require('lodash');
const connManager = require('../db/connManager');
class ReaderStorage {
constructor() {
this.storagePool = connManager.pool.readerStorage;
this.periodicCleanCache(3*3600*1000);//1 раз в 3 часа
}
async doAction(act) {
if (!_.isObject(act.items))
throw new Error('items is not an object');
let result = {};
switch (act.action) {
case 'check':
result = await this.checkItems(act.items);
break;
case 'get':
result = await this.getItems(act.items);
break;
case 'set':
result = await this.setItems(act.items, act.force);
break;
default:
throw new Error('Unknown action');
}
return result;
}
async checkItems(items) {
let result = {state: 'success', items: {}};
const dbh = await this.storagePool.get();
try {
for (const id of Object.keys(items)) {
if (this.cache[id]) {
result.items[id] = this.cache[id];
} else {
const rows = await dbh.all(SQL`SELECT rev FROM storage WHERE id = ${id}`);
const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
result.items[id] = {rev};
this.cache[id] = result.items[id];
}
}
} finally {
dbh.ret();
}
return result;
}
async getItems(items) {
let result = {state: 'success', items: {}};
const dbh = await this.storagePool.get();
try {
for (const id of Object.keys(items)) {
const rows = await dbh.all(SQL`SELECT rev, data FROM storage WHERE id = ${id}`);
const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
const data = (rows.length && rows[0].data ? rows[0].data : '');
result.items[id] = {rev, data};
}
} finally {
dbh.ret();
}
return result;
}
async setItems(items, force) {
let check = await this.checkItems(items);
//сначала проверим совпадение ревизий
for (const id of Object.keys(items)) {
if (!_.isString(items[id].data))
throw new Error('items.data is not a string');
if (!force && check.items[id].rev + 1 !== items[id].rev)
return {state: 'reject', items: check.items};
}
const dbh = await this.storagePool.get();
await dbh.run('BEGIN');
try {
const newRev = {};
for (const id of Object.keys(items)) {
await dbh.run(SQL`INSERT OR REPLACE INTO storage (id, rev, time, data) VALUES (${id}, ${items[id].rev}, strftime('%s','now'), ${items[id].data})`);
newRev[id] = {rev: items[id].rev};
}
await dbh.run('COMMIT');
Object.assign(this.cache, newRev);
} catch (e) {
await dbh.run('ROLLBACK');
throw e;
} finally {
dbh.ret();
}
return {state: 'success'};
}
periodicCleanCache(timeout) {
this.cache = {};
setTimeout(() => {
this.periodicCleanCache(timeout);
}, timeout);
}
}
const readerStorage = new ReaderStorage();
module.exports = readerStorage;

View File

@@ -1,3 +1,4 @@
const { spawn } = require('child_process');
const fs = require('fs-extra');
const crypto = require('crypto');
@@ -13,8 +14,48 @@ async function touchFile(filename) {
await fs.utimes(filename, Date.now()/1000, Date.now()/1000);
}
function spawnProcess(cmd, opts) {
let {args, killAfter, onData} = opts;
killAfter = (killAfter ? killAfter : 120*1000);
onData = (onData ? onData : () => {});
args = (args ? args : []);
return new Promise(async(resolve, reject) => {
let resolved = false;
const proc = spawn(cmd, args, {detached: true});
let stdout = '';
proc.stdout.on('data', (data) => {
stdout += data;
onData(data);
});
let stderr = '';
proc.stderr.on('data', (data) => {
stderr += data;
onData(data);
});
proc.on('close', (code) => {
resolved = true;
resolve({status: 'close', code, stdout, stderr});
});
proc.on('error', (error) => {
reject({status: 'error', error, stdout, stderr});
});
await sleep(killAfter);
if (!resolved) {
process.kill(proc.pid);
reject({status: 'killed', stdout, stderr});
}
});
}
module.exports = {
sleep,
randomHexString,
touchFile
touchFile,
spawnProcess
};

View File

@@ -0,0 +1,188 @@
const sqlite = require('sqlite');
const SQL = require('sql-template-strings');
const utils = require('../core/utils');
const waitingDelay = 100; //ms
class SqliteConnectionPool {
constructor() {
this.closed = true;
}
async open(connCount, dbFileName) {
if (!Number.isInteger(connCount) || connCount <= 0)
return;
this.connections = [];
this.taken = new Set();
this.freed = new Set();
for (let i = 0; i < connCount; i++) {
let client = await sqlite.open(dbFileName);
client.configure('busyTimeout', 10000); //ms
client.ret = () => {
this.taken.delete(i);
this.freed.add(i);
};
this.freed.add(i);
this.connections[i] = client;
}
this.closed = false;
}
_setImmediate() {
return new Promise((resolve) => {
setImmediate(() => {
return resolve();
});
});
}
async get() {
if (this.closed)
return;
let freeConnIndex = this.freed.values().next().value;
if (freeConnIndex == null) {
if (waitingDelay)
await utils.sleep(waitingDelay);
return await this._setImmediate().then(() => this.get());
}
this.freed.delete(freeConnIndex);
this.taken.add(freeConnIndex);
return this.connections[freeConnIndex];
}
async run(query) {
const dbh = await this.get();
try {
let result = await dbh.run(query);
dbh.ret();
return result;
} catch (e) {
dbh.ret();
throw e;
}
}
async all(query) {
const dbh = await this.get();
try {
let result = await dbh.all(query);
dbh.ret();
return result;
} catch (e) {
dbh.ret();
throw e;
}
}
async exec(query) {
const dbh = await this.get();
try {
let result = await dbh.exec(query);
dbh.ret();
return result;
} catch (e) {
dbh.ret();
throw e;
}
}
async close() {
for (let i = 0; i < this.connections.length; i++) {
await this.connections[i].close();
}
this.closed = true;
}
// Modified from node-sqlite/.../src/Database.js
async migrate(migs, table, force) {
const migrations = migs.sort((a, b) => Math.sign(a.id - b.id));
if (!migrations.length) {
throw new Error('No migration data');
}
migrations.map(migration => {
const data = migration.data;
const [up, down] = data.split(/^--\s+?down\b/mi);
if (!down) {
const message = `The ${migration.filename} file does not contain '-- Down' separator.`;
throw new Error(message);
} else {
/* eslint-disable no-param-reassign */
migration.up = up.replace(/^-- .*?$/gm, '').trim();// Remove comments
migration.down = down.trim(); // and trim whitespaces
}
});
// Create a database table for migrations meta data if it doesn't exist
await this.run(`CREATE TABLE IF NOT EXISTS "${table}" (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
up TEXT NOT NULL,
down TEXT NOT NULL
)`);
// Get the list of already applied migrations
let dbMigrations = await this.all(
`SELECT id, name, up, down FROM "${table}" ORDER BY id ASC`,
);
// Undo migrations that exist only in the database but not in migs,
// also undo the last migration if the `force` option was set to `last`.
const lastMigration = migrations[migrations.length - 1];
for (const migration of dbMigrations.slice().sort((a, b) => Math.sign(b.id - a.id))) {
if (!migrations.some(x => x.id === migration.id) ||
(force === 'last' && migration.id === lastMigration.id)) {
const dbh = await this.get();
await dbh.run('BEGIN');
try {
await dbh.exec(migration.down);
await dbh.run(SQL`DELETE FROM "`.append(table).append(SQL`" WHERE id = ${migration.id}`));
await dbh.run('COMMIT');
dbMigrations = dbMigrations.filter(x => x.id !== migration.id);
} catch (err) {
await dbh.run('ROLLBACK');
throw err;
} finally {
dbh.ret();
}
} else {
break;
}
}
// Apply pending migrations
let applied = [];
const lastMigrationId = dbMigrations.length ? dbMigrations[dbMigrations.length - 1].id : 0;
for (const migration of migrations) {
if (migration.id > lastMigrationId) {
const dbh = await this.get();
await dbh.run('BEGIN');
try {
await dbh.exec(migration.up);
await dbh.run(SQL`INSERT INTO "`.append(table).append(
SQL`" (id, name, up, down) VALUES (${migration.id}, ${migration.name}, ${migration.up}, ${migration.down})`)
);
await dbh.run('COMMIT');
applied.push(migration.id);
} catch (err) {
await dbh.run('ROLLBACK');
throw err;
} finally {
dbh.ret();
}
}
}
return applied;
}
}
module.exports = SqliteConnectionPool;

51
server/db/connManager.js Normal file
View File

@@ -0,0 +1,51 @@
const fs = require('fs-extra');
const SqliteConnectionPool = require('./SqliteConnectionPool');
const log = require('../core/getLogger').getLog();
const migrations = {
'app': require('./migrations/app'),
'readerStorage': require('./migrations/readerStorage'),
};
class ConnManager {
constructor() {
this._pool = {};
}
async init(config) {
this.config = config;
const force = null;//(config.branch == 'development' ? 'last' : null);
for (const poolConfig of this.config.db) {
const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
//бэкап
if (await fs.pathExists(dbFileName))
await fs.copy(dbFileName, `${dbFileName}.bak`);
const connPool = new SqliteConnectionPool();
await connPool.open(poolConfig.connCount, dbFileName);
log(`Opened database "${poolConfig.poolName}"`);
//миграции
const migs = migrations[poolConfig.poolName];
if (migs && migs.data.length) {
const applied = await connPool.migrate(migs.data, migs.table, force);
if (applied.length)
log(`${applied.length} migrations applied to "${poolConfig.poolName}"`);
}
this._pool[poolConfig.poolName] = connPool;
}
}
get pool() {
return this._pool;
}
}
const connManager = new ConnManager();
module.exports = connManager;

View File

@@ -0,0 +1,5 @@
module.exports = {
table: 'migration1',
data: [
]
}

View File

@@ -0,0 +1,7 @@
module.exports = `
-- Up
CREATE TABLE storage (id TEXT PRIMARY KEY, rev INTEGER, time INTEGER, data TEXT);
-- Down
DROP TABLE storage;
`;

View File

@@ -0,0 +1,6 @@
module.exports = {
table: 'migration1',
data: [
{id: 1, name: 'create', data: require('./001-create')}
]
}

View File

@@ -20,7 +20,7 @@ function webpackDevMiddleware(app) {
function logQueries(app) {
app.use(function(req, res, next) {
const start = Date.now();
log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body)}`);
log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body).substr(0, 2000)}`);
//log(`${JSON.stringify(req.headers, null, 2)}`)
res.once('finish', () => {
log(`${Date.now() - start}ms`);

View File

@@ -1,7 +1,7 @@
const config = require('./config');
const {initLogger, getLog} = require('./core/getLogger');
initLogger(config);
const log = getLog();
const logger = require('./core/getLogger');
logger.initLogger(config);
const log = logger.getLog();
const configSaver = require('./config/configSaver');
const argv = require('minimist')(process.argv.slice(2));
@@ -11,7 +11,7 @@ const path = require('path');
const express = require('express');
const compression = require('compression');
const SqliteConnectionPool = require('./core/SqliteConnectionPool');
const connManager = require('./db/connManager');
async function init() {
await fs.ensureDir(config.dataDir);
@@ -31,12 +31,11 @@ async function init() {
}
async function main() {
log(`${config.name} v${config.version}`);
log('Initializing');
await init();
log('Opening database');
const connPool = new SqliteConnectionPool(20, config);
await connPool.init();
await connManager.init(config);
//servers
for (let server of config.servers) {
@@ -52,7 +51,7 @@ async function main() {
}
app.use(compression({ level: 1 }));
app.use(express.json());
app.use(express.json({limit: '10mb'}));
if (devModule)
devModule.logQueries(app);
@@ -66,7 +65,7 @@ async function main() {
}
}));
require('./routes').initRoutes(app, connPool, serverConfig);
require('./routes').initRoutes(app, serverConfig);
if (devModule) {
devModule.logErrors(app);
@@ -89,7 +88,7 @@ async function main() {
try {
await main();
} catch (e) {
console.error(e.message);
console.error(e);
process.exit(1);
}
})();

View File

@@ -2,10 +2,10 @@ const c = require('./controllers');
const utils = require('./core/utils');
const multer = require('multer');
function initRoutes(app, connPool, config) {
const misc = new c.MiscController(connPool, config);
const reader = new c.ReaderController(connPool, config);
const worker = new c.WorkerController(connPool, config);
function initRoutes(app, config) {
const misc = new c.MiscController(config);
const reader = new c.ReaderController(config);
const worker = new c.WorkerController(config);
//access
const [aAll, aNormal, aSite, aReader, aOmnireader] = // eslint-disable-line no-unused-vars
@@ -26,6 +26,7 @@ function initRoutes(app, connPool, config) {
const routes = [
['POST', '/api/config', misc.getConfig.bind(misc), [aAll], {}],
['POST', '/api/reader/load-book', reader.loadBook.bind(reader), [aAll], {}],
['POST', '/api/reader/storage', reader.storage.bind(reader), [aAll], {}],
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
];