Compare commits

...

620 Commits

Author SHA1 Message Date
Book Pauk
8c9fd7678d Merge branch 'release/1.2.8' 2025-06-04 09:28:35 +07:00
Book Pauk
01313d66b2 Версия 1.2.8 2025-06-04 09:28:15 +07:00
Book Pauk
eaeacbfb1b Улучшено форматирование текста при копировании из окна 2025-06-04 09:23:13 +07:00
Book Pauk
5328998c21 Merge tag '1.2.7' into develop
1.2.7
2025-02-22 14:44:22 +07:00
Book Pauk
ee066c7c4b Merge branch 'release/1.2.7' 2025-02-22 14:44:18 +07:00
Book Pauk
130aebb514 Версия 1.2.7 2025-02-22 14:43:18 +07:00
Book Pauk
dbec1e630e Отключена форма для сбора донатов 2025-02-22 14:39:29 +07:00
Book Pauk
583b966616 Мелкая оптимизация, чтобы не отдавал большой конфиг каждый раз при обновлении страницы 2025-02-22 14:31:19 +07:00
Book Pauk
9e509ac845 Обновление caniuse-lite 2025-02-22 13:49:57 +07:00
Book Pauk
4ea2d8918e Merge tag '1.2.6' into develop
1.2.6
2024-10-03 15:43:48 +07:00
Book Pauk
6667688193 Merge branch 'release/1.2.6' 2024-10-03 15:43:44 +07:00
Book Pauk
30a1629f23 Исправления из-за нарушения авторских прав 2024-10-03 15:38:16 +07:00
Book Pauk
ba50faeebb Merge tag '1.2.5' into develop
1.2.5
2024-10-03 11:51:40 +07:00
Book Pauk
3c0d784e3d Merge branch 'release/1.2.5' 2024-10-03 11:51:36 +07:00
Book Pauk
3e75310e1f Исправления из-за нарушения авторских прав 2024-10-03 11:51:09 +07:00
Book Pauk
2b01d6d8d7 Merge tag '1.2.4' into develop
1.2.4
2024-08-27 12:59:44 +07:00
Book Pauk
be6d60d7a9 Merge branch 'release/1.2.4' 2024-08-27 12:59:41 +07:00
Book Pauk
3c0815d55b 1.2.4 2024-08-27 12:59:28 +07:00
Book Pauk
abd8584cb8 1.2.4 2024-08-27 12:59:20 +07:00
Book Pauk
5a910f80b3 Поправлена реакция на клик в строке статуса в режиме clickControl 2024-08-27 12:58:07 +07:00
Book Pauk
67bdfd853e Merge tag '1.2.3' into develop
1.2.3
2024-08-02 15:22:27 +07:00
Book Pauk
fc8e986acb Merge branch 'release/1.2.3' 2024-08-02 15:22:24 +07:00
Book Pauk
64539785c2 1.2.3 2024-08-02 15:22:07 +07:00
Book Pauk
f530455146 Версия 1.2.3 2024-08-02 15:21:43 +07:00
Book Pauk
70dc66e1ae Исправление мелких багов при прокрутке 2024-08-02 15:15:54 +07:00
Book Pauk
3e5894d9e0 Исправление багов 2024-07-31 11:44:07 +07:00
Book Pauk
d7ac9d1bfc Улучшение отображения примечаний 2024-07-31 11:30:31 +07:00
Book Pauk
5160c5fb75 Мелкая поправка текста 2024-07-30 21:29:02 +07:00
Book Pauk
d9c7964410 Поправки багов 2024-07-30 21:28:27 +07:00
Book Pauk
110952b4c4 К предыдущему 2024-07-30 18:41:21 +07:00
Book Pauk
ece17dc0dd Улучшение отображения сносок 2024-07-30 18:23:52 +07:00
Book Pauk
35e1087531 Merge tag '1.2.2' into develop
1.2.2
2024-07-28 20:22:59 +07:00
Book Pauk
59c4b62770 Merge branch 'release/1.2.2' 2024-07-28 20:22:54 +07:00
Book Pauk
4be9ce5ff3 Версия 1.2.2 2024-07-28 20:22:33 +07:00
Book Pauk
92a811cabd Поправки парсинга примечаний 2024-07-28 20:20:45 +07:00
Book Pauk
897cdc8ac7 Исправление парсинга примечаний 2024-07-28 20:13:35 +07:00
Book Pauk
418ff482ae Merge tag '1.2.1' into develop
1.2.1
2024-07-28 17:55:11 +07:00
Book Pauk
8858d6d1f2 Merge branch 'release/1.2.1' 2024-07-28 17:55:05 +07:00
Book Pauk
41f8a28631 Версия 1.2.1 2024-07-28 17:52:16 +07:00
Book Pauk
da0771d5e5 Мелкая поправка разметки 2024-07-28 17:47:15 +07:00
Book Pauk
c03995367a Поправки багов 2024-07-28 17:45:18 +07:00
Book Pauk
0430105061 Добавлено отображение примечаний на месте, по клику на примечании (#50) 2024-07-28 17:23:16 +07:00
Book Pauk
afd4d02dad Улучшение BUCServer 2024-07-26 17:19:45 +07:00
Book Pauk
d634ebf14c Улучшение BUCServer 2024-07-26 15:54:41 +07:00
Book Pauk
613230256a Небольшой тюнинг BUCServer 2024-07-26 00:49:39 +07:00
Book Pauk
2da1736c99 Поправка для игнорирования невалидных сертификатов 2024-07-25 18:10:13 +07:00
Book Pauk
1914092520 npx update-browserslist-db@latest 2024-07-25 16:51:44 +07:00
Book Pauk
4a6f93a14f edit 2024-03-25 13:02:13 +07:00
Book Pauk
9da8142078 Merge tag '1.2.0' into develop
1.2.0
2024-03-25 12:54:14 +07:00
Book Pauk
cafdb5b04b Merge branch 'release/1.2.0' 2024-03-25 12:54:05 +07:00
Book Pauk
697774978e Добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47) 2024-03-25 12:52:46 +07:00
Book Pauk
8c2c2fe2fc 1.2.0 2023-12-07 16:31:13 +07:00
Book Pauk
e3770463a1 В списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий 2023-12-07 16:26:30 +07:00
Book Pauk
d3ad23e9e4 Актуализация пакетов 2023-12-07 15:01:26 +07:00
Book Pauk
79d1e0b30d Merge tag '1.1.3' into develop
1.1.3
2023-02-06 19:48:02 +07:00
Book Pauk
1370bae4d6 Merge branch 'release/1.1.3' 2023-02-06 19:47:56 +07:00
Book Pauk
01fbdf38fa Версия 1.1.3 2023-02-06 19:47:28 +07:00
Book Pauk
be6b07a0cf Исправление бага при обнулении libs 2023-02-06 19:45:13 +07:00
Book Pauk
1b057029c8 Улучшено хранение ключа доступа 2023-02-05 16:04:52 +07:00
Book Pauk
b6b567f20b Улучшение парсинга невалидных fb2 2023-02-03 17:30:22 +07:00
Book Pauk
c4c109fe0e Мелкий рефакторинг 2023-02-03 16:28:24 +07:00
Book Pauk
4c8c921b03 Улучшения механизма запуска периодических задач 2023-02-03 16:23:13 +07:00
Book Pauk
69a2e5cda3 Merge tag '1.1.2-1' into develop
1.1.2-1
2023-01-25 17:06:39 +07:00
Book Pauk
c2adf8d5b8 Merge branch 'release/1.1.2-1' 2023-01-25 17:06:35 +07:00
Book Pauk
5c8d257923 Добавлены отладочные сообщения в журнал 2023-01-25 17:05:53 +07:00
Book Pauk
55dae33e60 "jembadb": "^5.1.7" 2023-01-25 15:46:09 +07:00
Book Pauk
57d8e9061f Merge tag '1.1.2' into develop
1.1.2
2023-01-22 20:56:12 +07:00
Book Pauk
4642679842 Merge branch 'release/1.1.2' 2023-01-22 20:56:08 +07:00
Book Pauk
ba18743fab Версия 1.1.2 2023-01-22 20:55:48 +07:00
Book Pauk
e739356733 Исправление бага - не открывалась ссылка по нажатию кнопки "Открыть" 2023-01-22 20:50:06 +07:00
Book Pauk
cae4aed8d2 Merge tag '1.1.1-1' into develop
1.1.1-1
2023-01-11 21:32:22 +07:00
Book Pauk
6c6a08d8e0 Merge branch 'release/1.1.1-1' 2023-01-11 21:32:13 +07:00
Book Pauk
deafbae945 Версия 1.1.1 2023-01-11 21:31:40 +07:00
Book Pauk
0b23c609f1 Merge tag '1.1.1' into develop
1.1.1
2023-01-11 21:30:53 +07:00
Book Pauk
0359061321 Merge branch 'release/1.1.1' 2023-01-11 21:30:45 +07:00
Book Pauk
bc7a5f6be4 Merge tag '1.1.0-1' into develop
1.1.0-1
2023-01-11 21:30:36 +07:00
Book Pauk
be36f8f6e8 Merge branch 'release/1.1.0-1' 2023-01-11 21:30:30 +07:00
Book Pauk
3b8d084c76 Доработки ночного режима 2023-01-11 21:29:35 +07:00
Book Pauk
ce1cdca6a0 Merge tag '1.1.0' into develop
1.1.0
2023-01-11 21:06:40 +07:00
Book Pauk
2f380dce1b Merge branch 'release/1.1.0' 2023-01-11 21:06:36 +07:00
Book Pauk
63b7bb24cf Версия 1.1.0 2023-01-11 21:06:08 +07:00
Book Pauk
2401ef8d16 Работа над ночным режимом 2023-01-11 20:53:19 +07:00
Book Pauk
62df3c0197 Работа над ночным режимом 2023-01-11 20:16:25 +07:00
Book Pauk
ba2dbca226 Работа над ночным режимом 2023-01-11 20:01:29 +07:00
Book Pauk
810b131b92 Работа над ночным режимом 2023-01-11 19:24:47 +07:00
Book Pauk
1d5bcde293 Работа над ночным режимом 2023-01-11 19:05:08 +07:00
Book Pauk
2fcf584e40 Вырезал ненужный код 2023-01-11 18:48:24 +07:00
Book Pauk
ecc6791892 Работа над ночным режимом 2023-01-11 18:44:54 +07:00
Book Pauk
8bf19c1e69 К предыдущему 2023-01-11 18:31:35 +07:00
Book Pauk
273ab4ae60 Работа над ночным режимом 2023-01-11 18:26:52 +07:00
Book Pauk
ec8fedc73d Работа над ночным режимом 2023-01-11 17:42:46 +07:00
Book Pauk
e6b1d4b032 Работа над ночным режимом 2023-01-11 14:46:27 +07:00
Book Pauk
a89572f85f Работа над ночным режимом 2023-01-11 14:33:16 +07:00
Book Pauk
bf4f5bc88b Работа над ночным режимом 2023-01-11 14:07:08 +07:00
Book Pauk
f4ce1f337e Работа над ночным режимом 2023-01-11 13:22:43 +07:00
Book Pauk
5e8afa15b2 Работа над ночным режимом 2023-01-11 12:57:40 +07:00
Book Pauk
7b1d0bb778 Работа над ночным режимом 2023-01-10 21:06:54 +07:00
Book Pauk
c0aec66f0f Работа над ночным режимом 2023-01-10 20:56:27 +07:00
Book Pauk
31481453f5 Работа над ночным режимом 2023-01-10 19:53:58 +07:00
Book Pauk
9724ec230c Работа над ночным режимом 2023-01-10 19:35:40 +07:00
Book Pauk
9e4be96522 Работа над ночным режимом 2023-01-08 20:08:03 +07:00
Book Pauk
91097515f2 Вырезал ненужный код 2023-01-08 18:08:17 +07:00
Book Pauk
230c3bb5b2 Начало работы над ночным режимом 2023-01-08 18:05:02 +07:00
Book Pauk
7a71db9de4 Поправил формирование заголовка 2023-01-08 15:22:36 +07:00
Book Pauk
7261afc428 Версия 1.0.1 2023-01-08 15:14:45 +07:00
Book Pauk
ddde7d038b Поправки редиректа при заданном url 2023-01-08 15:06:01 +07:00
Book Pauk
4d3d66fbe2 Merge tag '1.0.0' into develop
1.0.0
2022-12-18 15:17:20 +07:00
Book Pauk
b98a44def2 Merge branch 'release/1.0.0' 2022-12-18 15:17:09 +07:00
Book Pauk
c6e972b165 Поправки багов 2022-12-18 14:53:58 +07:00
Book Pauk
7b7146b502 Поправки дефолтных сетевых библиотек для режима omnireader 2022-12-18 14:35:40 +07:00
Book Pauk
f00700cb41 Поправка конфигов nginx 2022-12-18 13:30:47 +07:00
Book Pauk
c3e099f095 Поправка бага 2022-12-18 13:22:55 +07:00
Book Pauk
6393c24575 Поправка README 2022-12-18 13:21:25 +07:00
Book Pauk
17378f3686 Поправки конфигов nginx 2022-12-18 13:04:45 +07:00
Book Pauk
d7453302f7 versionHistory 2022-12-18 13:04:25 +07:00
Book Pauk
07f5146534 Небольшие улучшения UI 2022-12-17 20:49:39 +07:00
Book Pauk
d04851af72 versionHistory 2022-12-17 20:39:09 +07:00
Book Pauk
6aff0eb4e6 Улучшение формы доната 2022-12-17 20:28:16 +07:00
Book Pauk
2f5409b485 Актуализация пакетов 2022-12-16 21:22:47 +07:00
Book Pauk
3aa7dc32d3 Мелкая поправка 2022-12-16 21:03:22 +07:00
Book Pauk
f5cd6ebdbc Добавлена настройка "Многострочная панель" для размещения кнопок в
несколько рядов на тулбаре
2022-12-16 21:02:07 +07:00
Book Pauk
a7289cda74 Поправки процедуры скроллинга 2022-12-16 20:04:20 +07:00
Book Pauk
ada3a3b4fd Рефакторинг 2022-12-16 19:41:20 +07:00
Book Pauk
a21e216eb9 Поправка бага 2022-12-16 19:34:56 +07:00
Book Pauk
b85fe7f219 Поправки webkit-scrollbar 2022-12-16 19:26:30 +07:00
Book Pauk
4efb3031de Удаление более ненужного кода 2022-12-16 19:07:47 +07:00
Book Pauk
6b66acb2cf Поправил справку 2022-12-16 18:22:48 +07:00
Book Pauk
481e1e840e Добавлен переход в полноэкраннй режим по двойному тапу в середину экрана 2022-12-16 18:14:08 +07:00
Book Pauk
e296b49821 Поправки положения всплывающих сообщений 2022-12-16 15:38:38 +07:00
Book Pauk
254118f845 Мелкий рефакторинг 2022-12-16 14:44:16 +07:00
Book Pauk
88f5a98c55 Решение проблемы откатов страницы чтения при нестабильной связи во время синхронизации 2022-12-15 20:05:03 +07:00
Book Pauk
572a5dd200 Поправка положения кнопок панели 2022-12-15 18:19:04 +07:00
Book Pauk
8dce00db44 Поправки отображения кнопок панели 2022-12-15 18:00:05 +07:00
Book Pauk
0ab73deffd Исправление бага offlineModeActive 2022-12-15 17:53:22 +07:00
Book Pauk
9863dc6dd0 Поправка комментария 2022-12-15 17:52:40 +07:00
Book Pauk
797f93d467 Убрал дебаг 2022-12-15 17:36:24 +07:00
Book Pauk
c602f3d531 Переход на dayjs 2022-12-15 17:18:05 +07:00
Book Pauk
dfd45a58bd "dayjs": "^1.11.7" 2022-12-15 17:17:46 +07:00
Book Pauk
70a832530e versionHistory 2022-12-15 16:48:51 +07:00
Book Pauk
4fc32eafd7 Удалены неиспользуемые компоненты 2022-12-15 16:46:20 +07:00
Book Pauk
6579d34b90 Переименование режима "liberama.top" в "liberama" 2022-12-15 16:32:52 +07:00
Book Pauk
a5bf8f88cd Добавлен раздел "Сетевая библиотека" в режим "omnireader" 2022-12-15 16:31:42 +07:00
Book Pauk
55264314b8 Обработка ошибок 2022-12-14 20:07:45 +07:00
Book Pauk
23a9e9154b Доработки Logger 2022-12-14 20:06:56 +07:00
Book Pauk
0ee373c1f3 Убрал лишний код 2022-12-14 19:22:13 +07:00
Book Pauk
29b40bc91d Начата работа над 1.0.0 2022-12-14 19:07:36 +07:00
Book Pauk
10b7363b06 Поправки docs в соответствии с новыми изменениями 2022-12-14 18:56:48 +07:00
Book Pauk
e37f15975d Поправки README 2022-12-12 19:13:41 +07:00
Book Pauk
ce0f61c543 Поправки README 2022-12-12 19:11:19 +07:00
Book Pauk
ea62abfc9a Добавлен createWebApp 2022-12-12 18:10:18 +07:00
Book Pauk
15a2b6ba7e Обновление node_stream_zip_changed с моими изменениями 2022-12-12 18:06:20 +07:00
Book Pauk
10773526e4 Добавлена сборка релизов, ipfs удален 2022-12-12 16:24:19 +07:00
Book Pauk
facd7f1414 Поправка production-конфига 2022-12-12 16:04:57 +07:00
Book Pauk
29bf80108d Переход на WebSocket, поправки багов 2022-12-12 16:03:41 +07:00
Book Pauk
00bbb56ec6 README.md 2022-12-12 16:03:34 +07:00
Book Pauk
2e057f5c96 Поправки сборки webpack 2022-12-12 15:26:14 +07:00
Book Pauk
936fa6a172 gitignore 2022-12-11 18:34:16 +07:00
Book Pauk
5d5ad40f4e Выделение файлов приложения в рабочую директорию 2022-12-11 18:30:33 +07:00
Book Pauk
55ee303fc5 Убрал sharedDir 2022-12-11 16:32:54 +07:00
Book Pauk
f30f11ce2d Поправки FileDownloader 2022-12-11 15:38:51 +07:00
Book Pauk
f5e57b3319 Консольный лог включен всегда 2022-12-11 14:30:30 +07:00
Book Pauk
d5fe4f8eb4 Улучшение AsyncExit 2022-12-11 14:29:30 +07:00
Book Pauk
4f4f226d8c Поддержка наследования классов 2022-12-11 14:22:39 +07:00
Book Pauk
5b7712c274 Актуализация пакетов 2022-12-11 14:12:42 +07:00
Book Pauk
8da71a98da Небольшие поправки 2022-11-01 00:54:07 +07:00
Book Pauk
f9fc59718a Изменения для встраивания inpx-web в сетевую библиотеку 2022-10-14 20:12:36 +07:00
Book Pauk
9bc4c3201c Добавлен location.reload для фрейма 2022-10-14 17:20:48 +07:00
Book Pauk
eb4ea0cc9c Удалил лишние файлы 2022-10-14 15:49:13 +07:00
Book Pauk
4b2e63bb5b Улучшение NumInput 2022-10-13 21:19:18 +07:00
Book Pauk
817f018d4d Убрал лишнее 2022-10-13 21:13:37 +07:00
Book Pauk
9160b4ef90 Глобальный рефакторинг SettingsPage (закончено) 2022-10-13 21:12:56 +07:00
Book Pauk
e8d1817566 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-13 20:29:19 +07:00
Book Pauk
419b203fcf Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 21:45:40 +07:00
Book Pauk
528b32ccf7 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 20:24:08 +07:00
Book Pauk
bc0c9932c8 Поправлен баг 2022-10-12 19:21:48 +07:00
Book Pauk
5827d7a246 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 18:02:32 +07:00
Book Pauk
5dd08c43a6 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:51:48 +07:00
Book Pauk
13c5fc244a Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:37:58 +07:00
Book Pauk
b8b52fe662 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:13:34 +07:00
Book Pauk
f4c0a48868 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:13:21 +07:00
Book Pauk
78b98e77c6 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 16:57:30 +07:00
Book Pauk
8cbaf60755 К предыдущему 2022-10-12 16:30:27 +07:00
Book Pauk
62ac60887e Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 16:25:58 +07:00
Book Pauk
fe6243e889 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 15:58:17 +07:00
Book Pauk
8abd8ecaab Глобальный рефакторинг SettingsPage (начало), избавление от includer 2022-10-12 15:18:23 +07:00
Book Pauk
c860422a5a Merge tag '0.12.2-3' into develop
0.12.2-3
2022-10-05 17:59:15 +07:00
Book Pauk
083151460a Merge branch 'release/0.12.2-3' 2022-10-05 17:59:10 +07:00
Book Pauk
c8f97ef386 Решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом 2022-10-05 17:58:39 +07:00
Book Pauk
c9a22a5eaf Merge tag '0.12.2-2' into develop
0.12.2-2
2022-10-05 15:16:29 +07:00
Book Pauk
f926732070 Merge branch 'release/0.12.2-2' 2022-10-05 15:16:24 +07:00
Book Pauk
3fbe6e9d9b Улучшение обработки ошибок 2022-10-05 15:15:26 +07:00
Book Pauk
225230381f Добавлена чистка output перед сборкой 2022-10-01 13:39:02 +07:00
Book Pauk
b58d3a1b8b Поправки параметров CopyWebpackPlugin 2022-09-20 20:21:41 +07:00
Book Pauk
ffedce4351 Поправки обработки ошибок сервера 2022-09-12 15:23:22 +07:00
Book Pauk
a4fdb67913 Merge tag '0.12.2-1' into develop
0.12.2-1
2022-09-04 21:44:06 +07:00
Book Pauk
6ba46421b9 Merge branch 'release/0.12.2-1' 2022-09-04 21:43:54 +07:00
Book Pauk
d201961046 Поправка положения notify-сообщений 2022-09-04 21:42:50 +07:00
Book Pauk
614a7f9da7 Merge tag '0.12.2' into develop
0.12.2
2022-09-04 21:22:39 +07:00
Book Pauk
113ab3e596 Merge branch 'release/0.12.2' 2022-09-04 21:22:28 +07:00
Book Pauk
c95870bfe5 Добавлено сохранение во vuex настройки offlineModeActive 2022-09-04 21:20:21 +07:00
Book Pauk
e69e9335f9 Исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц 2022-09-04 21:19:30 +07:00
Book Pauk
fd21cd77dd Node 16 2022-09-01 21:13:31 +07:00
Book Pauk
d1880acaf9 Merge tag '0.12.1' into develop
0.12.1
2022-09-01 21:10:57 +07:00
Book Pauk
428b507257 Merge branch 'release/0.12.1' 2022-09-01 21:10:52 +07:00
Book Pauk
043dab0731 Версия 0.12.1 2022-09-01 21:08:56 +07:00
Book Pauk
a7b4d9c0d8 Добавлена форма доната 2022-09-01 21:05:22 +07:00
Book Pauk
6f9c95e351 Переход на node 16, актуализация пакетов 2022-09-01 15:36:28 +07:00
Book Pauk
7a53063ea8 Исправление багов 2022-09-01 15:31:16 +07:00
Book Pauk
ec4d5cac4f Поправлен баг 2022-08-16 23:40:40 +07:00
Book Pauk
f8557cba88 Исправление багов 2022-08-05 02:25:45 +07:00
Book Pauk
5dead039f5 Дебаг 2022-08-05 01:09:47 +07:00
Book Pauk
ea38392df4 Дебаг 2022-08-05 00:57:18 +07:00
Book Pauk
0cc9d90a94 Поправлен мелкий баг 2022-08-05 00:31:56 +07:00
Book Pauk
8c7b86c458 Поправлен баг 2022-08-05 00:16:54 +07:00
Book Pauk
0e29546fc5 Добавлены таймауты 2022-08-04 23:53:46 +07:00
Book Pauk
c9fa90d07c Поправлен donate-адрес 2022-08-04 15:08:43 +07:00
Book Pauk
7d8e0525b1 Активировал DonateHelpPage 2022-08-04 15:03:48 +07:00
Book Pauk
ddf69876a6 Добавлено сообщение при изменении чекбокса проверки обновления 2022-08-04 13:23:32 +07:00
Book Pauk
1d78e75e38 Merge tag '0.12.0-2' into develop
0.12.0-2
2022-08-03 15:58:49 +07:00
Book Pauk
7ed58fe3c6 Merge branch 'release/0.12.0-2' 2022-08-03 15:58:42 +07:00
Book Pauk
058c79570b Поправки багов 2022-08-03 15:52:48 +07:00
Book Pauk
ec8fbcdf38 Исправление багов 2022-08-03 15:34:24 +07:00
Book Pauk
76673295bf Добавлена автоотмена проверки обновлений книг по истечении заданного количества дней 2022-08-03 14:57:01 +07:00
Book Pauk
084401b9c3 Мелкие поправки 2022-08-03 14:53:58 +07:00
Book Pauk
49038b10f7 Улучшение обработки ошибок 2022-07-29 17:45:33 +07:00
Book Pauk
45ea26810a Улучшение fillCheckQueue 2022-07-28 20:22:38 +07:00
Book Pauk
18c8b2d803 Мелкие поправки 2022-07-28 18:50:56 +07:00
Book Pauk
f4a7482b3b Улучшение парсинга head-запроса 2022-07-28 18:38:49 +07:00
Book Pauk
32dff128f4 Улучшение парсинга head-запроса 2022-07-28 18:04:47 +07:00
Book Pauk
a00b2d6574 Исправлен баг 2022-07-27 23:29:52 +07:00
Book Pauk
10c6e7d522 Merge tag '0.12.0-1' into develop
0.12.0-1
2022-07-27 21:33:56 +07:00
Book Pauk
df6a256d51 Merge branch 'release/0.12.0-1' 2022-07-27 21:33:49 +07:00
Book Pauk
fbdb74ee68 Поправка текста 2022-07-27 21:33:22 +07:00
Book Pauk
9ad7250da0 Merge tag '0.12.0' into develop
0.12.0
2022-07-27 21:10:04 +07:00
Book Pauk
8c86984ea1 Merge branch 'release/0.12.0' 2022-07-27 21:09:59 +07:00
Book Pauk
834b3f6210 Версия 0.12.0 2022-07-27 21:09:42 +07:00
Book Pauk
105b8d5042 Мелкие поправки 2022-07-27 21:02:26 +07:00
Book Pauk
7ca8fd9ca1 Доработки отправки bookUrls 2022-07-27 20:50:39 +07:00
Book Pauk
0067c2800a Дебаг 2022-07-27 20:37:56 +07:00
Book Pauk
688c8796f4 Поправлен баг 2022-07-27 19:00:25 +07:00
Book Pauk
56af65742b Улучшение настроек для BookUpdateChecker 2022-07-27 18:49:51 +07:00
Book Pauk
629ad26d40 Доработки BookUpdateChecker 2022-07-27 17:55:29 +07:00
Book Pauk
4b0e499c10 Работа над BookUpdateChecker 2022-07-27 17:28:02 +07:00
Book Pauk
4697b46cba Работа над BookUpdateChecker 2022-07-27 16:50:24 +07:00
Book Pauk
7f17e7daed Работа над BookUpdateChecker 2022-07-27 15:40:46 +07:00
Book Pauk
a1fcb7597b Работа над BookUpdateChecker 2022-07-27 14:08:59 +07:00
Book Pauk
35e46d0685 Работа над BookUpdateChecker 2022-07-27 12:44:10 +07:00
Book Pauk
e2c0f3658b Улучшения ServerStorage 2022-07-27 11:42:39 +07:00
Book Pauk
a3541ec16a Работа над BookUpdateChecker 2022-07-26 20:37:49 +07:00
Book Pauk
08d0d3e7f3 Работа над BookUpdateChecker 2022-07-26 20:12:44 +07:00
Book Pauk
2c47b2bee3 Работа над BookUpdateChecker 2022-07-26 18:43:42 +07:00
Book Pauk
e6008b5ec4 Работа над BookUpdateChecker 2022-07-26 17:30:34 +07:00
Book Pauk
e214ddf8d5 Работа над BookUpdateChecker 2022-07-26 00:41:07 +07:00
Book Pauk
52927c6188 Работа над BookUpdateChecker 2022-07-26 00:11:15 +07:00
Book Pauk
92ca9dd983 Работа над BookUpdateChecker 2022-07-25 23:27:38 +07:00
Book Pauk
ed8be34c12 Работа над BookUpdateChecker 2022-07-25 17:52:57 +07:00
Book Pauk
93bddfd05e Переход на vuex-persist вместо vuex-persistedstate 2022-07-25 17:03:29 +07:00
Book Pauk
8c99101bb3 Обновление пакетов 2022-07-25 16:41:07 +07:00
Book Pauk
d874f9ded4 Актуализация пакетов 2022-07-25 16:30:38 +07:00
Book Pauk
d7be4d3d94 Окончательное избавление от sqlite в пользу jembadb 2022-07-25 16:12:15 +07:00
Book Pauk
a2fa312839 Merge tag '0.11.8-7' into develop
0.11.8-7
2022-07-19 00:52:43 +07:00
Book Pauk
f7e1e09928 Merge branch 'release/0.11.8-7' 2022-07-19 00:52:36 +07:00
Book Pauk
f0832b07cb Исправление привнесенного бага 2022-07-19 00:50:44 +07:00
Book Pauk
7c253df291 Merge tag '0.11.8-6' into develop
0.11.8-6
2022-07-19 00:36:00 +07:00
Book Pauk
bb7cd9cbde Merge branch 'release/0.11.8-6' 2022-07-19 00:35:55 +07:00
Book Pauk
56c4182985 Небольшой тюнинг 2022-07-19 00:35:12 +07:00
Book Pauk
cb6c7536bf Небольшой тюнинг 2022-07-19 00:32:52 +07:00
Book Pauk
fbfe8cbda0 Решение проблемы невалидного tls-сертификата 2022-07-19 00:27:54 +07:00
Book Pauk
6129d2d7eb Небольшие поправки 2022-07-19 00:14:18 +07:00
Book Pauk
16b30c922a Улучшение работы с удаленным хранилищем 2022-07-18 23:54:25 +07:00
Book Pauk
c42ad66be6 Merge tag '0.11.8-5' into develop
0.11.8-5
2022-07-17 21:15:37 +07:00
Book Pauk
f36c13fea1 Merge branch 'release/0.11.8-5' 2022-07-17 21:15:31 +07:00
Book Pauk
4fd9d579e0 Небольшие доработки remoteSent, оптимизация отправки файлов 2022-07-17 21:10:52 +07:00
Book Pauk
e65a8a13ea Рефакторинг 2022-07-17 20:04:23 +07:00
Book Pauk
6ddb97d43e Тюнинг таймаутов 2022-07-17 17:11:34 +07:00
Book Pauk
89082603de Merge tag '0.11.8-4' into develop
0.11.8-4
2022-07-17 16:54:15 +07:00
Book Pauk
a9a3227433 Merge branch 'release/0.11.8-4' 2022-07-17 16:53:59 +07:00
Book Pauk
60cb3514b2 Тюнинг таймаутов 2022-07-17 16:53:12 +07:00
Book Pauk
4aeaa05f0b Merge tag '0.11.8-3' into develop
0.11.8-3
2022-07-17 15:58:34 +07:00
Book Pauk
9c06552278 Merge branch 'release/0.11.8-3' 2022-07-17 15:58:28 +07:00
Book Pauk
000f8dde82 Переход на RemoteStorage 2022-07-17 15:43:12 +07:00
Book Pauk
9ffc218002 Поправка 2022-07-16 21:36:50 +07:00
Book Pauk
68a188f099 Конфиг nginx 2022-07-16 21:10:33 +07:00
Book Pauk
8829bb3810 Конфиг nginx 2022-07-16 21:07:16 +07:00
Book Pauk
5164d2f536 Merge tag '0.11.8-2' into develop
0.11.8-2
2022-07-16 21:02:05 +07:00
Book Pauk
451538fcf7 Merge branch 'release/0.11.8-2' 2022-07-16 21:01:56 +07:00
Book Pauk
82a02ef339 Удаление более ненужной функциональности 2022-07-16 20:48:50 +07:00
Book Pauk
b834d4951f Обработка ошибок 2022-07-16 20:40:21 +07:00
Book Pauk
edc3b669be Добавлено восстановление файлов из webdav 2022-07-16 20:35:34 +07:00
Book Pauk
522826311d Переделка механизма чистки папок и отправки через RemoteWebDavStorage 2022-07-16 20:24:37 +07:00
Book Pauk
e69b9951d5 Отключил проверку валидности tls-сертификата 2022-07-16 18:43:09 +07:00
Book Pauk
c6300222ea Мелкий рефакторинг 2022-07-16 17:54:27 +07:00
Book Pauk
5aa6ee899c Изменение механизма работы с /tmp и /upload (начало) 2022-07-16 17:35:32 +07:00
Book Pauk
4b76f97d2b Поправки конфигов nginx 2022-07-16 15:45:52 +07:00
Book Pauk
5ccfe71c55 Начало работы над BookUpdateChecker 2022-07-16 13:16:57 +07:00
Book Pauk
97fc902cdb Поправлен баг 2022-07-15 23:53:54 +07:00
Book Pauk
7e935951d7 Поправка разметки 2022-07-15 23:17:30 +07:00
Book Pauk
810c6d68d2 Поправка разметки 2022-07-15 23:14:09 +07:00
Book Pauk
003dc70f4f Merge tag '0.11.8-1' into develop
0.11.8-1
2022-07-15 18:14:12 +07:00
Book Pauk
371ff64a95 Merge branch 'release/0.11.8-1' 2022-07-15 18:14:06 +07:00
Book Pauk
b0de5adbf3 Добавлена возможность скачивать обои 2022-07-15 18:11:24 +07:00
Book Pauk
d1d2b07c33 Поправки разметки 2022-07-15 17:42:19 +07:00
Book Pauk
d9b2444c1a Улучшен механизм загрузки обложек 2022-07-15 17:36:49 +07:00
Book Pauk
e7fae27031 Убрал отладку 2022-07-15 17:17:00 +07:00
Book Pauk
eb0c7b0a32 Отладка 2022-07-15 17:11:58 +07:00
Book Pauk
3d7ad0dd9a Небюольшие оптимизации загрузки обложек 2022-07-15 17:05:17 +07:00
Book Pauk
ae04feb311 Merge tag '0.11.8' into develop
0.11.8
2022-07-15 02:11:03 +07:00
Book Pauk
7b59f911ef Merge branch 'release/0.11.8' 2022-07-15 02:10:58 +07:00
Book Pauk
d3444da647 Поправки разметки 2022-07-15 01:58:42 +07:00
Book Pauk
66738d0c9c К предыдущему 2022-07-15 01:51:28 +07:00
Book Pauk
7e187acd68 Версия 0.11.8 2022-07-15 01:50:17 +07:00
Book Pauk
c751372a54 Добавлен resizeImage 2022-07-15 01:38:25 +07:00
Book Pauk
7fc98fc7da Добавление отображения обложки (coverpage) в окне загруженных файлов 2022-07-15 00:47:24 +07:00
Book Pauk
b56f45694e Добавлен coversStorage для хранения coverpage 2022-07-15 00:45:56 +07:00
Book Pauk
091ca521ef Новые upload-методы 2022-07-15 00:45:09 +07:00
Book Pauk
c7a17b0a76 Добавлена синхронизация файлов обоев 2022-07-14 20:14:40 +07:00
Book Pauk
26468b996a Мелкая поправка 2022-07-14 20:12:37 +07:00
Book Pauk
c4e240d87c Увеличил maxPayloadSize 2022-07-14 20:11:17 +07:00
Book Pauk
04713f47c8 Небольшие поправки 2022-07-14 16:14:25 +07:00
Book Pauk
37ab3493db Merge tag '0.11.7-6' into develop
0.11.7-6
2022-07-14 03:52:50 +07:00
Book Pauk
a4cb3c628e Merge branch 'release/0.11.7-6' 2022-07-14 03:52:44 +07:00
Book Pauk
8492da8a13 Небольшое улучшение 2022-07-14 03:51:59 +07:00
Book Pauk
98d7c64a56 Исправление багов 2022-07-14 03:34:55 +07:00
Book Pauk
25f121e5ed Merge tag '0.11.7-5' into develop
0.11.7-5
2022-07-14 01:57:36 +07:00
Book Pauk
4c8797c99c Merge branch 'release/0.11.7-5' 2022-07-14 01:57:30 +07:00
Book Pauk
1155aa285d Лишние пробелы 2022-07-14 01:57:03 +07:00
Book Pauk
239bbb8263 Добавлено восстановление из архива 2022-07-14 01:55:09 +07:00
Book Pauk
e6b9330108 Добавление работы с архивом 2022-07-14 01:17:09 +07:00
Book Pauk
935b767c2e Поправил поведение buttonActiveClass 2022-07-14 00:31:24 +07:00
Book Pauk
8acf3295b5 Поправил разметку 2022-07-14 00:31:09 +07:00
Book Pauk
48c3a12fa0 Улучшение парсинга плохих fb2 2022-07-14 00:30:27 +07:00
Book Pauk
a1dea514b7 Поправка разметки 2022-07-13 23:47:55 +07:00
Book Pauk
d4788439cb Merge tag '0.11.7-4' into develop
0.11.7-4
2022-07-13 16:38:10 +07:00
Book Pauk
0a60ad354c Merge branch 'release/0.11.7-4' 2022-07-13 16:38:04 +07:00
Book Pauk
c565a20344 Поправки разметки 2022-07-13 16:37:47 +07:00
Book Pauk
735ee88f0b Merge tag '0.11.7-3' into develop
0.11.7-3
2022-07-13 16:34:22 +07:00
Book Pauk
9405ce2cc0 Merge branch 'release/0.11.7-3' 2022-07-13 16:34:16 +07:00
Book Pauk
115277d88a Поправки разметки 2022-07-13 16:34:00 +07:00
Book Pauk
6925c11dbd Merge tag '0.11.7-2' into develop
0.11.7-2
2022-07-13 16:25:11 +07:00
Book Pauk
984d835892 Merge branch 'release/0.11.7-2' 2022-07-13 16:25:05 +07:00
Book Pauk
23353a4960 Улучшен парсинг fb2 2022-07-13 16:23:52 +07:00
Book Pauk
955bcda032 Поправки разметки 2022-07-13 15:01:35 +07:00
Book Pauk
81ad5d7a2c Поправки разметки 2022-07-13 14:47:24 +07:00
Book Pauk
dada7980ec Merge tag '0.11.7-1' into develop
0.11.7-1
2022-07-12 19:23:38 +07:00
Book Pauk
511a308646 Merge branch 'release/0.11.7-1' 2022-07-12 19:23:33 +07:00
Book Pauk
65c8f2cc81 Небольшие поправки на панели, изменена нумерация на обратную 2022-07-12 19:21:26 +07:00
Book Pauk
238c18bc48 Merge tag '0.11.7' into develop
0.11.7
2022-07-12 19:08:35 +07:00
Book Pauk
873a08fee1 Merge branch 'release/0.11.7' 2022-07-12 19:08:27 +07:00
Book Pauk
7e89228803 Версия 0.11.7 2022-07-12 19:07:39 +07:00
Book Pauk
fc630923a4 Настройка методов сортировки 2022-07-12 18:50:35 +07:00
Book Pauk
928f911d03 Добавлены подсказки к кнопкам 2022-07-12 17:53:14 +07:00
Book Pauk
7ffcd3fe1b Поправки поведения при скроллинге 2022-07-12 17:33:03 +07:00
Book Pauk
0efbaf643a Поправил сообщение об ошибке 2022-07-12 17:32:19 +07:00
Book Pauk
f1bf8e54ae Добавлен метод scrollToActiveBook 2022-07-12 17:10:50 +07:00
Book Pauk
b4aa6ab6c8 Поправки поиска 2022-07-12 16:58:34 +07:00
Book Pauk
72431f0202 Работа над группировкой 2022-07-12 16:51:32 +07:00
Book Pauk
04a326c0e4 Работа над группировкой 2022-07-12 15:51:43 +07:00
Book Pauk
931966f4f3 Поправки разметки 2022-07-12 15:05:17 +07:00
Book Pauk
8808cc4779 Работа над группировкой по файлам 2022-07-12 14:46:34 +07:00
Book Pauk
988c959eba Работа над группировкой файлов 2022-07-12 04:05:51 +07:00
Book Pauk
c0b658d9e6 К предыдущему 2022-07-12 01:41:18 +07:00
Book Pauk
3190246f34 Улучшена реакция на onResize 2022-07-12 01:35:19 +07:00
Book Pauk
d957b4a5f9 Добавлена возможность автосокрытия панели при прокрутке 2022-07-12 01:03:44 +07:00
Book Pauk
bef9e5705c Поправки текстовых строк 2022-07-11 23:53:54 +07:00
Book Pauk
eb2affa518 Приведение input к единому стилю 2022-07-11 23:50:51 +07:00
Book Pauk
07b9a3c033 Мелкие правки 2022-07-11 22:28:48 +07:00
Book Pauk
3ca14ae06a Работа над группировкой 2022-07-11 22:26:34 +07:00
Book Pauk
7caa0c2112 Начало добавления группировки в RecentBooksPage 2022-07-11 20:11:38 +07:00
Book Pauk
9c69f5bc01 Поправил размер иконки 2022-07-11 20:10:51 +07:00
Book Pauk
125a2e0f17 Исправление багов 2022-07-11 17:12:17 +07:00
Book Pauk
1b4360b897 Дополнение в convertRecent 2022-07-11 16:26:03 +07:00
Book Pauk
4775d6e47b Поправлен баг 2022-07-10 20:07:33 +07:00
Book Pauk
33fc553c55 Добавлен запрос на объединение позиций при
обнаружении похожего файла в загруженных
2022-07-10 19:54:00 +07:00
Book Pauk
25cad81c50 Улучшение отображения загруженных 2022-07-10 19:53:30 +07:00
Book Pauk
02a2099c1f Поправлен z-index 2022-07-10 19:52:58 +07:00
Book Pauk
1cda186b1a Добавлен диалог askYesNo 2022-07-10 19:52:29 +07:00
Book Pauk
f10291b6c6 Поправка названия действия 2022-07-10 19:51:31 +07:00
Book Pauk
26ab5d6765 Рефакторинг 2022-07-10 18:27:05 +07:00
Book Pauk
5edeed0747 Изменение механизма хранения книг 2022-07-10 17:31:21 +07:00
Book Pauk
c878ce432f Небольшое исправление опознававния кодировки 2022-07-10 17:20:47 +07:00
Book Pauk
81798897c8 Изменения в механизме хранения книг:
теперь ориентируемся на "ключ-filepath", а не "ключ-url"
2022-07-10 16:38:54 +07:00
Book Pauk
63840fadbc К предыдущему 2022-07-10 14:59:39 +07:00
Book Pauk
36aa057035 Поправка цвета 2022-07-09 21:00:09 +07:00
Book Pauk
30afd2421c Рефакторинг 2022-07-09 20:50:31 +07:00
Book Pauk
53a1d90bd8 Улучшение поведения при очереди загрузки книг 2022-07-09 02:01:14 +07:00
Book Pauk
2ecf6beef2 Небольшой багфикс 2022-07-09 01:56:42 +07:00
Book Pauk
85910a20e9 Улучшение ContentsPage 2022-07-08 20:50:55 +07:00
Book Pauk
66cf7790b3 Улучшения ContentsPage 2022-07-08 19:09:57 +07:00
Book Pauk
4a9eb7e4bb Удалил устаревшее 2022-07-08 14:30:44 +07:00
Book Pauk
07446696c1 Поправлен цвет заголовка 2022-07-08 13:52:45 +07:00
Book Pauk
a29f9d9a4b Унификация размеров окон 2022-07-08 13:43:59 +07:00
Book Pauk
d49c9baec3 Унификация интерфейса 2022-07-08 13:34:53 +07:00
Book Pauk
8c9d4a12ee Настройка цветов 2022-07-08 13:24:13 +07:00
Book Pauk
fce69e4657 Настройка цветов 2022-07-08 13:21:42 +07:00
Book Pauk
b387509f88 Добавил блокировку при загрузке книг, теперь загружаются последовательно 2022-07-08 12:26:47 +07:00
Book Pauk
8dc8bdc0d6 Merge tag '0.11.6-2' into develop
0.11.6-2
2022-07-07 19:43:47 +07:00
Book Pauk
00caae8363 Merge branch 'release/0.11.6-2' 2022-07-07 19:43:40 +07:00
Book Pauk
2ead8570a7 Небольшая поправка 2022-07-07 19:39:02 +07:00
Book Pauk
408315466b Частичный откат предыдущих изменений 2022-07-07 19:38:17 +07:00
Book Pauk
c651836554 Поправки скриптов запуска 2022-07-07 19:33:32 +07:00
Book Pauk
03a1e70fce Поправки, чтобы не падал в случае детача скрина 2022-07-07 19:05:54 +07:00
Book Pauk
ab5a11a24f Убрал сайт flibs.in из сетевых библиотек 2022-07-07 17:42:05 +07:00
Book Pauk
8cd6ed472c Изменил client_max_body_size 100m 2022-07-07 17:37:25 +07:00
Book Pauk
055181b744 Исправлен баг выпадающих списков в оглавлении 2022-07-07 17:34:03 +07:00
Book Pauk
e331a3920b Актуализация пакетов 2022-07-07 17:29:47 +07:00
Book Pauk
c62bccb470 Улучшил журналирование ошибок БД 2022-07-07 16:24:59 +07:00
Book Pauk
ea351ea293 Merge tag '0.11.6-1' into develop
0.11.6-1
2022-07-04 12:23:55 +07:00
Book Pauk
d806a07c60 Merge branch 'release/0.11.6-1' 2022-07-04 12:23:48 +07:00
Book Pauk
c0ea096f1f Обновил jembadb 2022-07-04 12:22:27 +07:00
Book Pauk
011d4a1672 Merge tag '0.11.6' into develop
0.11.6
2022-07-02 17:41:42 +07:00
Book Pauk
4836a737c6 Merge branch 'release/0.11.6' 2022-07-02 17:41:34 +07:00
Book Pauk
5712b2ee17 Версия 0.11.6 2022-07-02 17:40:28 +07:00
Book Pauk
32dd17694e Улучшено копирование текстов со страницы 2022-07-02 17:36:12 +07:00
Book Pauk
3ebc932a6a Поправил список расширений 2022-07-02 14:46:22 +07:00
Book Pauk
8f351d9bef Удалил неиспользуемый код 2022-07-02 14:18:16 +07:00
Book Pauk
5ae3ea94e4 Добавлены типы файлов в диалог загрузки 2022-07-02 13:57:44 +07:00
Book Pauk
f203d453a4 Актуализация пакетов 2022-07-02 13:21:30 +07:00
Book Pauk
0d5cba121b Мелкий рефакторинг 2022-07-02 13:02:22 +07:00
Book Pauk
0cd6a48a46 Актуализация пакетов 2022-07-02 12:59:07 +07:00
Book Pauk
4e07ce2b5c Актуализация пакетов 2022-07-02 12:55:39 +07:00
Book Pauk
85a525e301 Актуализация пакета base-x 2022-07-02 12:46:10 +07:00
Book Pauk
03e4a6d723 Мелкий рефакторинг 2022-07-02 12:36:59 +07:00
Book Pauk
ab28af1abe Актуализация пакетов 2022-07-02 12:16:52 +07:00
Book Pauk
7fceed5301 Переход на axios 2022-07-02 12:16:19 +07:00
Book Pauk
0077816afa Улучшена обработка и журналирование ошибок 2022-07-02 12:07:42 +07:00
Book Pauk
cb01423147 Поправил настройки прокси 2022-07-02 00:00:13 +07:00
Book Pauk
61b0712d36 Переход на axios 2022-07-01 21:38:32 +07:00
Book Pauk
12d7843377 Merge tag '0.11.5' into develop
0.11.5
2022-04-15 16:42:40 +07:00
Book Pauk
9293c0a0d4 Merge branch 'release/0.11.5' 2022-04-15 16:42:35 +07:00
Book Pauk
bb9522197a 0.11.5 2022-04-15 16:41:24 +07:00
Book Pauk
450a2e0664 Поправки css 2022-04-15 16:38:34 +07:00
Book Pauk
41e35f3ec8 Поправки css 2022-04-15 16:09:41 +07:00
Book Pauk
a9bc98abe3 Рефакторинг 2022-04-15 15:12:28 +07:00
Book Pauk
47bca03532 Поправки подсказок 2022-04-15 15:02:02 +07:00
Book Pauk
942021371c Merge tag '0.11.4-2' into develop
0.11.4-2
2022-04-14 19:54:20 +07:00
Book Pauk
ea2f178730 Merge branch 'release/0.11.4-2' 2022-04-14 19:54:14 +07:00
Book Pauk
4b5c8d9efe Добавил подсказку 2022-04-14 19:53:47 +07:00
Book Pauk
28ebf13c3a Merge tag '0.11.4-1' into develop
0.11.4-1
2022-04-14 19:19:21 +07:00
Book Pauk
5d52e63dd9 Merge branch 'release/0.11.4-1' 2022-04-14 19:19:14 +07:00
Book Pauk
1a0e024050 Поправил баг 2022-04-14 19:18:49 +07:00
Book Pauk
e627a0d970 Merge tag '0.11.4' into develop
0.11.4
2022-04-14 19:05:36 +07:00
Book Pauk
48668d94ad Merge branch 'release/0.11.4' 2022-04-14 19:05:31 +07:00
Book Pauk
e08c431dd9 Версия 0.11.4 2022-04-14 19:05:07 +07:00
Book Pauk
5ee58ad6f0 Поправка багов 2022-04-14 19:00:04 +07:00
Book Pauk
ac0a4f0586 Добавлена кнопка 'Управление кликом' 2022-04-14 18:50:11 +07:00
Book Pauk
b6f4c153e5 Добавлена кнопка 'Загрузить из буфера обмена' 2022-04-14 18:34:41 +07:00
Book Pauk
4fdaf5f555 Добавлена кнопка 'Загрузить файл с диска' 2022-04-14 17:48:51 +07:00
Book Pauk
b4ee9d6c00 Скрыта опция "Помочь проекту".
Добавлена кнопка "Вызвать справку".
2022-04-14 17:27:29 +07:00
Book Pauk
7c73c74730 Добавлена подсказка при невалидном URL книги 2022-04-14 17:13:38 +07:00
Book Pauk
c20aa089fa npm 2022-03-29 17:45:57 +07:00
Book Pauk
b0e15c22ea Merge tag '0.11.3' into develop
0.11.3
2022-03-29 17:41:03 +07:00
Book Pauk
d58a2c065a Merge branch 'release/0.11.3' 2022-03-29 17:40:57 +07:00
Book Pauk
53135e7ee8 Поправка даты 2022-03-29 17:40:29 +07:00
Book Pauk
5c48ca9e6c Рефакторинг versionHistory, небольшие поправки 2022-03-29 17:37:24 +07:00
Book Pauk
c4a280f3d8 Скрыл устаревший чекбокс 2022-03-29 16:52:03 +07:00
Book Pauk
ba2943c722 Поправлен баг 2022-03-29 16:49:04 +07:00
Book Pauk
26f6ffc83a Убрал PayPal из списка 2022-03-29 16:25:26 +07:00
Book Pauk
bcf075a72c Доработки WebSocketConnection 2022-03-29 16:23:34 +07:00
Book Pauk
02d458d192 Миграция "jembadb" => "^2.3.0" 2022-03-29 15:49:48 +07:00
Book Pauk
a349d8af68 Обновил пакет JembaDb 2022-02-08 20:55:31 +07:00
Book Pauk
0dbaf32aac Merge tag '0.11.2' into develop
0.11.2
2022-01-11 23:25:23 +07:00
Book Pauk
e8c41ef3a8 Merge branch 'release/0.11.2' 2022-01-11 23:24:58 +07:00
Book Pauk
e43a44e986 0.11.2 2022-01-11 23:24:37 +07:00
Book Pauk
f14b8ed277 Добавлена реакция на сигнал SIGUSR2 2022-01-11 23:23:54 +07:00
Book Pauk
bbfe8a64cb Мелкая поправка 2022-01-11 23:11:04 +07:00
Book Pauk
bcf3c2dab0 Улучшение обработки ошибок 2022-01-11 22:23:35 +07:00
Book Pauk
d5404fd260 Убрал устаревший код 2022-01-11 21:30:43 +07:00
Book Pauk
54bc662e43 Поправил конфиг для nginx 2021-12-24 17:59:26 +07:00
Book Pauk
42546ca97e Обновление jembadb до версии 1.3.0 2021-12-21 20:21:32 +07:00
Book Pauk
5c13cf0eb9 Добавил -C GZip для pkg 2021-12-20 17:27:04 +07:00
Book Pauk
2a9d44ae9a Поправка конфига для eslint 2021-12-20 17:26:19 +07:00
Book Pauk
38414ae7b6 Переход на пакет jembadb 2021-12-17 20:05:57 +07:00
Book Pauk
3ecb3e80ac Удалил комментарии 2021-12-12 01:56:24 +07:00
Book Pauk
4968828488 Merge tag '0.11.1-2' into develop
0.11.1-2
2021-12-03 15:25:17 +07:00
Book Pauk
4db3cd24df Merge branch 'release/0.11.1-2' 2021-12-03 15:25:11 +07:00
Book Pauk
45c6d3da77 Поправил таймаут, улучшение скорости синхронизации 2021-12-03 15:16:39 +07:00
Book Pauk
4aab1da3c6 Merge tag '0.11.1-1' into develop
0.11.1-1
2021-12-03 15:03:46 +07:00
Book Pauk
bf5dfa1c15 Merge branch 'release/0.11.1-1' 2021-12-03 15:03:37 +07:00
Book Pauk
7549bdd2b4 Обновил pkg 2021-12-03 15:02:56 +07:00
Book Pauk
1bb2525ab2 Merge tag '0.11.1' into develop
0.11.1
2021-12-03 14:35:04 +07:00
Book Pauk
22a556f612 Merge branch 'release/0.11.1' 2021-12-03 14:34:56 +07:00
Book Pauk
056611e87c Версия 0.11.1 2021-12-03 14:34:36 +07:00
Book Pauk
6debe24880 Удален более ненужный файл 2021-12-03 14:30:57 +07:00
Book Pauk
56559bddab Мелкий рефакторинг 2021-12-03 14:28:17 +07:00
Book Pauk
9ec74eccb4 Добавлен папаметр forceAutoRepair 2021-12-03 14:21:50 +07:00
Book Pauk
3d2f45c20d Мелие поправки 2021-12-03 14:21:36 +07:00
Book Pauk
fb2eedd5ba Добавлен конвертер SQLITE -> JambaDb 2021-12-03 14:07:32 +07:00
Book Pauk
e278b4a00e Мелкие поправки 2021-12-02 18:39:28 +07:00
Book Pauk
0beaa611f6 Переход на JembaDb 2021-12-02 18:36:49 +07:00
Book Pauk
14ca2daa39 Небольшой рефакторинг 2021-12-01 22:09:48 +07:00
Book Pauk
714eb3ae83 Поправки по результату тестирования 2021-12-01 21:26:26 +07:00
Book Pauk
6286d663c9 Поправлен баг 2021-12-01 19:27:16 +07:00
Book Pauk
b5db2079d2 Jemba-миграции 2021-12-01 17:50:48 +07:00
Book Pauk
b3b30b9bd9 Поправил триггер для autorepair 2021-11-24 15:15:22 +07:00
Book Pauk
0b6a726503 Новый движок БД 2021-11-24 14:15:09 +07:00
Book Pauk
609334c5a6 Пометил модули устаревшими 2021-11-24 14:14:24 +07:00
Book Pauk
4852c7aec3 Добавлен модуль AsyncExit для выполненния cleanup-процедур перед выходом из приложения 2021-11-24 14:13:13 +07:00
Book Pauk
b1e3d33694 Merge tag '0.11.0-1' into develop
0.11.0-1
2021-11-22 21:12:42 +07:00
Book Pauk
2bfc557071 Merge branch 'release/0.11.0-1' 2021-11-22 21:12:35 +07:00
Book Pauk
e1216109bc Поправлен баг с maxBodyLength клиента WebDav 2021-11-22 21:12:02 +07:00
Book Pauk
990b8f390c Merge tag '0.11.0' into develop
0.11.0
2021-11-18 18:43:53 +07:00
Book Pauk
e6f6cd4ff3 Merge branch 'release/0.11.0' 2021-11-18 18:43:45 +07:00
Book Pauk
7deb745651 Поправил ссылку на инструкцию certbot 2021-11-18 18:28:25 +07:00
Book Pauk
70f3ca8067 Добавил beta-конфиг nginx для http 2021-11-18 18:25:04 +07:00
Book Pauk
bb8497a997 Поправил ошибку в доке 2021-11-18 18:22:08 +07:00
Book Pauk
2127e2ec0a Версия 0.11.0 2021-11-18 18:17:22 +07:00
Book Pauk
9be4011d54 Небольшая поправка формирования заголовка 2021-11-18 17:56:54 +07:00
Book Pauk
c534edfeb5 Поправки 2021-11-16 15:41:51 +07:00
Book Pauk
adc8cd7243 Переход на Vue 3 2021-11-16 15:05:00 +07:00
Book Pauk
522d2d3b9c Актуализация пакетов 2021-11-16 14:41:53 +07:00
Book Pauk
046933a05e Поправка багов 2021-11-16 14:32:54 +07:00
Book Pauk
9143288de2 Поправка копирования assets 2021-11-16 14:03:21 +07:00
Book Pauk
6053ca6c0e Настройка правильных редиректов роутера 2021-11-07 15:38:17 +07:00
Book Pauk
084197530e Форматирование кода 2021-11-07 15:38:05 +07:00
Book Pauk
9f366ca811 Поправлен баг resize 2021-11-07 14:49:33 +07:00
Book Pauk
7c07e6f004 Поправка бага 2021-11-01 19:08:17 +07:00
Book Pauk
3d4d7e0342 Переход на Vue 3 2021-11-01 18:23:58 +07:00
Book Pauk
1a8f241aad Переход на Vue 3, небольшая реструктуризация файлов 2021-11-01 17:56:45 +07:00
Book Pauk
33e938b76a Поправлен баг 2021-10-31 21:51:03 +07:00
Book Pauk
e2db546066 Переход на Vue 3 2021-10-31 21:28:31 +07:00
Book Pauk
def9ee52e2 Поправка разметки 2021-10-31 13:19:11 +07:00
Book Pauk
1afe10be03 Переход на Vue 3 2021-10-31 13:14:12 +07:00
Book Pauk
fa44641fa2 Актуализация пакетов 2021-10-31 13:00:41 +07:00
Book Pauk
9a1ef85c93 Переход на Vue 3 2021-10-29 19:11:10 +07:00
Book Pauk
b848cf5aa7 Переход на Vue 3 2021-10-29 18:24:23 +07:00
Book Pauk
8057e18ebc Переход на Vue 3 2021-10-29 16:33:38 +07:00
Book Pauk
76e09ef34e Переход на Vue 3, в процессе 2021-10-29 15:27:04 +07:00
Book Pauk
00cb2dc274 Переход на Vue 3, в процессе 2021-10-29 12:56:28 +07:00
Book Pauk
ed46e91432 Переход на Vue 3, в процессе 2021-10-29 12:21:53 +07:00
Book Pauk
88d75fb0d8 Переход на Vue 3, в процессе 2021-10-28 16:55:44 +07:00
Book Pauk
a1d7a73459 Переход на Vue 3, в процессе 2021-10-28 15:17:19 +07:00
Book Pauk
687f89729b Переход на Vue 3, в процессе 2021-10-28 14:53:22 +07:00
Book Pauk
6bf678e01f Функция для преобразования Vue-класса во Vue-компонент 2021-10-28 13:52:25 +07:00
Book Pauk
a18aec2f96 Переход на Vue 3 - начало, пока ничего не работает 2021-10-27 23:09:20 +07:00
Book Pauk
1c0cf303a0 Поправка настроек eslint 2021-10-27 16:11:20 +07:00
Book Pauk
5c7ae73982 Поправки по требованиям eslint 2021-10-27 15:07:25 +07:00
Book Pauk
4e9c69a1cf Настройка eslint 2021-10-27 15:05:37 +07:00
Book Pauk
78375be8bf Поправки по требованиям eslint 2021-10-27 15:05:18 +07:00
Book Pauk
b684725094 Настройка eslint 2021-10-27 04:11:07 +07:00
Book Pauk
ff52602c3a Мелкие поправки 2021-10-27 04:10:19 +07:00
Book Pauk
ce704c5e26 Актуализация пакетов 2021-10-27 01:28:16 +07:00
Book Pauk
4503e4ed17 Актуализация пакетов 2021-10-27 01:03:47 +07:00
Book Pauk
01c384c43a Актуализация пакетов 2021-10-27 00:52:57 +07:00
Book Pauk
dda2de58a8 Актуализация пакетов, в процессе 2021-10-27 00:06:42 +07:00
Book Pauk
0365acbf7a Актуализация пакетов 2021-10-26 01:05:09 +07:00
Book Pauk
bbf1ab7180 Актуализация пакетов 2021-10-26 00:35:45 +07:00
Book Pauk
83bf1f1d3a Актуализация пакетов 2021-10-26 00:18:18 +07:00
Book Pauk
fdf04fed0e Актуализация пакетов 2021-10-25 23:55:26 +07:00
Book Pauk
acce32bfa7 Актуализация пакетов 2021-10-25 16:15:15 +07:00
Book Pauk
614c45ac7d Актуализировал readme 2021-10-25 15:31:07 +07:00
Book Pauk
c4c0199a1b Merge tag '0.10.3' into develop
0.10.3
2021-10-25 01:54:07 +07:00
Book Pauk
a53ebb9355 Merge branch 'release/0.10.3' 2021-10-25 01:54:00 +07:00
Book Pauk
06e12930c7 Актуализирован конвертер для samlib.ru 2021-10-25 01:01:26 +07:00
Book Pauk
0f7655773a Версия 0.10.3 2021-10-20 18:29:52 +07:00
Book Pauk
26660461d4 Исправлен баг парсера с пустыми параграфами (содержащими только разметку) 2021-10-20 18:05:38 +07:00
Book Pauk
b41ee91db5 Актуализировал инструкцию 2021-10-20 15:49:07 +07:00
Book Pauk
746dd8d37a Актуализация инструкции 2021-10-20 15:42:09 +07:00
Book Pauk
fb4a57027d Merge tag '0.10.2' into develop
0.10.2
2021-10-19 23:33:48 +07:00
Book Pauk
c97660bed0 Merge branch 'release/0.10.2' 2021-10-19 23:33:38 +07:00
Book Pauk
fd8c8812a3 Версия 0.10.2 2021-10-19 23:26:45 +07:00
Book Pauk
0101392858 Актуализированы версии пакетов 2021-10-19 19:55:46 +07:00
Book Pauk
cc3f82d693 Обновление пакетов pkg и sqlite 2021-10-19 02:48:08 +07:00
Book Pauk
d21997c918 Подготовка конфигов и инструкций к разворачиванию на Ubuntu 20.04 2021-10-18 20:25:45 +07:00
Book Pauk
74fec12f5c Merge tag '0.10.1-1' into develop
0.10.1-1
2021-10-10 19:08:28 +07:00
Book Pauk
59525f8fa7 Merge branch 'release/0.10.1-1' 2021-10-10 19:08:20 +07:00
Book Pauk
3c6d3befb2 Поправил историю версий 2021-10-10 19:07:48 +07:00
Book Pauk
dfa72c80bc Merge tag '0.10.1' into develop
0.10.1
2021-10-10 18:35:24 +07:00
Book Pauk
c6e534b9db Merge branch 'hotfix/0.10.1' 2021-10-10 18:35:11 +07:00
Book Pauk
032ab6a85d Хотфикс для исправления проблемы с пустой БД storage при инициализации 2021-10-10 18:34:18 +07:00
Book Pauk
830c066ebf Обновил иконку 2021-10-10 18:25:32 +07:00
Book Pauk
c432388515 Поправил иконку 2021-02-11 22:51:41 +07:00
Book Pauk
476deba93a Заменил иконку 2021-02-11 22:07:58 +07:00
Book Pauk
ffb4f2386d Merge tag '0.10.0-2' into develop
0.10.0-2
2021-02-10 20:19:51 +07:00
Book Pauk
21716163cb Merge branch 'release/0.10.0-2' 2021-02-10 20:19:47 +07:00
Book Pauk
ca924148a5 Поправки багов 2021-02-10 20:18:41 +07:00
Book Pauk
37aa9b84ae Merge tag '0.10.0-1' into develop
0.10.0-1
2021-02-10 15:41:24 +07:00
Book Pauk
c7bd7d4d7d Merge branch 'release/0.10.0-1' 2021-02-10 15:41:19 +07:00
Book Pauk
d81a50e696 Поправки багов 2021-02-10 15:40:44 +07:00
Book Pauk
dda9943dbe Merge tag '0.10.0' into develop
0.10.0
2021-02-10 03:23:57 +07:00
Book Pauk
2b4b9f24a1 Merge branch 'release/0.10.0' 2021-02-10 03:23:50 +07:00
Book Pauk
2af77f22d6 Мелкая поправка 2021-02-10 03:22:20 +07:00
Book Pauk
f142e5812d Добавлена опция "Не включать строку статуса в обои" 2021-02-10 03:18:47 +07:00
Book Pauk
ed901fc181 Добавлена возможность загружать пользовательские обои, пока без синхронизации 2021-02-10 02:55:47 +07:00
Book Pauk
87a068899a Поправки wallpaper 2021-02-09 22:29:20 +07:00
Book Pauk
115f683128 Улучшение отображения селектора обоев 2021-02-09 21:55:19 +07:00
Book Pauk
111568fc2e Поправлен баг 2021-02-09 21:16:17 +07:00
Book Pauk
825136b5ff 0.10.0 2021-02-09 21:05:26 +07:00
Book Pauk
eae34b1121 История 2021-02-09 21:04:56 +07:00
Book Pauk
b9d7a6a3bb Убрал дебаг 2021-02-09 21:00:51 +07:00
Book Pauk
1e5375f8f9 Рефакторинг 2021-02-09 21:00:18 +07:00
Book Pauk
f597c603bf Добавил цвета для статусбара и разделителя 2021-02-09 18:43:43 +07:00
Book Pauk
b93dd0a59e Поправка 2021-02-09 18:08:13 +07:00
Book Pauk
a5740e4349 Доработки 2021-02-09 18:07:02 +07:00
Book Pauk
dacbd05911 Работа над двухстраничным режимом 2021-02-09 17:47:10 +07:00
Book Pauk
65c66e0feb Работа над двухстраничным режимом 2021-02-09 15:46:57 +07:00
Book Pauk
52f9131f99 Доработки двухстраничного режима 2021-02-04 20:34:25 +07:00
Book Pauk
cfc946ad12 Работа над двухстраничным режимом 2021-02-04 20:08:06 +07:00
Book Pauk
a207a0554c Работа на двухстраничным режимом 2021-02-04 15:55:12 +07:00
Book Pauk
675e898163 Работа над двухстраничным режимом 2021-02-04 15:18:32 +07:00
Book Pauk
d2167d8605 Работа над двухстраничным режимом 2021-02-02 18:09:21 +07:00
Book Pauk
de849d3447 Рефакторинг 2021-02-02 18:08:55 +07:00
Book Pauk
6c20b0b83e Улучшения SqliteConnectionPool 2021-02-01 18:05:32 +07:00
Book Pauk
a09b70a991 Рефакторинг WebSocketConnection, небольшие улучшения 2021-02-01 17:57:24 +07:00
Book Pauk
2427a3e08b Поправка версии node 2020-12-30 03:41:59 +07:00
Book Pauk
1104f9b850 Небольшая поправка 2020-12-24 21:39:39 +07:00
Book Pauk
dc48700e9e Небольшая поправка 2020-12-24 21:35:31 +07:00
Book Pauk
f0b0c39328 Поправки по результату тестирования, незначительные улучшения 2020-12-24 20:51:02 +07:00
Book Pauk
aad74cf682 Поправки по результату тестирования, оптимизации 2020-12-24 18:32:57 +07:00
Book Pauk
d449478204 Небольшое форматирование 2020-12-24 18:21:18 +07:00
Book Pauk
d4f6536caa Поправки по результату тестирования 2020-12-24 16:33:44 +07:00
Book Pauk
1eac00f71c Поправка багов 2020-12-24 00:44:38 +07:00
Book Pauk
ca1170a9f0 Поправки по результату тестирования 2020-12-24 00:25:54 +07:00
Book Pauk
79dda03bac Рефакторинг, плюс небольшое улучшение механизма загрузки шрифта 2020-12-23 22:38:52 +07:00
Book Pauk
6c8e0b8573 Поправил баг 2020-12-23 22:23:37 +07:00
Book Pauk
17c14722fd Рефакторинг 2020-12-23 21:17:39 +07:00
Book Pauk
48612ee118 Поправлен баг 2020-12-22 02:24:46 +07:00
Book Pauk
205c676999 Переименование YandexMoney -> ЮMoney 2020-12-21 19:50:27 +07:00
Book Pauk
54e0dd0478 В список недавних добавлена полоска прочитанного 2020-12-21 18:08:35 +07:00
Book Pauk
2de8d7515e Добалвлен крестик в строку поиска 2020-12-21 17:48:49 +07:00
Book Pauk
a251d16432 Merge tag '0.9.12-1' into develop
0.9.12-1
2020-12-19 21:23:43 +07:00
172 changed files with 29586 additions and 19004 deletions

6
.babelrc Normal file
View File

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

View File

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

10
.gitignore vendored
View File

@@ -1,5 +1,5 @@
/node_modules /node_modules
/server/data /server/.liberama*
/server/public /dist
/server/ipfs dev*.sh
/dist

203
README.md
View File

@@ -1,43 +1,160 @@
# Liberama # Liberama
Браузерная онлайн-читалка книг и децентрализованная библиотека. Браузерная онлайн-читалка книг.
Читалка ![](https://omnireader.ru/favicon.ico)[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS: Выглядит соледующим образом: <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru)
![](docs/assets/face.jpg) ![](docs/assets/face.jpg)
![](docs/assets/reader.jpg) ![](docs/assets/reader.jpg)
## VPS При запуске приложения, по умолчанию веб-сервер доступен по адресу [http://127.0.0.1:44080](http://127.0.0.1:44080)
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
Для указания местоположения рабочей директории, воспользуйтесь [параметрами командной строки](#cli).
## Сборка проекта Дополнительные параметры сервера настраиваются в [конфигурационном файле](#config).
Необходима версия node.js не ниже 10.
[Отблагодарить автора проекта](https://donatty.com/liberama)
```
$ git clone https://github.com/bookpauk/liberama ##
$ cd liberama * [Возможности читалки](#capabilities)
$ npm i * [Использование](#usage)
``` * [Параметры командной строки](#cli)
* [Конфигурация](#config)
### Windows * [Разворачивание на VPS](#vps)
``` * [Сборка проекта](#build)
$ npm run build:win * [Разработка](#development)
```
<a id="capabilities" />
### Linux
``` ## Возможности читалки
$ npm run build:linux - загрузка любой страницы интернета
``` - синхронизация данных (настроек и читаемых книг) между различными устройствами
- работа в автономном режиме (без связи)
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла - изменение цвета фона, текста, размер и тип шрифта и прочее
- установка и запоминание текущей позиции и настроек в браузере и на сервере
### Разработка - кэширование файлов книг на клиенте и на сервере
``` - открытие книг с локального диска
$ npm run dev - плавный скроллинг текста
``` - анимация перелистывания
- поиск по тексту и копирование фрагмента
## Помочь проекту - запоминание недавних книг, скачивание книги из читалки в формате fb2
- управление кликом и с клавиатуры
* bitcoin: 3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85 - регистрация не требуется
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ - поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz - релизы сервера под Linux, MacOS и Windows
<a id="usage" />
## Использование
Приложение представляет собой полноценный веб-сервер в виде единого исполнимого файла.
При первом запуске, будет создана рабочая директория `.liberama` (по умолчанию - в той же папке, где исполнимый файл),
в которой хранится конфигурационный файл `config.json`, файлы веб-приложения, файлы базы данных, журналы и прочее.
Изменить рабочую директорию можно с помощью cli-параметра --app-dir
По умолчанию веб-интерфейс будет доступен по адресу [http://127.0.0.1:44080](http://127.0.0.1:44080)
<a id="cli" />
### Параметры командной строки
Запустите `liberama --help`, чтобы увидеть список опций:
```console
Usage: liberama [options]
Options:
--help Показать опции командной строки
--app-dir=<dirpath> Задать рабочую директорию, по умолчанию: <execDir>/.liberama
--auto-repair Починить БД приложения при запуске, если она повреждена
```
<a id="config" />
### Конфигурация
При первом запуске в рабочей директории будет создан конфигурационный файл `config.json`:
```js
{
// Максимальный размер файла загружаемой книги (в байтах)
"maxUploadFileSize": 52428800,
// Максимальный размер каталога <appDir>/public-files/tmp для хранения конвертированных
// файлов книг пользователей (в байтах)
"maxTempPublicDirSize": 536870912,
// Максимальный размер каталога <appDir>/public-files/upload для хранения
// загруженных в /upload (кнопка "Загрузить файл с диска") файлов книг пользователей (в байтах)
"maxUploadPublicDirSize": 209715200,
// Использование внешних конвертеров (только в среде Linux)
// Без них читалка может работать только с файлами формата fb2, txt, html, xml
// Инструкции установки внешних конвертеров см. в docs/omnireader.ru/README.md
"useExternalBookConverter": false,
// Настройки для списка серверов.
// Приложение может запускать одновременно несколько веб-серверов на разных портах
"servers": [
{
// Произвольное название сервера
"serverName": "1",
// Режим работы сервера:
// "reader" - обычная читалка
// "omnireader" - модификации для сайта omnireader.ru
// "liberama" - модификации для сайта liberama.top
// "book_update_checker" - сервер обновлений
"mode": "reader",
// Хост, порт сервера
"ip": "0.0.0.0",
"port": "44080"
}
],
// Настройки удаленного хранилища
"remoteStorage": false,
// Для веб-приложения: включение/выключение работы с сервером обновлений
"bucEnabled": false,
// Подключение себя, как клиента, к серверу обновлений
"bucServer": false
// Сcылка для открытия в новом окне брауpера по клику на кнопку "Сетевая библиотека"
// Если не задано, открывается внутренний менеджер библиотек с использванием фрейма
"networkLibraryLink": "http://samlib.ru/"
}
```
При необходимости, можно настроить нужный параметр в этом файле вручную.
<a id="vps" />
## VPS
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
<a id="build" />
### Сборка проекта
Сборка только в среде Linux.
Необходима версия node.js не ниже 16.
Для сборки linux-arm64 необходимо предварительно установить [QEMU](https://wiki.debian.org/QemuUserEmulation).
```sh
git clone https://github.com/bookpauk/liberama
cd liberama
npm i
```
#### Релизы
```sh
npm run release
```
Результат сборки будет доступен в каталоге `dist/release`
<a id="development" />
### Разработка
```sh
npm run dev
```
Связаться с автором проекта: [bookpauk@gmail.com](mailto:bookpauk@gmail.com)

View File

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

View File

@@ -1,67 +0,0 @@
const fs = require('fs-extra');
const path = require('path');
const util = require('util');
const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);
const got = require('got');
const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist');
const publicDir = `${distDir}/tmp/public`;
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))
await fs.move(publicDir, `${outDir}/public`);
await fs.ensureDir(tempDownloadDir);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-linux-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-linux-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
//ipfs
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
if (!await fs.pathExists(ipfsDecompressedFilename)) {
// Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
console.log(`done downloading ${ipfsRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/ipfs.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(ipfsDecompressedFilename, `${outDir}/ipfs`);
console.log(`copied ${tempDownloadDir}/go-ipfs/ipfs to ${outDir}/ipfs`);
//для development
const devIpfsFile = path.resolve(__dirname, '../server/ipfs');
if (!await fs.pathExists(devIpfsFile)) {
await fs.copy(ipfsDecompressedFilename, devIpfsFile);
}
}
main();

51
build/prepkg.js Normal file
View File

@@ -0,0 +1,51 @@
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const showdown = require('showdown');
const platform = process.argv[2];
const distDir = path.resolve(__dirname, '../dist');
const tmpDir = `${distDir}/tmp`;
const publicDir = `${tmpDir}/public`;
const outDir = `${distDir}/${platform}`;
async function build() {
if (!platform)
throw new Error(`Please set platform`);
await fs.emptyDir(outDir);
//добавляем readme в релиз
let readme = await fs.readFile(path.resolve(__dirname, '../README.md'), 'utf-8');
const converter = new showdown.Converter();
readme = converter.makeHtml(readme);
await fs.writeFile(`${outDir}/readme.html`, readme);
// перемещаем public на место
if (await fs.pathExists(publicDir)) {
const zipFile = `${tmpDir}/public.zip`;
const jsonFile = `${distDir}/public.json`;//distDir !!!
await fs.remove(zipFile);
execSync(`zip -r ${zipFile} .`, {cwd: publicDir, stdio: 'inherit'});
const data = (await fs.readFile(zipFile)).toString('base64');
await fs.writeFile(jsonFile, JSON.stringify({data}));
} else {
throw new Error(`publicDir: ${publicDir} does not exist`);
}
}
async function main() {
try {
await build();
} catch(e) {
console.error(e);
process.exit(1);
}
}
main();

33
build/release.js Normal file
View File

@@ -0,0 +1,33 @@
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const pckg = require('../package.json');
const distDir = path.resolve(__dirname, '../dist');
const outDir = `${distDir}/release`;
async function makeRelease(target) {
const srcDir = `${distDir}/${target}`;
if (await fs.pathExists(srcDir)) {
const zipFile = `${outDir}/${pckg.name}-${pckg.version}-${target}.zip`;
execSync(`zip -r ${zipFile} .`, {cwd: srcDir, stdio: 'inherit'});
}
}
async function main() {
try {
await fs.emptyDir(outDir);
await makeRelease('win');
await makeRelease('linux');
await makeRelease('linux-arm64');
await makeRelease('macos');
} catch(e) {
console.error(e);
process.exit(1);
}
}
main();

View File

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

View File

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

View File

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

View File

@@ -1,61 +0,0 @@
const fs = require('fs-extra');
const path = require('path');
const util = require('util');
const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);
const got = require('got');
const FileDecompressor = require('../server/core/FileDecompressor');
const distDir = path.resolve(__dirname, '../dist');
const publicDir = `${distDir}/tmp/public`;
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))
await fs.move(publicDir, `${outDir}/public`);
await fs.ensureDir(tempDownloadDir);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-win32-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-win32-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
//ipfs
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
if (!await fs.pathExists(ipfsDecompressedFilename)) {
// Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
console.log(`done downloading ${ipfsRemoteUrl}`);
//распаковываем
console.log(await decomp.unpack(`${tempDownloadDir}/ipfs.zip`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(ipfsDecompressedFilename, `${outDir}/ipfs.exe`);
console.log(`copied ${ipfsDecompressedFilename} to ${outDir}/ipfs.exe`);
}
main();

View File

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

View File

@@ -1,14 +1,15 @@
import axios from 'axios'; import axios from 'axios';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
import * as cryptoUtils from '../share/cryptoUtils';
import wsc from './webSocketConnection'; import wsc from './webSocketConnection';
const api = axios.create({ const api = axios.create({
baseURL: '/api/reader' baseURL: '/api/reader'
}); });
const workerApi = axios.create({ /*const workerApi = axios.create({
baseURL: '/api/worker' baseURL: '/api/worker'
}); });*/
class Reader { class Reader {
constructor() { constructor() {
@@ -18,59 +19,24 @@ class Reader {
if (!callback) callback = () => {}; if (!callback) callback = () => {};
let response = {}; let response = {};
try { const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
await wsc.open();
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
let prevResponse = false; let prevResponse = false;
while (1) {// eslint-disable-line no-constant-condition
response = await wsc.message(requestId);
if (!response.state && prevResponse !== false) {//экономия траффика
callback(prevResponse);
} else {//были изменения worker state
if (!response.state)
throw new Error('Неверный ответ api');
callback(response);
prevResponse = response;
}
if (response.state == 'finish' || response.state == 'error') {
break;
}
}
return response;
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const refreshPause = 500;
let i = 0;
response = {};
while (1) {// eslint-disable-line no-constant-condition while (1) {// eslint-disable-line no-constant-condition
const prevProgress = response.progress || 0; response = await wsc.message(requestId);
const prevState = response.state || 0;
response = await workerApi.post('/get-state', {workerId});
response = response.data;
callback(response);
if (!response.state) if (!response.state && prevResponse !== false) {//экономия траффика
throw new Error('Неверный ответ api'); callback(prevResponse);
} else {//были изменения worker state
if (!response.state)
throw new Error('Неверный ответ api');
callback(response);
prevResponse = response;
}
if (response.state == 'finish' || response.state == 'error') { if (response.state == 'finish' || response.state == 'error') {
break; break;
} }
if (i > 0)
await utils.sleep(refreshPause);
i++;
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
} }
return response; return response;
@@ -79,14 +45,13 @@ class Reader {
async loadBook(opts, callback) { async loadBook(opts, callback) {
if (!callback) callback = () => {}; if (!callback) callback = () => {};
let response = await api.post('/load-book', opts); let response = await wsc.message(await wsc.send(Object.assign({action: 'load-book'}, opts)));
const workerId = response.workerId;
const workerId = response.data.workerId;
if (!workerId) if (!workerId)
throw new Error('Неверный ответ api'); throw new Error('Неверный ответ api');
callback({totalSteps: 4}); callback({totalSteps: 4});
callback(response.data); callback(response);
response = await this.getWorkerStateFinish(workerId, callback); response = await this.getWorkerStateFinish(workerId, callback);
@@ -120,33 +85,7 @@ class Reader {
estSize = response.headers['content-length']; estSize = response.headers['content-length'];
} }
} catch (e) { } catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака //
let response = null
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
if (response.state == 'error') {
throw new Error(response.error);
}
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
} }
return estSize; return estSize;
@@ -176,11 +115,10 @@ class Reader {
return await axios.get(url, options); return await axios.get(url, options);
} }
async uploadFile(file, maxUploadFileSize, callback) { async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
if (!maxUploadFileSize)
maxUploadFileSize = 10*1024*1024;
if (file.size > maxUploadFileSize) if (file.size > maxUploadFileSize)
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`); throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
let formData = new FormData(); let formData = new FormData();
formData.append('file', file, file.name); formData.append('file', file, file.name);
@@ -208,26 +146,56 @@ class Reader {
} }
async storage(request) { async storage(request) {
let response = null; const response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/storage', request);
response = response.data;
}
const state = response.state; if (response.error)
if (!state)
throw new Error('Неверный ответ api');
if (response.state == 'error') {
throw new Error(response.error); throw new Error(response.error);
}
if (!response.state)
throw new Error('Неверный ответ api');
return response; return response;
} }
makeUrlFromBuf(buf) {
const key = utils.toHex(cryptoUtils.sha256(buf));
return `disk://${key}`;
}
async uploadFileBuf(buf, url) {
if (!url)
url = this.makeUrlFromBuf(buf);
let response;
try {
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
} catch (e) {
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
}
if (response.error)
throw new Error(response.error);
return response;
}
async getUploadedFileBuf(url) {
url = url.replace('disk://', '/upload/');
return (await axios.get(url)).data;
}
async checkBuc(bookUrls) {
const response = await wsc.message(await wsc.send({action: 'check-buc', bookUrls}));
if (response.error)
throw new Error(response.error);
if (!response.data)
throw new Error(`response.data is empty`);
return response.data;
}
} }
export default new Reader(); export default new Reader();

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,25 +1,27 @@
<template> <template>
<div class="fit row"> <div class="fit row">
<Notify ref="notify"/> <Notify ref="notify" />
<StdDialog ref="stdDialog"/> <StdDialog ref="stdDialog" />
<keep-alive v-if="showPage">
<router-view class="col"></router-view> <router-view v-slot="{ Component }">
</keep-alive> <keep-alive v-if="showPage">
<component :is="Component" class="col" />
</keep-alive>
</router-view>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from './vueComponent.js';
import Component from 'vue-class-component';
import Notify from './share/Notify.vue'; import Notify from './share/Notify.vue';
import StdDialog from './share/StdDialog.vue'; import StdDialog from './share/StdDialog.vue';
import sanitizeHtml from 'sanitize-html';
import miscApi from '../api/misc'; import miscApi from '../api/misc';
import * as utils from '../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Notify, Notify,
StdDialog, StdDialog,
@@ -28,33 +30,55 @@ export default @Component({
mode: function() { mode: function() {
this.setAppTitle(); this.setAppTitle();
this.redirectIfNeeded(); this.redirectIfNeeded();
} },
nightMode() {
this.setNightMode();
},
}, },
}) };
class App extends Vue { class App {
_options = componentOptions;
showPage = false; showPage = false;
itemRuText = {
'/cardindex': 'Картотека',
'/reader': 'Читалка',
'/forum': 'Форум-чат',
'/income': 'Поступления',
'/sources': 'Источники',
'/settings': 'Параметры',
'/help': 'Справка',
};
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.state = this.$store.state; this.state = this.$store.state;
this.uistate = this.$store.state.uistate; this.uistate = this.$store.state.uistate;
this.config = this.$store.state.config; this.config = this.$store.state.config;
//dark mode
let darkMode = null;
this.$root.setDarkMode = (value) => {
if (darkMode !== value) {
const vars = [
'--bg-app-color', '--text-app-color', '--bg-dialog-color', '--text-anchor-color',
'--bg-loader-color', '--bg-input-color', '--bg-btn-color1', '--bg-btn-color2',
'--bg-header-color1', '--bg-header-color2', '--bg-header-color3',
'--bg-menu-color1', '--bg-menu-color2', '--text-menu-color', '--text-ubtn-color',
'--text-tb-normal', '--bg-tb-normal', '--bg-tb-hover',
'--text-tb-active', '--bg-tb-active', '--bg-tb-active-hover',
'--text-tb-disabled', '--bg-tb-disabled',
'--bg-selected-item-color1', '--bg-selected-item-color2',
];
let root = document.querySelector(':root');
let cs = getComputedStyle(root);
let mode = (value ? '-dark' : '-light');
for (const v of vars) {
const propValue = cs.getPropertyValue(`${v}${mode}`);
root.style.setProperty(v, propValue);
}
darkMode = value;
}
};
//root route //root route
let cachedRoute = ''; let cachedRoute = '';
let cachedPath = ''; let cachedPath = '';
this.$root.rootRoute = () => { this.$root.getRootRoute = () => {
if (this.$route.path != cachedPath) { if (this.$route.path != cachedPath) {
cachedPath = this.$route.path; cachedPath = this.$route.path;
const m = cachedPath.match(/^(\/[^/]*).*$/i); const m = cachedPath.match(/^(\/[^/]*).*$/i);
@@ -62,7 +86,7 @@ class App extends Vue {
} }
return cachedRoute; return cachedRoute;
} };
this.$router.beforeEach((to, from, next) => { this.$router.beforeEach((to, from, next) => {
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница //распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
@@ -73,47 +97,53 @@ class App extends Vue {
} }
}); });
// set-app-title this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
this.$root.$on('set-app-title', this.setAppTitle);
//global keyHooks // setAppTitle
this.keyHooks = []; this.$root.setAppTitle = this.setAppTitle;
this.keyHook = (event) => {
for (const hook of this.keyHooks) //sanitize
this.$root.sanitize = sanitizeHtml;
//global event hooks
this.eventHooks = {};
this.$root.eventHook = (hookName, event) => {
if (!this.eventHooks[hookName])
return;
for (const hook of this.eventHooks[hookName])
hook(event); hook(event);
} }
this.$root.addKeyHook = (hook) => { this.$root.addEventHook = (hookName, hook) => {
if (this.keyHooks.indexOf(hook) < 0) if (!this.eventHooks[hookName])
this.keyHooks.push(hook); this.eventHooks[hookName] = [];
if (this.eventHooks[hookName].indexOf(hook) < 0)
this.eventHooks[hookName].push(hook);
} }
this.$root.removeKeyHook = (hook) => { this.$root.removeEventHook = (hookName, hook) => {
const i = this.keyHooks.indexOf(hook); if (!this.eventHooks[hookName])
return;
const i = this.eventHooks[hookName].indexOf(hook);
if (i >= 0) if (i >= 0)
this.keyHooks.splice(i, 1); this.eventHooks[hookName].splice(i, 1);
} }
document.addEventListener('keyup', (event) => { document.addEventListener('keyup', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
document.addEventListener('keypress', (event) => { document.addEventListener('keypress', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
this.keyHook(event); this.$root.eventHook('key', event);
}); });
window.addEventListener('resize', () => {
this.$root.$emit('resize');
});
}
routerReady() { window.addEventListener('resize', (event) => {
return new Promise ((resolve) => { this.$root.eventHook('resize', event);
this.$router.onReady(() => {
resolve();
});
}); });
this.setNightMode();
} }
mounted() { mounted() {
@@ -122,10 +152,13 @@ class App extends Vue {
this.setAppTitle(); this.setAppTitle();
(async() => { (async() => {
//загрузим конфиг сревера //загрузим конфиг сервера
try { try {
const config = await miscApi.loadConfig(); const config = await miscApi.loadConfig(this.config._configHash);
this.commit('config/setConfig', config);
if (!config._useCached)
this.commit('config/setConfig', config);
this.showPage = true; this.showPage = true;
} catch(e) { } catch(e) {
//проверим, не получен ли конфиг ранее //проверим, не получен ли конфиг ранее
@@ -142,59 +175,36 @@ class App extends Vue {
if (navigator.storage && navigator.storage.persist) { if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist(); navigator.storage.persist();
} }
await this.routerReady(); await this.$router.isReady();
this.redirectIfNeeded(); this.redirectIfNeeded();
})(); })();
} }
toggleCollapse() {
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
this.$root.$emit('resize');
}
get isCollapse() {
return this.uistate.asideBarCollapse;
}
get asideWidth() {
if (this.uistate.asideBarCollapse) {
return 64;
} else {
return 170;
}
}
get buttonCollapseIcon() {
if (this.uistate.asideBarCollapse) {
return 'el-icon-d-arrow-right';
} else {
return 'el-icon-d-arrow-left';
}
}
get appName() {
if (this.isCollapse)
return '<br><br>';
else
return `${this.config.name} <br>v${this.config.version}`;
}
get apiError() { get apiError() {
return this.state.apiError; return this.state.apiError;
} }
get rootRoute() { get rootRoute() {
return this.$root.rootRoute(); return this.$root.getRootRoute();
}
get nightMode() {
return this.$store.state.reader.settings.nightMode;
}
setNightMode() {
this.$root.setDarkMode(this.nightMode);
this.$q.dark.set(this.nightMode);
} }
setAppTitle(title) { setAppTitle(title) {
if (!title) { if (!title) {
if (this.mode == 'liberama.top') { if (this.mode == 'liberama') {
document.title = `Liberama Reader - всегда с вами`; document.title = `Liberama Reader - всегда с вами`;
} else if (this.mode == 'omnireader') { } else if (this.mode == 'omnireader') {
document.title = `Omni Reader - всегда с вами`; document.title = `Omni Reader - всегда с вами`;
} else if (this.config && this.mode !== null) { } else if (this.config && this.mode !== null) {
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`; document.title = `Универсальная читалка книг и ресурсов интернета`;
} }
} else { } else {
document.title = title; document.title = title;
@@ -209,56 +219,173 @@ class App extends Vue {
return this.$store.state.config.mode; return this.$store.state.config.mode;
} }
get showAsideBar() {
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader' && this.mode != 'liberama.top');
}
set showAsideBar(value) {
}
get isReaderActive() {
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
}
redirectIfNeeded() { redirectIfNeeded() {
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top')) { const search = window.location.search.substr(1);
const search = window.location.search.substr(1);
//распознавание параметра url вида "?url=<link>" и редирект при необходимости //распознавание параметра url вида "?url=<link>" и редирект при необходимости
if (!this.isReaderActive) { const s = search.split('url=');
const s = search.split('url='); const url = s[1] || '';
const url = s[1] || ''; if (url) {
const q = utils.parseQuery(s[0] || ''); window.history.replaceState({}, '', '/');
if (url) { this.$router.replace({ path: '/reader', query: {url} });
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
}
} }
} }
} }
export default vueComponent(App);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
.app-name {
margin-left: 10px;
margin-top: 10px;
text-align: center;
line-height: 140%;
font-weight: bold;
}
</style> </style>
<style> <style>
/* color schemes */
:root {
/* current */
--bg-app-color: #fff;
--text-app-color: #000;
--bg-dialog-color: #fff;
--text-anchor-color: #00f;
--bg-loader-color: #ebe2c9;
--bg-input-color: #eee;
--bg-btn-color1: #1976d2;
--bg-btn-color2: #eee;
--bg-header-color1: #007000;
--bg-header-color2: #59b04f;
--bg-header-color3: #bbdefb;
--bg-menu-color1: #eee;
--bg-menu-color2: #e0e0e0;
--text-menu-color: #757575;
--text-ubtn-color: #bbb;
--text-tb-normal: #3e843e;
--bg-tb-normal: #e6edf4;
--bg-tb-hover: #fff;
--text-tb-active: #fff;
--bg-tb-active: #8ab45f;
--bg-tb-active-hover: #81c581;
--text-tb-disabled: #d3d3d3;
--bg-tb-disabled: #808080;
--bg-selected-item-color1: #b0f0b0;
--bg-selected-item-color2: #d0f5d0;
/* light */
--bg-app-color-light: #fff;
--text-app-color-light: #000;
--bg-dialog-color-light: #fff;
--text-anchor-color-light: #00f;
--bg-loader-color-light: #ebe2c9;
--bg-input-color-light: #eee;
--bg-btn-color1-light: #1976d2;
--bg-btn-color2-light: #eee;
--bg-header-color1-light: #007000;
--bg-header-color2-light: #59b04f;
--bg-header-color3-light: #bbdefb;
--bg-menu-color1-light: #eee;
--bg-menu-color2-light: #e0e0e0;
--text-menu-color-light: #757575;
--text-ubtn-color-light: #bbb;
--text-tb-normal-light: #3e843e;
--bg-tb-normal-light: #e6edf4;
--bg-tb-hover-light: #fff;
--text-tb-active-light: #fff;
--bg-tb-active-light: #8ab45f;
--bg-tb-active-hover-light: #81c581;
--text-tb-disabled-light: #d3d3d3;
--bg-tb-disabled-light: #808080;
--bg-selected-item-color1-light: #b0f0b0;
--bg-selected-item-color2-light: #d0f5d0;
/* dark */
--bg-app-color-dark: #222;
--text-app-color-dark: #ccc;
--bg-dialog-color-dark: #444;
--text-anchor-color-dark: #09f;
--bg-loader-color-dark: #222;
--bg-input-color-dark: #333;
--bg-btn-color1-dark: #00695c;
--bg-btn-color2-dark: #333;
--bg-header-color1-dark: #004000;
--bg-header-color2-dark: #29901f;
--bg-header-color3-dark: #335673;
--bg-menu-color1-dark: #333;
--bg-menu-color2-dark: #424242;
--text-menu-color-dark: #858585;
--text-ubtn-color-dark: #555;
--text-tb-normal-dark: #3e843e;
--bg-tb-normal-dark: #ddd;
--bg-tb-hover-dark: #ccc;
--text-tb-active-dark: #ddd;
--bg-tb-active-dark: #7aa44f;
--bg-tb-active-hover-dark: #71b571;
--text-tb-disabled-dark: #d3d3d3;
--bg-tb-disabled-dark: #808080;
--bg-selected-item-color1-dark: #605020;
--bg-selected-item-color2-dark: #403010;
}
a {
color: var(--text-anchor-color);
}
.bg-app, .text-bg-app {
background-color: var(--bg-app-color);
}
.text-app {
color: var(--text-app-color);
}
.bg-dialog {
background-color: var(--bg-dialog-color);
}
.bg-input {
background-color: var(--bg-input-color);
}
.bg-btn1 {
background-color: var(--bg-btn-color1);
}
.bg-btn2 {
background-color: var(--bg-btn-color2);
}
.bg-menu-1 {
background-color: var(--bg-menu-color1);
}
.bg-menu-2 {
background-color: var(--bg-menu-color2);
}
.text-menu {
color: var(--text-menu-color);
}
.bg-header-3 {
background-color: var(--bg-header-color3);
}
/* main section */
body, html, #app { body, html, #app {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
font: normal 12pt ReaderDefault; font: normal 12pt ReaderDefault;
background-color: #333;
}
.q-notifications__list--top {
top: 55px !important;
} }
.dborder { .dborder {
@@ -270,6 +397,14 @@ body, html, #app {
animation: rotating 2s linear infinite; animation: rotating 2s linear infinite;
} }
@keyframes rotating {
from {
transform: rotate(0deg);
} to {
transform: rotate(360deg);
}
}
.notify-button-icon { .notify-button-icon {
font-size: 16px !important; font-size: 16px !important;
} }

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Book в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Book extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Card в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Card extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,70 +0,0 @@
<template>
<div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import _ from 'lodash';
const selfRoute = '/cardindex';
const tab2Route = [
'/cardindex/search',
'/cardindex/card',
'/cardindex/book',
'/cardindex/history',
];
let lastActiveTab = null;
export default @Component({
watch: {
selectedTab: function(newValue, oldValue) {
lastActiveTab = newValue;
this.setRouteByTab(newValue);
},
curRoute: function(newValue, oldValue) {
this.setTabByRoute(newValue);
},
},
})
class CardIndex extends Vue {
selectedTab = null;
mounted() {
this.setTabByRoute(this.curRoute);
}
setTabByRoute(route) {
const t = _.indexOf(tab2Route, route);
if (t >= 0) {
if (t !== this.selectedTab)
this.selectedTab = t.toString();
} else {
if (route == selfRoute && lastActiveTab !== null)
this.setRouteByTab(lastActiveTab);
}
}
setRouteByTab(tab) {
const t = Number(tab);
if (tab2Route[t] !== this.curRoute) {
this.$router.replace(tab2Route[t]);
}
}
get curRoute() {
const m = this.$route.path.match(/^(\/[^\/]*\/[^\/]*).*$/i);
return (m ? m[1] : this.$route.path);
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
</style>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел History в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class History extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Search в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Search extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

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

View File

@@ -1,124 +1,186 @@
<template> <template>
<Window ref="window" @close="close" margin="2px"> <Window ref="window" margin="2px" @close="close">
<template slot="header"> <template #header>
{{ header }} {{ header }}
</template> </template>
<template slot="buttons"> <template #buttons>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle"> <span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/> <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)"> <span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<q-icon name="la la-plus" size="16px"/> <q-icon name="la la-plus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)"> <span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<q-icon name="la la-minus" size="16px"/> <q-icon name="la la-minus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
</span> </span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp"> <span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
<q-icon name="la la-question-circle" size="16px"/> <q-icon name="la la-question-circle" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
</span> </span>
</template> </template>
<div v-show="ready" class="col column" style="min-width: 600px"> <div v-show="ready" class="col column" style="min-width: 600px">
<div class="row items-center q-px-sm" style="height: 50px"> <div class="row items-center q-px-sm" style="height: 50px">
<q-select class="q-mr-sm" ref="rootLink" v-model="rootLink" :options="rootLinkOptions" @input="rootLinkInput" <q-select
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide" ref="rootLink"
v-model="rootLink"
class="q-mr-sm"
bg-color="input"
:options="rootLinkOptions"
style="width: 230px" style="width: 230px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize outlined dense emit-value map-options display-value-sanitize options-sanitize
>
<template v-slot:prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
</q-btn>
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
</q-btn>
</template>
<template v-slot:selected>
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
</template>
</q-select>
<q-select class="q-mr-sm" ref="selectedLink" v-model="selectedLink" :options="selectedLinkOptions" @input="selectedLinkInput" style="width: 50px"
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide" @popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
> >
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Закладки</q-tooltip> <template #prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" size="12px" @click.stop="addBookmark">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить закладку
</q-tooltip>
</q-btn>
<q-btn round dense color="blue" icon="la la-bars" size="12px" @click.stop="bookmarkSettings">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Настроить закладки
</q-tooltip>
</q-btn>
</template>
<template #selected>
<div style="overflow: hidden; white-space: nowrap;">
{{ rootLinkWithoutProtocol }}
</div>
</template>
</q-select> </q-select>
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги" <q-select
ref="selectedLink"
v-model="selectedLink"
class="q-mr-sm"
bg-color="input"
:options="selectedLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Закладки
</q-tooltip>
</q-select>
<q-input
ref="input"
v-model="bookUrl"
class="col q-mr-sm"
bg-color="input"
outlined dense
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown" @focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
> >
<template v-slot:prepend> <template #prepend>
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px"> <q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" size="12px" @click="goToLink(selectedLink)">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Вернуться на стартовую страницу
</q-tooltip>
</q-btn> </q-btn>
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl"> <q-btn :disabled="!bookUrl" round dense color="blue" icon="la la-angle-double-down" size="12px" @click="openBookUrlInFrame">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Загрузить URL во фрейм
</q-tooltip>
</q-btn> </q-btn>
</template> </template>
<template v-slot:append> <template #append>
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px"> <q-btn round dense color="blue" icon="la la-cog" size="12px" @click.stop="optionsVisible = true">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Опции
</q-tooltip>
</q-btn> </q-btn>
</template> </template>
</q-input> </q-input>
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть <q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl()">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip> Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть в читалке
</q-tooltip>
</q-btn> </q-btn>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div class="col fit" ref="frameBox" style="position: relative;"> <div ref="frameBox" class="col fit" style="position: relative; background-color: white">
<div ref="frameWrap" class="overflow-hidden"> <div ref="frameWrap" class="overflow-hidden">
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe> <iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0" allow="clipboard-read; clipboard-write"></iframe>
</div> </div>
<div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div> <div v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
</div> </div>
<Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible"> <Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
<template slot="header"> <template #header>
<div class="row items-center"> <div class="row items-center">
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon> <q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div> <div v-if="addBookmarkMode == 'edit'">
<div v-else>Добавить закладку</div> Редактировать закладку
</div>
<div v-else>
Добавить закладку
</div>
</div> </div>
</template> </template>
<div class="q-mx-md row"> <div class="q-mx-md row">
<q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" @keydown="bookmarkLinkKeyDown" <q-input
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus"> ref="bookmarkLink"
v-model="bookmarkLink"
class="col q-mr-sm"
bg-color="input"
outlined dense
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
>
</q-input> </q-input>
<q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px" <q-select
ref="defaultRootLink"
v-model="defaultRootLink"
class="q-mr-sm"
bg-color="input"
:options="defaultRootLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
> >
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Предустановленные ссылки</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Предустановленные ссылки
</q-tooltip>
</q-select> </q-select>
</div> </div>
<div class="q-mx-md q-mt-md"> <div class="q-mx-md q-mt-md">
<q-input class="col q-mr-sm" ref="bookmarkDesc" outlined dense bg-color="white" v-model="bookmarkDesc" @keydown="bookmarkDescKeyDown" <q-input
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus"> ref="bookmarkDesc"
v-model="bookmarkDesc"
class="col q-mr-sm"
bg-color="input"
outlined dense
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
>
</q-input> </q-input>
</div> </div>
<template slot="footer"> <template #footer>
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn> <q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn> Отмена
</q-btn>
<q-btn :disabled="!bookmarkLink" class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark">
OK
</q-btn>
</template> </template>
</Dialog> </Dialog>
<Dialog ref="options" v-model="optionsVisible"> <Dialog ref="options" v-model="optionsVisible">
<template slot="header"> <template #header>
<div class="row items-center"> <div class="row items-center">
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon> <q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
Опции Опции
@@ -131,22 +193,29 @@
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" /> <q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
</div> </div>
<template slot="footer"> <template #footer>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn> <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">
OK
</q-btn>
</template> </template>
</Dialog> </Dialog>
</div> </div>
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible" <BookmarkSettings
@do-action="doAction" @close="closeBookmarkSettings"> v-if="bookmarkSettingsActive"
ref="bookmarkSettings"
:libs="libs"
:add-bookmark-visible="addBookmarkVisible"
@do-action="doAction" @close="closeBookmarkSettings"
>
</BookmarkSettings> </BookmarkSettings>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import Window from '../share/Window.vue'; import Window from '../share/Window.vue';
@@ -162,20 +231,20 @@ const proxySubst = {
'http://fantasy-worlds.org': 'http://b.liberama.top:23580', 'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
}; };
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
Dialog, Dialog,
BookmarkSettings BookmarkSettings
}, },
watch: { watch: {
libs: function() { libs() {
this.loadLibs(); this.loadLibs();
}, },
defaultRootLink: function() { defaultRootLink() {
this.updateBookmarkLink(); this.updateBookmarkLink();
}, },
bookUrl: function(newValue) { bookUrl(newValue) {
const value = lu.addProtocol(newValue); const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true); const subst = this.makeProxySubst(value, true);
if (value != subst) { if (value != subst) {
@@ -184,7 +253,7 @@ export default @Component({
}); });
} }
}, },
bookmarkLink: function(newValue) { bookmarkLink(newValue) {
const value = lu.addProtocol(newValue); const value = lu.addProtocol(newValue);
const subst = this.makeProxySubst(value, true); const subst = this.makeProxySubst(value, true);
if (value != subst) { if (value != subst) {
@@ -193,18 +262,26 @@ export default @Component({
}); });
} }
}, },
closeAfterSubmit: function(newValue) { closeAfterSubmit(newValue) {
this.commitProp('closeAfterSubmit', newValue); this.commitProp('closeAfterSubmit', newValue);
}, },
openInFrameOnEnter: function(newValue) { openInFrameOnEnter(newValue) {
this.commitProp('openInFrameOnEnter', newValue); this.commitProp('openInFrameOnEnter', newValue);
}, },
openInFrameOnAdd: function(newValue) { openInFrameOnAdd(newValue) {
this.commitProp('openInFrameOnAdd', newValue); this.commitProp('openInFrameOnAdd', newValue);
}, },
rootLink() {
this.rootLinkInput();
},
selectedLink() {
this.selectedLinkInput();
},
} }
}) };
class ExternalLibs extends Vue { class ExternalLibs {
_options = componentOptions;
ready = false; ready = false;
frameVisible = false; frameVisible = false;
rootLink = ''; rootLink = '';
@@ -230,12 +307,17 @@ class ExternalLibs extends Vue {
openInFrameOnAdd = false; openInFrameOnAdd = false;
frameScale = 1; frameScale = 1;
inpxReady = false;
inpxTitle = '';
inpxUrl = '';
created() { created() {
this.commit = this.$store.commit;
this.oldStartLink = ''; this.oldStartLink = '';
this.justOpened = true; this.justOpened = true;
this.$root.addKeyHook(this.keyHook); this.$root.addEventHook('key', this.keyHook);
this.$root.$on('resize', async() => { this.$root.addEventHook('resize', async() => {
await utils.sleep(200); await utils.sleep(200);
this.frameResize(); this.frameResize();
}); });
@@ -247,34 +329,9 @@ class ExternalLibs extends Vue {
this.debouncedGoToLink = _.debounce((link) => { this.debouncedGoToLink = _.debounce((link) => {
this.goToLink(link); this.goToLink(link);
}, 100, {'maxWait':200}); }, 100, {'maxWait':200});
//this.commit = this.$store.commit;
//this.commit('reader/setLibs', rstore.libsDefaults);
} }
mounted() { mounted() {
//Поправка метода toggleOption компонента select фреймворка quasar, необходимо другое поведение
//$emit('input'.. вызывается всегда
this.toggleOption = function(opt, keepOpen) {
if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
return;
}
const optValue = this.getOptionValue(opt);
if (this.multiple !== true) {
if (keepOpen !== true) {
this.updateInputValue(this.fillInput === true ? this.getOptionLabel(opt) : '', true, true);
this.hidePopup();
}
this.$refs.target !== void 0 && this.$refs.target.focus();
this.$emit('input', this.emitValue === true ? optValue : opt);
}
};
this.$refs.rootLink.toggleOption = this.toggleOption;
this.$refs.selectedLink.toggleOption = this.toggleOption;
(async() => { (async() => {
//подождем this.mode //подождем this.mode
let i = 0; let i = 0;
@@ -283,10 +340,7 @@ class ExternalLibs extends Vue {
i++; i++;
} }
if (this.mode != 'liberama.top') { this.libsDefaults = rstore.getLibsDefaults(this.mode);
this.$router.replace('/404');
return;
}
this.$refs.window.init(); this.$refs.window.init();
@@ -297,17 +351,28 @@ class ExternalLibs extends Vue {
const openerOrigin2 = `https://${openerHost}`; const openerOrigin2 = `https://${openerHost}`;
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
//from inpx-web
if (_.isObject(event.data) && event.data.from === 'inpx-web') {
//console.log(event);
this.inpxOrigin = event.origin;
this.recvInpxMessage(event.data);
return;
}
//from parent
if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2) if (event.origin !== openerOrigin1 && event.origin !== openerOrigin2)
return; return;
if (!_.isObject(event.data) || event.data.from != 'LibsPage') if (!_.isObject(event.data) || event.data.from != 'LibsPage')
return; return;
if (event.origin == openerOrigin1) if (event.origin == openerOrigin1)
this.opener = window.opener; this.opener = window.opener;
else else
this.opener = event.source; this.opener = event.source;
this.openerOrigin = event.origin;
//console.log(event); this.openerOrigin = event.origin;
this.recvMessage(event.data); this.recvMessage(event.data);
}); });
@@ -338,7 +403,10 @@ class ExternalLibs extends Vue {
} }
} else if (d.type == 'libs') { } else if (d.type == 'libs') {
this.ready = true; this.ready = true;
this.libs = _.cloneDeep(d.data); if (d.data)
this.libs = _.cloneDeep(d.data);
if (d.sets)
this.updateSets(d.sets);
} else if (d.type == 'notify') { } else if (d.type == 'notify') {
this.$root.notify.success(d.data, '', {position: 'bottom-right'}); this.$root.notify.success(d.data, '', {position: 'bottom-right'});
} }
@@ -352,6 +420,30 @@ class ExternalLibs extends Vue {
})(); })();
} }
recvInpxMessage(d) {
if (d.type == 'mes') {
switch(d.data) {
case 'hello-from-inpx-web':
this.sendInpxMessage({type: 'mes', data: 'ready'});
break;
case 'ready':
this.inpxReady = true;
break;
}
} else if (d.type == 'submitUrl') {
this.submitUrl(d.data);
} else if (d.type == 'titleChange') {
this.inpxTitle = d.data;
} else if (d.type == 'urlChange') {
this.inpxUrl = d.data;
}
}
sendInpxMessage(d) {
if (this.$refs.frame && this.inpxOrigin)
this.$refs.frame.contentWindow.postMessage(Object.assign({}, {from: 'ExternalLibs'}, d), this.inpxOrigin);
}
async checkOpener() { async checkOpener() {
if (this.opener.closed) { if (this.opener.closed) {
await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка'); await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
@@ -359,6 +451,11 @@ class ExternalLibs extends Vue {
} }
} }
updateSets(sets) {
if (sets.nightMode !== this.nightMode)
this.commit('reader/nightModeToggle');
}
commitLibs(libs) { commitLibs(libs) {
this.sendMessage({type: 'libs', data: libs}); this.sendMessage({type: 'libs', data: libs});
} }
@@ -407,12 +504,25 @@ class ExternalLibs extends Vue {
return this.$store.state.config.mode; return this.$store.state.config.mode;
} }
get nightMode() {
return this.$store.state.reader.settings.nightMode;
}
get header() { get header() {
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...'); let result = [this.ready ? 'Сетевая библиотека' : 'Загрузка...'];
if (this.ready && this.selectedLink) { if (this.ready && this.selectedLink) {
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
if (this.inpxReady && this.inpxTitle) {
result.push(this.inpxTitle);
result.push(lu.removeProtocol(this.inpxUrl));
} else {
result.push(this.libs.comment);
result.push(lu.removeProtocol(this.libs.startLink));
}
} }
this.$root.$emit('set-app-title', result);
result = result.filter(s => s).join(' | ');
this.$root.setAppTitle(result);
return result; return result;
} }
@@ -481,7 +591,7 @@ class ExternalLibs extends Vue {
get defaultRootLinkOptions() { get defaultRootLinkOptions() {
let result = []; let result = [];
rstore.libsDefaults.groups.forEach(group => { this.libsDefaults.groups.forEach(group => {
result.push({label: lu.removeProtocol(group.r), value: group.r}); result.push({label: lu.removeProtocol(group.r), value: group.r});
}); });
@@ -510,6 +620,11 @@ class ExternalLibs extends Vue {
} }
goToLink(link) { goToLink(link) {
this.inpxReady = false;
this.inpxTitle = '';
this.inpxUrl = '';
this.inpxOrigin = false;
if (!this.ready || !link) if (!this.ready || !link)
return; return;
@@ -525,6 +640,7 @@ class ExternalLibs extends Vue {
this.frameVisible = true; this.frameVisible = true;
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.frame) { if (this.$refs.frame) {
this.$refs.frame.contentWindow.location.reload(true);
this.$refs.frame.contentWindow.focus(); this.$refs.frame.contentWindow.focus();
this.frameResize(); this.frameResize();
} }
@@ -597,13 +713,17 @@ class ExternalLibs extends Vue {
this.updateStartLink(true); this.updateStartLink(true);
} }
submitUrl() { submitUrl(url) {
if (this.bookUrl) { if (!url) {
url = this.bookUrl;
this.bookUrl = '';
}
if (url) {
this.sendMessage({type: 'submitUrl', data: { this.sendMessage({type: 'submitUrl', data: {
url: this.bookUrl, url,
force: true force: true
}}); }});
this.bookUrl = '';
if (this.closeAfterSubmit) if (this.closeAfterSubmit)
this.close(); this.close();
} }
@@ -617,31 +737,33 @@ class ExternalLibs extends Vue {
} else { } else {
this.bookmarkLink = this.bookUrl; this.bookmarkLink = this.bookUrl;
this.bookmarkDesc = ''; this.bookmarkDesc = '';
if (!this.bookmarkLink && this.inpxReady && this.inpxUrl) {
this.bookmarkLink = this.inpxUrl;
if (this.inpxTitle)
this.bookmarkDesc = this.inpxTitle;
}
} }
this.addBookmarkMode = mode; this.addBookmarkMode = mode;
this.addBookmarkVisible = true; this.addBookmarkVisible = true;
this.$nextTick(() => { this.$nextTick(async() => {
await this.$refs.dialogAddBookmark.waitShown();
this.$refs.bookmarkLink.focus(); this.$refs.bookmarkLink.focus();
this.$refs.defaultRootLink.toggleOption = this.toggleOption;
}); });
} }
updateBookmarkLink() { updateBookmarkLink() {
const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink); const index = lu.getSafeRootIndexByUrl(this.libsDefaults.groups, this.defaultRootLink);
if (index >= 0) { if (index >= 0) {
this.bookmarkLink = rstore.libsDefaults.groups[index].s; this.bookmarkLink = this.libsDefaults.groups[index].s;
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink); this.bookmarkDesc = this.getCommentByLink(this.libsDefaults.groups[index].list, this.bookmarkLink);
} else { } else {
this.bookmarkLink = ''; this.bookmarkLink = '';
this.bookmarkDesc = ''; this.bookmarkDesc = '';
} }
} }
defaultRootLinkInput() {
this.updateBookmarkLink();
}
bookmarkLinkKeyDown(event) { bookmarkLinkKeyDown(event) {
if (event.key == 'Enter') { if (event.key == 'Enter') {
this.$refs.bookmarkDesc.focus(); this.$refs.bookmarkDesc.focus();
@@ -704,7 +826,7 @@ class ExternalLibs extends Vue {
this.commitLibs(libs); this.commitLibs(libs);
} else if (item.c != this.bookmarkDesc) { } else if (item.c != this.bookmarkDesc) {
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` + if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) { `Заменить '${this.$root.sanitize(item.c)}' на '${this.$root.sanitize(this.bookmarkDesc)}'?`, ' ')) {
item.c = this.bookmarkDesc; item.c = this.bookmarkDesc;
this.commitLibs(libs); this.commitLibs(libs);
} else } else
@@ -790,27 +912,29 @@ class ExternalLibs extends Vue {
<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами, <p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
что особенно актуально для мобильных устройств. Имеется возможность управлять закладками что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но, на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
к сожалению, в нем открываются не все страницы.</p> к сожалению, в нем открываются не все страницы.</p>` +
<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси. (this.mode === 'liberama' ?
`<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span> <br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
к третьим лицам. к третьим лицам.
</p> </p>
`
: '') +
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши. `<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера. На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
</p> </p>
<p>Приятного пользования ;-) <p>Приятного пользования ;-)
</p> </p>
`, 'Справка', {iconName: 'la la-info-circle'}); `, 'Справка', {iconName: 'la la-info-circle'});
} }
keyHook(event) { keyHook(event) {
if (this.$root.rootRoute() == '/external-libs') { if (this.$root.getRootRoute() == '/external-libs') {
if (this.$root.stdDialog.active) if (this.$root.stdDialog.active)
return false; return false;
@@ -836,6 +960,8 @@ class ExternalLibs extends Vue {
return false; return false;
} }
} }
export default vueComponent(ExternalLibs);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -845,14 +971,15 @@ class ExternalLibs extends Vue {
background-color: #A0A0A0; background-color: #A0A0A0;
} }
.full-screen-button { .header-button {
width: 30px; width: 30px;
height: 30px; height: 30px;
cursor: pointer; cursor: pointer;
} }
.full-screen-button:hover { .header-button:hover {
background-color: #69C05F; color: white;
background-color: #39902F;
} }
.transparent-layout { .transparent-layout {

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Help в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Help extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Income в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Income extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Страница не найдена
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class NotFound404 extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

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

View File

@@ -1,125 +1,146 @@
<template> <template>
<Window width="600px" ref="window" @close="close"> <Window ref="window" width="600px" @close="close">
<template slot="header"> <template #header>
Оглавление/закладки Оглавление/закладки
</template> </template>
<div class="bg-grey-3 row"> <div class="bg-menu-1 row">
<q-tabs <q-tabs
v-model="selectedTab" v-model="selectedTab"
active-color="black" active-color="app"
active-bg-color="white" active-bg-color="app"
indicator-color="white" indicator-color="bg-app"
dense dense
no-caps no-caps
inline-label inline-label
class="no-mp bg-grey-4 text-grey-7" class="no-mp bg-menu-2 text-menu"
> >
<q-tab name="contents" icon="la la-list" label="Оглавление" /> <q-tab name="contents" icon="la la-list" label="Оглавление" />
<q-tab name="images" icon="la la-image" label="Изображения" /> <q-tab name="images" icon="la la-image" label="Изображения" />
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" /> <!--q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" /-->
</q-tabs> </q-tabs>
</div> </div>
<div class="q-mb-sm"/> <div class="q-mb-sm" />
<div class="tab-panel" v-show="selectedTab == 'contents'"> <div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
<div> <div>
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px"> <div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}"> <div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)"> <div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/> <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
</div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div> </div>
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/> <div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
</div> <div
<div class="col row clickable" @click="setBookPos(item.offset)"> v-for="subitem in item.list"
<div :style="item.indentStyle"></div> :ref="`subitem${subitem.key}`"
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div> :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
<div class="column justify-center">{{ item.perc }}%</div> >
</div> <div class="col row clickable" @click="setBookPos(subitem.offset)">
</div> <div class="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition"> <div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"> <div class="column justify-center">
<div class="col row clickable" @click="setBookPos(subitem.offset)"> {{ subitem.perc }}%
<div class="no-expand-button"></div> </div>
<div :style="subitem.indentStyle"></div> </div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
<div class="column justify-center">{{ subitem.perc }}%</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px"> Оглавление отсутствует
Оглавление отсутствует
</div>
</div>
</div>
<div class="tab-panel" v-show="selectedTab == 'images'">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
</div>
<div class="no-expand-button column justify-center items-center">
<div class="image-num">{{ item.num }}</div>
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
<div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
</div>
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">{{ item.perc }}%</div>
</div>
</div> </div>
</div> </div>
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px"> </div>
Изображения отсутствуют
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<div>
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
<div class="col row clickable" @click="setBookPos(item.offset)">
<div class="image-thumb-box row justify-center items-center">
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
<i class="loading-img-icon la la-images"></i>
</div>
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
</div>
<div class="no-expand-button column justify-center items-center">
<div class="image-num">
{{ item.num }}
</div>
<div v-show="item.type == 'image/jpeg'" class="image-type text-black it-jpg-color row justify-center">
JPG
</div>
<div v-show="item.type == 'image/png'" class="image-type text-black it-png-color row justify-center">
PNG
</div>
<div v-show="!item.local" class="image-type text-black it-net-color row justify-center">
INET
</div>
</div>
<div :style="item.indentStyle"></div>
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
<div class="column justify-center">
{{ item.perc }}%
</div>
</div>
</div>
</div>
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
Изображения отсутствуют
</div>
</div> </div>
</div> </div>
</div>
<div class="tab-panel" v-show="selectedTab == 'bookmarks'"> <div v-show="selectedTab == 'bookmarks'" class="tab-panel">
<div class="column justify-center items-center" style="height: 100px"> <div class="column justify-center items-center" style="height: 100px">
Раздел находится в разработке Раздел находится в разработке
</div>
</div> </div>
</div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
//import _ from 'lodash'; //import _ from 'lodash';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
const ContentsPageProps = Vue.extend({ const componentOptions = {
props: {
bookPos: Number,
isVisible: Boolean,
}
});
export default @Component({
components: { components: {
Window, Window,
}, },
watch: { watch: {
bookPos: function() { bookPos() {
this.updateBookPosSelection(); this.updateBookPosSelection();
} },
selectedTab() {
this.updateBookPosScrollTop();
},
}, },
}) };
class ContentsPage extends ContentsPageProps { class ContentsPage {
_options = componentOptions;
_props = {
bookPos: Number,
isVisible: Boolean,
};
selectedTab = 'contents'; selectedTab = 'contents';
contents = []; contents = [];
images = []; images = [];
@@ -229,7 +250,7 @@ class ContentsPage extends ContentsPageProps {
const bin = parsed.binary[image.id]; const bin = parsed.binary[image.id];
const type = (bin ? bin.type : ''); const type = (bin ? bin.type : '');
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>'); const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: var(--bg-menu-color2)"><i>Без названия</i></span>');
const indentStyle = getIndentStyle(1); const indentStyle = getIndentStyle(1);
const labelStyle = getLabelStyle(1); const labelStyle = getLabelStyle(1);
@@ -255,9 +276,9 @@ class ContentsPage extends ContentsPageProps {
const {id, local} = ims[i]; const {id, local} = ims[i];
const bin = this.parsed.binary[id]; const bin = this.parsed.binary[id];
if (local) if (local)
this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : '')); this.imageSrc[id] = (bin ? `data:${bin.type};base64,${bin.data}` : '');
else else
this.$set(this.imageSrc, id, id); this.imageSrc[id] = id;
this.imageLoaded[id] = true; this.imageLoaded[id] = true;
await utils.sleep(5); await utils.sleep(5);
} }
@@ -268,31 +289,30 @@ class ContentsPage extends ContentsPageProps {
if (!this.isVisible) if (!this.isVisible)
return; return;
await utils.sleep(50); await this.$nextTick();
const bp = this.bookPos; const bp = this.bookPos;
for (let i = 0; i < this.contents.length; i++) { for (let i = 0; i < this.contents.length; i++) {
const item = this.contents[i]; const item = this.contents[i];
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength); const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
if (bp >= item.offset && bp < nextOffset) {
item.isBookPos = true;
} else if (item.isBookPos) {
item.isBookPos = false;
}
for (let j = 0; j < item.list.length; j++) { for (let j = 0; j < item.list.length; j++) {
const subitem = item.list[j]; const subitem = item.list[j];
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset); const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
if (bp >= subitem.offset && bp < nextSubOffset) { if (bp >= subitem.offset && bp < nextSubOffset) {
subitem.isBookPos = true; subitem.isBookPos = true;
this.$set(this.contents, i, Object.assign(item, {list: item.list})); this.updateBookPosScrollTop('contents', item, subitem, j);
} else if (subitem.isBookPos) { } else if (subitem.isBookPos) {
subitem.isBookPos = false; subitem.isBookPos = false;
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
} }
} }
if (bp >= item.offset && bp < nextOffset) {
this.$set(this.contents, i, Object.assign(item, {isBookPos: true}));
} else if (item.isBookPos) {
this.$set(this.contents, i, Object.assign(item, {isBookPos: false}));
}
} }
for (let i = 0; i < this.images.length; i++) { for (let i = 0; i < this.images.length; i++) {
@@ -300,11 +320,96 @@ class ContentsPage extends ContentsPageProps {
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength); const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
if (bp >= img.offset && bp < nextOffset) { if (bp >= img.offset && bp < nextOffset) {
this.$set(this.images, i, Object.assign(img, {isBookPos: true})); this.images[i].isBookPos = true;
} else if (img.isBookPos) { } else if (img.isBookPos) {
this.$set(this.images, i, Object.assign(img, {isBookPos: false})); this.images[i].isBookPos = false;
} }
} }
this.updateBookPosScrollTop();
}
/*getOffsetTop(key) {
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
return (el ? el.offsetTop : 0);
}*/
async updateBookPosScrollTop() {
try {
await this.$nextTick();
if (this.selectedTab == 'contents') {
let item;
let subitem;
let i;
//ищем выделенные item
for(const _item of this.contents) {
if (_item.isBookPos) {
item = _item;
for (let ii = 0; ii < item.list.length; ii++) {
const _subitem = item.list[ii];
if (_subitem.isBookPos) {
subitem = _subitem;
i = ii;
break;
}
}
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
let elShift = 0;
if (subitem && item.expanded) {
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
} else {
elShift = el.offsetHeight;
}
const tabPanel = this.$refs.tabPanelContents;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - elShift;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
if (this.selectedTab == 'images') {
let item;
//ищем выделенные item
for(const _item of this.images) {
if (_item.isBookPos) {
item = _item;
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
const tabPanel = this.$refs.tabPanelImages;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
} catch (e) {
console.error(e);
}
}
getFirstElem(items) {
return (Array.isArray(items) ? items[0] : items);
} }
async expandClick(key) { async expandClick(key) {
@@ -312,17 +417,17 @@ class ContentsPage extends ContentsPageProps {
const expanded = !item.expanded; const expanded = !item.expanded;
if (!expanded) { if (!expanded) {
const subitems = this.$refs[`subitem${key}`][0]; let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subitems.style.height = '0'; subdiv.style.height = '0';
await utils.sleep(200); await utils.sleep(200);
} }
this.$set(this.contents, key, Object.assign({}, item, {expanded})); this.contents[key].expanded = expanded;
if (expanded) { if (expanded) {
await this.$nextTick(); await this.$nextTick();
const subitems = this.$refs[`subitem${key}`][0]; let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subitems.style.height = subitems.scrollHeight + 'px'; subdiv.style.height = subdiv.scrollHeight + 'px';
} }
} }
@@ -342,6 +447,8 @@ class ContentsPage extends ContentsPageProps {
return true; return true;
} }
} }
export default vueComponent(ContentsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -359,27 +466,31 @@ class ContentsPage extends ContentsPageProps {
} }
.item, .subitem, .item-book-pos, .subitem-book-pos { .item, .subitem, .item-book-pos, .subitem-book-pos {
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid var(--bg-menu-color2);
} }
.item:hover, .subitem:hover { .item:hover, .subitem:hover {
background-color: #f0f0f0; background-color: var(--bg-menu-color2);
} }
.item-book-pos { .item-book-pos {
background-color: #b0f0b0; opacity: 1;
background-color: var(--bg-selected-item-color1);
} }
.subitem-book-pos { .subitem-book-pos {
background-color: #d0f5d0; opacity: 1;
background-color: var(--bg-selected-item-color2);
} }
.item-book-pos:hover { .item-book-pos:hover {
background-color: #b0e0b0; opacity: 0.8;
transition: opacity 0.2s linear;
} }
.subitem-book-pos:hover { .subitem-book-pos:hover {
background-color: #d0f0d0; opacity: 0.8;
transition: opacity 0.2s linear;
} }
.expand-button, .no-expand-button { .expand-button, .no-expand-button {
@@ -428,6 +539,7 @@ class ContentsPage extends ContentsPageProps {
.image-thumb { .image-thumb {
height: 50px; height: 50px;
background-color: white;
} }
.loading-img-icon { .loading-img-icon {

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window @close="close"> <Window @close="close">
<template slot="header"> <template #header>
Скопировать текст Скопировать текст
</template> </template>
@@ -12,18 +12,19 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import {sleep} from '../../../share/utils'; import {sleep} from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
}) };
class CopyTextPage extends Vue { class CopyTextPage {
_options = componentOptions;
text = null; text = null;
initStep = null; initStep = null;
initPercentage = 0; initPercentage = 0;
@@ -51,18 +52,21 @@ class CopyTextPage extends Vue {
from = (from < 0 ? 0 : from); from = (from < 0 ? 0 : from);
to = paraIndex + 100; to = paraIndex + 100;
to = (to > parsed.para.length ? parsed.para.length : to); to = (to > parsed.para.length ? parsed.para.length : to);
cut = '<p>..... Текст вырезан. Если хотите скопировать больше, поставьте в настройках галочку "Загружать весь текст"'; cut = '<dd>..... Текст вырезан. Если хотите скопировать больше, поставьте в настройках галочку "Загружать весь текст"';
} }
if (from > 0) if (from > 0)
text += cut; text += cut;
for (let i = from; i < to; i++) { for (let i = from; i < to; i++) {
const p = parsed.para[i]; const p = parsed.para[i];
if (p.addIndex > 0)
continue;
const parts = parsed.splitToStyle(p.text); const parts = parsed.splitToStyle(p.text);
if (this.stopInit) if (this.stopInit)
return; return;
text += `<p id="p${i}" class="copyPara">`; text += `<dd id="p${i}" class="copyPara">&nbsp;&nbsp;`;
for (const part of parts) for (const part of parts)
text += part.text; text += part.text;
@@ -101,6 +105,8 @@ class CopyTextPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(CopyTextPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

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

View File

@@ -1,51 +1,17 @@
<template> <template>
<div class="page"> <div class="page">
<div class="box"> <div class="column items-center" style="width: 500px">
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p> <p class="p">
<div class="address"> Здесь вы можете пожертвовать на развитие проекта:
<img class="logo" src="./assets/yandex.png"> </p>
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
<div class="para">{{ yandexAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address"> <q-btn no-caps class="q-my-lg" color="green-8" size="14px" style="width: 200px" @click="makeDonation">
<img class="logo" src="./assets/paypal.png"> <q-icon class="q-mr-xs" name="la la-donate" size="24px" />
<div class="para">{{ paypalAddress }} Поддержать проект
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')"> </q-btn>
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address"> <div style="font-size: 60%">
<img class="logo" src="./assets/bitcoin.png"> * Ваш донат является подарком автору проекта
<div class="para">{{ bitcoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/litecoin.png">
<div class="para">{{ litecoinAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/monero.png">
<div class="para">{{ moneroAddress }}
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -53,34 +19,20 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({ import * as utils from '../../../../share/utils';
})
class DonateHelpPage extends Vue {
yandexAddress = '410018702323056';
paypalAddress = 'bookpauk@gmail.com';
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
moneroAddress = '8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz';
class DonateHelpPage {
created() { created() {
} }
donateYandexMoney() { makeDonation() {
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank'); utils.makeDonation();
}
async copyAddress(address, prefix) {
const result = await copyTextToClipboard(address);
if (result)
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
else
this.$root.notify.error('Копирование не удалось');
} }
} }
export default vueComponent(DonateHelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -97,31 +49,4 @@ class DonateHelpPage extends Vue {
padding: 0; padding: 0;
text-indent: 20px; text-indent: 20px;
} }
.box {
max-width: 550px;
overflow-wrap: break-word;
}
.address {
padding-top: 10px;
margin-top: 20px;
}
.para {
margin: 10px 10px 10px 40px;
}
.logo {
width: 130px;
position: relative;
top: 10px;
}
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
color: blue;
}
</style> </style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -1,21 +1,27 @@
<template> <template>
<Window @close="close"> <Window style="z-index: 200" @close="close">
<template slot="header"> <template #header>
Справка Справка
</template> </template>
<div class="col column" style="min-width: 600px"> <div class="col column" style="min-width: 600px">
<q-btn-toggle <div class="bg-menu-1 row">
v-model="selectedTab" <q-tabs
toggle-color="primary" v-model="selectedTab"
no-caps unelevated active-color="app"
:options="buttons" active-bg-color="app"
/> indicator-color="bg-app"
<div class="separator"></div> dense
no-caps
inline-label
class="bg-menu-2 text-menu"
>
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
</q-tabs>
</div>
<keep-alive> <keep-alive>
<component ref="page" class="col" :is="activePage" <component :is="activePage" ref="page" class="col"></component>
></component>
</keep-alive> </keep-alive>
</div> </div>
</Window> </Window>
@@ -23,8 +29,7 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue'; import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
@@ -46,13 +51,15 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'], ['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'], ['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'], ['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'], //['DonateHelpPage', 'Помочь проекту'],
]; ];
export default @Component({ const componentOptions = {
components: Object.assign({ Window }, pages), components: Object.assign({ Window }, pages),
}) };
class HelpPage extends Vue { class HelpPage {
_options = componentOptions;
selectedTab = 'CommonHelpPage'; selectedTab = 'CommonHelpPage';
close() { close() {
@@ -87,12 +94,10 @@ class HelpPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(HelpPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style> </style>

View File

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

View File

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

View File

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

View File

@@ -4,14 +4,14 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
//import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
import _ from 'lodash';
export default @Component({ const componentOptions = {
components: { components: {
Window Window
}, },
@@ -20,19 +20,26 @@ export default @Component({
this.sendLibs(); this.sendLibs();
}, },
} }
}) };
class LibsPage extends Vue { class LibsPage {
_options = componentOptions;
created() { created() {
this.popupWindow = null; this.popupWindow = null;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.messageListener = null; this.messageListener = null;
//this.commit('reader/setLibs', rstore.libsDefaults);
} }
init() { async init() {
if (this.mode != 'liberama.top') if (!this.mode)
return; return;
//TODO: убрать условие с mode в 24г
if (!this.libs || !this.libs.groups || (this.mode === 'omnireader' && this.libs.mode !== this.mode)) {
const defaults = rstore.getLibsDefaults(this.mode);
this.commit('reader/setLibs', defaults);
}
this.childReady = false; this.childReady = false;
const subdomain = (window.location.protocol != 'http:' ? 'b.' : ''); const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
this.origin = `http://${subdomain}${window.location.host}`; this.origin = `http://${subdomain}${window.location.host}`;
@@ -112,14 +119,20 @@ class LibsPage extends Vue {
return this.$store.state.reader.libs; return this.$store.state.reader.libs;
} }
get nightMode() {
return this.$store.state.reader.settings.nightMode;
}
sendLibs() { sendLibs() {
this.sendMessage({type: 'libs', data: this.libs}); this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs), sets: {nightMode: this.nightMode}});
} }
close() { close() {
this.$emit('libs-close'); this.$emit('libs-close');
} }
} }
export default vueComponent(LibsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div ref="main" class="column no-wrap" style="min-height: 500px"> <div ref="main" class="column no-wrap" style="min-height: 500px">
<div v-if="mode != 'liberama.top'" class="relative-position"> <div v-if="mode != 'liberama'" class="relative-position">
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner> <GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
</div> </div>
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px"> <div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
<span class="greeting"><b>{{ title }}</b></span> <span class="greeting"><b>{{ title }}</b></span>
@@ -12,21 +12,31 @@
</div> </div>
<div class="col-auto column justify-start items-center no-wrap overflow-hidden"> <div class="col-auto column justify-start items-center no-wrap overflow-hidden">
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги"> <q-input
<template v-slot:append> ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/> outlined dense bg-color="input" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
>
<template #append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
</template> </template>
</q-input> </q-input>
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/> <input
id="file" ref="file" type="file"
style="display: none;"
:accept="acceptFileExt"
@change="loadFile"
/>
<div class="q-my-sm"></div> <div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick"> <q-btn no-caps dense class="q-px-sm" color="btn1" size="13px" @click="loadFileClick">
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
Загрузить файл с диска Загрузить файл с диска
</q-btn> </q-btn>
<div class="q-my-sm"></div> <div class="q-my-sm"></div>
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick"> <q-btn no-caps dense class="q-px-sm" color="btn1" size="13px" @click="loadBufferClick">
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена Из буфера обмена
</q-btn> </q-btn>
@@ -58,20 +68,25 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import GithubCorner from './GithubCorner/GithubCorner.vue'; import GithubCorner from './GithubCorner/GithubCorner.vue';
import Dialog from '../../share/Dialog.vue';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue'; import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
import * as utils from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
GithubCorner, GithubCorner,
Dialog,
PasteTextPage, PasteTextPage,
}, },
}) };
class LoaderPage extends Vue { class LoaderPage {
_options = componentOptions;
bookUrl = null; bookUrl = null;
loadPercent = 0; loadPercent = 0;
pasteTextActive = false; pasteTextActive = false;
@@ -93,7 +108,7 @@ class LoaderPage extends Vue {
get title() { get title() {
if (this.mode == 'omnireader') if (this.mode == 'omnireader')
return 'Omni Reader - браузерная онлайн-читалка.'; return 'Omni Reader - браузерная онлайн-читалка.';
if (this.mode == 'liberama.top') if (this.mode == 'liberama')
return 'Liberama Reader - браузерная онлайн-читалка.'; return 'Liberama Reader - браузерная онлайн-читалка.';
return 'Универсальная читалка книг и ресурсов интернета.'; return 'Универсальная читалка книг и ресурсов интернета.';
@@ -107,14 +122,16 @@ class LoaderPage extends Vue {
return this.$store.state.config.version; return this.$store.state.config.version;
} }
get acceptFileExt() {
return this.$store.state.config.acceptFileExt;
}
get isExternalConverter() { get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter; return this.$store.state.config.useExternalBookConverter;
} }
get clientVersion() { get clientVersion() {
let v = versionHistory[0].header; return versionHistory[0].version;
v = v.split(' ')[0];
return v;
} }
submitUrl() { submitUrl() {
@@ -136,16 +153,20 @@ class LoaderPage extends Vue {
} }
loadBufferClick() { loadBufferClick() {
this.pasteTextToggle(); this.showPasteText();
} }
loadBuffer(opts) { loadBuffer(opts) {
if (opts.buffer.length) { if (opts.buffer.length) {
const file = new File([opts.buffer], 'dummyName-PasteFromClipboard'); const file = new File([opts.buffer], `paste_from_clipboard_#${utils.randomHexString(10)}`);
this.$emit('load-file', {file}); this.$emit('load-file', {file});
} }
} }
showPasteText() {
this.pasteTextActive = true;
}
pasteTextToggle() { pasteTextToggle() {
this.pasteTextActive = !this.pasteTextActive; this.pasteTextActive = !this.pasteTextActive;
} }
@@ -166,30 +187,27 @@ class LoaderPage extends Vue {
window.open('http://old.omnireader.ru', '_blank'); window.open('http://old.omnireader.ru', '_blank');
} }
async onInputKeydown(event) {
if (event.key == 'Enter') {
await utils.sleep(100);
this.submitUrl();
}
}
keyHook(event) { keyHook(event) {
if (this.pasteTextActive) { if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event); return this.$refs.pasteTextPage.keyHook(event);
} }
//недостатки сторонних ui const input = this.$refs.input.getNativeElement();
const input = this.$refs.input.$refs.input; if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
this.submitUrl();
return true; return true;
}
if (event.type == 'keydown' && document.activeElement !== input) {
const action = this.$root.readerActionByKeyEvent(event);
switch (action) {
case 'help':
this.openHelp(event);
return true;
}
}
return false; return false;
} }
} }
export default vueComponent(LoaderPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
@@ -199,7 +217,7 @@ class LoaderPage extends Vue {
} }
.clickable { .clickable {
color: blue; color: var(--text-anchor-color);
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window @close="close"> <Window @close="close">
<template slot="header"> <template #header>
<span style="position: relative; top: -3px"> <span style="position: relative; top: -3px">
Вставьте текст и нажмите Вставьте текст и нажмите
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span> <span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
@@ -8,27 +8,30 @@
</span> </span>
</template> </template>
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/> <div class="fit column main">
<hr/> <q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea> <hr />
<textarea ref="textArea" class="main text" @paste="calcTitle"></textarea>
</div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../../share/Window.vue'; import Window from '../../../share/Window.vue';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../../share/utils'; import * as utils from '../../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
}) };
class PasteTextPage extends Vue { class PasteTextPage {
_options = componentOptions;
bookTitle = ''; bookTitle = '';
created() { created() {
@@ -38,6 +41,10 @@ class PasteTextPage extends Vue {
this.$refs.textArea.focus(); this.$refs.textArea.focus();
} }
get dark() {
return this.$store.state.reader.settings.nightMode;
}
getNonEmptyLine3words(text, count) { getNonEmptyLine3words(text, count) {
let result = ''; let result = '';
const lines = text.split("\n"); const lines = text.split("\n");
@@ -59,16 +66,20 @@ class PasteTextPage extends Vue {
calcTitle(event) { calcTitle(event) {
if (this.bookTitle == '') { if (this.bookTitle == '') {
let text = event.clipboardData.getData('text'); this.bookTitle = `Из буфера обмена ${utils.dateFormat(new Date())}`;
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}: ` + _.compact([ if (event) {
this.getNonEmptyLine3words(text, 1), let text = event.clipboardData.getData('text');
this.getNonEmptyLine3words(text, 2) this.bookTitle += ': ' + _.compact([
]).join(' - '); this.getNonEmptyLine3words(text, 1),
this.getNonEmptyLine3words(text, 2)
]).join(' - ');
}
} }
} }
loadBuffer() { loadBuffer() {
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`}); this.calcTitle();
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
this.close(); this.close();
} }
@@ -90,6 +101,8 @@ class PasteTextPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(PasteTextPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -108,6 +121,11 @@ class PasteTextPage extends Vue {
outline: none; outline: none;
} }
.main {
color: var(--text-app-color);
background-color: var(--bg-app-color);
}
hr { hr {
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +1,109 @@
<template> <template>
<div> <div>
<Dialog ref="dialog1" v-model="whatsNewVisible"> <Dialog ref="dialog1" v-model="whatsNewVisible">
<template slot="header"> <template #header>
Что нового: Что нового:
</template> </template>
<div style="line-height: 20px" v-html="whatsNewContent"></div> <div style="line-height: 20px; min-width: 300px">
<div v-html="whatsNewContent"></div>
</div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span> <span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer">
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn> <template #footer>
</span> <q-btn class="q-px-md" color="btn2" text-color="app" dense no-caps @click="whatsNewDisable">
Больше не показывать
</q-btn>
</template>
</Dialog> </Dialog>
<Dialog ref="dialog2" v-model="donationVisible"> <q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
<template slot="header"> <div class="column bg-dialog no-wrap q-pa-md">
Здравствуйте, уважаемые читатели! <div class="row justify-center q-mb-md">
Здравствуйте, дорогие читатели!
</div>
<div class="q-mx-md column" style="font-size: 90%; word-break: normal">
<div>
Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
Напоминаем вам, что проект является некоммерческим и обладает такими
достоинствами, как:
<ul>
<li>все функции читалки открыты и доступны совершенно бесплатно</li>
<li>в проекте отсутствует какая-либо реклама или баннеры</li>
<li>нет никакой регистрации и монетизации</li>
<li>нет сбора персональных данных</li>
<li>открытый исходный код</li>
<li>проект постепенно улучшается, по мере возможности</li>
</ul>
Однако на оплату хостинга читалки и сервера обновлений автор тратит свои
собственные средства, а также тратит свое время и силы на улучшение проекта.
<br><br>
Давайте поддержим наш ресурс, чтобы и дальше спокойно существовать и развиваться:
</div>
<q-btn style="margin: 10px 20px 10px 20px" color="green-8" no-caps @click="makeDonation">
<q-icon class="q-mr-xs" name="la la-donate" size="24px" />
Поддержать проект
</q-btn>
<div class="row justify-center q-mt-sm">
Напомнить снова через:
</div>
<div class="row justify-between" style="margin: 0 20px 10px 20px">
<q-btn style="width: 140px; margin-top: 5px" no-caps @click="donationDialogRemindLater(30)">
1 месяц
</q-btn>
<q-btn style="width: 140px; margin-top: 5px" no-caps @click="donationDialogRemindLater(60)">
2 месяца
</q-btn>
<q-btn style="width: 140px; margin-top: 5px" no-caps @click="donationDialogRemindLater(90)">
3 месяца
</q-btn>
</div>
<div class="row justify-center q-mt-md">
<div class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
Помочь проекту можно в любое время
</div>
</div>
</div>
</div>
</q-dialog>
<Dialog ref="dialog3" v-model="urlHelpVisible">
<template #header>
Обнаружена невалидная ссылка в поле "URL книги".
<br>
</template> </template>
<div style="word-break: normal"> <div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br> Если вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
<q-btn no-caps dense class="q-px-sm" color="btn1" size="13px" @click="loadBufferClick">
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей. <q-icon class="q-mr-xs" name="la la-comment" size="24px" />
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый Из буфера обмена
сможет проголосовать рублем за то, чтобы читалка так и оставалась: </q-btn>
на странице загрузки.
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
</q-icon>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
<br><br>
<div class="row justify-center">
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
</div>
</div> </div>
<span slot="footer">
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br>
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
</span>
</Dialog> </Dialog>
</div> </div>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Dialog from '../../share/Dialog.vue'; import Dialog from '../../share/Dialog.vue';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import {versionHistory} from '../versionHistory'; import {versionHistory} from '../versionHistory';
import rstore from '../../../store/modules/reader';
export default @Component({ const componentOptions = {
components: { components: {
Dialog Dialog
}, },
@@ -78,11 +112,14 @@ export default @Component({
this.loadSettings(); this.loadSettings();
}, },
}, },
}) };
class ReaderDialogs extends Vue { class ReaderDialogs {
_options = componentOptions;
whatsNewVisible = false; whatsNewVisible = false;
whatsNewContent = ''; whatsNewContent = '';
donationVisible = false; donationVisible = false;
urlHelpVisible = false;
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -94,49 +131,54 @@ class ReaderDialogs extends Vue {
async init() { async init() {
await this.showWhatsNew(); await this.showWhatsNew();
await this.showDonation(); //await this.showDonation();
} }
loadSettings() { loadSettings() {
const settings = this.settings; const settings = this.settings;
this.showWhatsNewDialog = settings.showWhatsNewDialog; this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020; this.showDonationDialog = settings.showDonationDialog;
} }
async showWhatsNew() { async showWhatsNew() {
const whatsNew = versionHistory[0]; const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog && if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') && whatsNew.showUntil >= utils.dateFormat(new Date(), 'YYYY-MM-DD') &&
whatsNew.header != this.whatsNewContentHash) { this.whatsNewHeader != this.whatsNewContentHash) {
await utils.sleep(2000); await utils.sleep(2000);
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content; this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
this.whatsNewVisible = true; this.whatsNewVisible = true;
} }
} }
async showDonation() { async showDonation() {
const today = utils.formatDate(new Date(), 'coDate'); if ((this.mode == 'omnireader' || this.mode == 'liberama') && this.showDonationDialog && this.donationNextPopup <= Date.now()) {
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
await utils.sleep(3000); await utils.sleep(3000);
this.donationVisible = true; this.donationVisible = true;
} }
} }
donationDialogDisable() { async showUrlHelp() {
this.donationVisible = false; this.urlHelpVisible = true;
if (this.showDonationDialog2020) {
this.commit('reader/setSettings', { showDonationDialog2020: false });
}
} }
donationDialogRemind() { loadBufferClick() {
this.$emit('load-buffer-toggle');
this.urlHelpVisible = false;
}
donationDialogRemindLater(remindAfter = 30) {
this.donationVisible = false; this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
this.commit('reader/setDonationNextPopup', Date.now() + rstore.dayMs*remindAfter);
}
makeDonation() {
utils.makeDonation();
this.donationDialogRemindLater();
} }
openDonate() { openDonate() {
this.donationVisible = false;
this.$emit('donate-toggle'); this.$emit('donate-toggle');
} }
@@ -155,8 +197,11 @@ class ReaderDialogs extends Vue {
whatsNewDisable() { whatsNewDisable() {
this.whatsNewVisible = false; this.whatsNewVisible = false;
const whatsNew = versionHistory[0]; this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
this.commit('reader/setWhatsNewContentHash', whatsNew.header); }
get whatsNewHeader() {
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
} }
get mode() { get mode() {
@@ -171,22 +216,24 @@ class ReaderDialogs extends Vue {
return this.$store.state.reader.whatsNewContentHash; return this.$store.state.reader.whatsNewContentHash;
} }
get donationRemindDate() { get donationNextPopup() {
return this.$store.state.reader.donationRemindDate; return this.$store.state.reader.donationNextPopup;
} }
keyHook() { keyHook() {
if (this.$refs.dialog1.active || this.$refs.dialog2.active) if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
return true; return true;
return false; return false;
} }
} }
export default vueComponent(ReaderDialogs);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
.clickable { .clickable {
color: blue; color: var(--text-anchor-color);
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<template> <template>
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close"> <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header"> <template #header>
{{ header }} {{ header }}
</template> </template>
@@ -8,18 +8,24 @@
<span v-show="initStep">{{ initPercentage }}%</span> <span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input"> <div v-show="!initStep" class="input">
<!--input ref="input" <q-input
placeholder="что ищем" ref="input" v-model="needle"
:value="needle" @input="needle = $event.target.value"/--> class="col" outlined dense
<q-input ref="input" class="col" outlined dense bg-color="input"
placeholder="что ищем" placeholder="Найти"
v-model="needle" @keydown="inputKeyDown" @keydown="inputKeyDown"
/> />
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div> <div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
{{ foundText }}
</div>
</div> </div>
<q-btn-group v-show="!initStep" class="button-group row no-wrap"> <q-btn-group v-show="!initStep" class="button-group row no-wrap">
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn> <q-btn class="button" dense stretch @click="showNext">
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn> <q-icon style="top: -2px" name="la la-angle-down" dense size="22px" />
</q-btn>
<q-btn class="button" dense stretch @click="showPrev">
<q-icon name="la la-angle-up" dense size="22px" />
</q-btn>
</q-btn-group> </q-btn-group>
</div> </div>
</Window> </Window>
@@ -27,13 +33,12 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import {sleep} from '../../../share/utils'; import {sleep} from '../../../share/utils';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
@@ -49,8 +54,10 @@ export default @Component({
el.style.paddingRight = newValue.length*12 + 'px'; el.style.paddingRight = newValue.length*12 + 'px';
}, },
}, },
}) };
class SearchPage extends Vue { class SearchPage {
_options = componentOptions;
header = null; header = null;
initStep = null; initStep = null;
initPercentage = 0; initPercentage = 0;
@@ -100,12 +107,17 @@ class SearchPage extends Vue {
this.parsed = parsed; this.parsed = parsed;
} }
this.header = 'Найти'; this.header = 'Поиск в тексте';
await this.$nextTick(); await this.$nextTick();
this.$refs.input.focus(); this.focusInput();
this.$refs.input.select(); this.$refs.input.select();
} }
focusInput() {
if (!this.$root.isMobileDevice)
this.$refs.input.focus();
}
get foundText() { get foundText() {
if (this.foundList.length && this.foundCur >= 0) if (this.foundList.length && this.foundCur >= 0)
return `${this.foundCur + 1}/${this.foundList.length}`; return `${this.foundCur + 1}/${this.foundList.length}`;
@@ -143,7 +155,8 @@ class SearchPage extends Vue {
} else { } else {
this.$emit('stop-text-search'); this.$emit('stop-text-search');
} }
this.$refs.input.focus();
this.focusInput();
} }
showPrev() { showPrev() {
@@ -159,7 +172,8 @@ class SearchPage extends Vue {
} else { } else {
this.$emit('stop-text-search'); this.$emit('stop-text-search');
} }
this.$refs.input.focus();
this.focusInput();
} }
close() { close() {
@@ -180,6 +194,8 @@ class SearchPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SearchPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -4,27 +4,30 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import _ from 'lodash'; import _ from 'lodash';
import bookManager from '../share/bookManager'; import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader'; import readerApi from '../../../api/reader';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils'; import * as cryptoUtils from '../../../share/cryptoUtils';
import LockQueue from '../../../share/LockQueue';
import localForage from 'localforage'; import localForage from 'localforage';
const ssCacheStore = localForage.createInstance({ const ssCacheStore = localForage.createInstance({
name: 'ssCacheStore' name: 'ssCacheStore'
}); });
export default @Component({ const componentOptions = {
watch: { watch: {
serverSyncEnabled: function() { serverSyncEnabled: function() {
this.serverSyncEnabledChanged(); if (this.inited)
this.serverSyncEnabledChanged();
}, },
serverStorageKey: function() { serverStorageKey: function() {
this.serverStorageKeyChanged(true); if (this.inited)
this.serverStorageKeyChanged(true);
}, },
settings: function() { settings: function() {
this.debouncedSaveSettings(); this.debouncedSaveSettings();
@@ -39,14 +42,19 @@ export default @Component({
this.debouncedSaveLibs(); this.debouncedSaveLibs();
}, },
}, },
}) };
class ServerStorage extends Vue { class ServerStorage {
_options = componentOptions;
created() { created() {
this.inited = false; this.inited = false;
this.keyInited = false; this.keyInited = false;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.prevServerStorageKey = null; this.prevServerStorageKey = null;
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()}); this.identity = utils.randomHexString(20);
this.lock = new LockQueue(100);
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
this.debouncedSaveSettings = _.debounce(() => { this.debouncedSaveSettings = _.debounce(() => {
this.saveSettings(); this.saveSettings();
@@ -79,6 +87,13 @@ class ServerStorage extends Vue {
if (!this.cachedRecentMod) if (!this.cachedRecentMod)
await this.cleanCachedRecent('cachedRecentMod'); await this.cleanCachedRecent('cachedRecentMod');
//подстраховка хранения ключа, восстановим из IndexedDB при проблемах в localStorage
if (!this.serverStorageKey) {
const key = await ssCacheStore.getItem('storageKey');
if (key)
this.commit('reader/setServerStorageKey', key);
}
if (!this.serverStorageKey) { if (!this.serverStorageKey) {
//генерируем новый ключ //генерируем новый ключ
await this.generateNewServerStorageKey(); await this.generateNewServerStorageKey();
@@ -117,6 +132,7 @@ class ServerStorage extends Vue {
async generateNewServerStorageKey() { async generateNewServerStorageKey() {
const key = utils.toBase58(utils.randomArray(32)); const key = utils.toBase58(utils.randomArray(32));
this.commit('reader/setServerStorageKey', key); this.commit('reader/setServerStorageKey', key);
//дождемся serverStorageKeyChanged, событие по watch не работает при this.inited == false
await this.serverStorageKeyChanged(true); await this.serverStorageKeyChanged(true);
} }
@@ -135,6 +151,10 @@ class ServerStorage extends Vue {
async serverStorageKeyChanged(force) { async serverStorageKeyChanged(force) {
if (this.prevServerStorageKey != this.serverStorageKey) { if (this.prevServerStorageKey != this.serverStorageKey) {
this.prevServerStorageKey = this.serverStorageKey; this.prevServerStorageKey = this.serverStorageKey;
//сохраним ключ также в IndexedDB, чтобы была возможность восстановить при проблемах с localStorage
await ssCacheStore.setItem('storageKey', this.serverStorageKey);
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey)); this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
this.keyInited = true; this.keyInited = true;
@@ -199,6 +219,10 @@ class ServerStorage extends Vue {
return this.$store.state.reader.libsRev; return this.$store.state.reader.libsRev;
} }
get offlineModeActive() {
return this.$store.state.reader.offlineModeActive;
}
checkCurrentProfile() { checkCurrentProfile() {
if (!this.profiles[this.currentProfile]) { if (!this.profiles[this.currentProfile]) {
this.commit('reader/setCurrentProfile', ''); this.commit('reader/setCurrentProfile', '');
@@ -540,14 +564,16 @@ class ServerStorage extends Vue {
return true; return true;
} }
async saveRecent(itemKey, recurse) { async saveRecent(itemKeys, recurse) {
while (!this.inited || this.savingRecent) while (!this.inited)
await utils.sleep(100); await utils.sleep(100);
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent) if (!this.keyInited || !this.serverSyncEnabled)
return; return;
this.savingRecent = true; let needRecurseCall = false;
await this.lock.get();
try { try {
const bm = bookManager; const bm = bookManager;
@@ -557,24 +583,31 @@ class ServerStorage extends Vue {
//newRecentMod //newRecentMod
let newRecentMod = {}; let newRecentMod = {};
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) { let oneItemKey = null;
if (itemKeys && itemKeys.length == 1)
oneItemKey = itemKeys[0];
if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
newRecentMod = _.cloneDeep(this.cachedRecentMod); newRecentMod = _.cloneDeep(this.cachedRecentMod);
newRecentMod.rev++; newRecentMod.rev++;
newRecentMod.data.key = itemKey; newRecentMod.data.key = oneItemKey;
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]); newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
needSaveRecentMod = true; needSaveRecentMod = true;
} }
this.prevItemKey = itemKey; this.prevItemKey = oneItemKey;
//newRecentPatch //newRecentPatch
let newRecentPatch = {}; let newRecentPatch = {};
if (itemKey && !needSaveRecentMod) { if (itemKeys && !needSaveRecentMod) {
newRecentPatch = _.cloneDeep(this.cachedRecentPatch); newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
newRecentPatch.rev++; newRecentPatch.rev++;
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
let applyMod = this.cachedRecentMod.data; for (const key of itemKeys) {
newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
}
const applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key]) if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true}); newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
@@ -585,11 +618,7 @@ class ServerStorage extends Vue {
//newRecent //newRecent
let newRecent = {}; let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) { if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent
/*while (!bookManager.loaded)
await utils.sleep(100);*/
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)}; newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}}; newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}}; newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
@@ -623,10 +652,8 @@ class ServerStorage extends Vue {
if (res) if (res)
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`); this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) { if (!recurse && itemKeys) {
this.savingRecent = false; needRecurseCall = true;
this.saveRecent(itemKey, true);
return;
} }
} else if (result.state == 'success') { } else if (result.state == 'success') {
if (needSaveRecent && newRecent.rev) if (needSaveRecent && newRecent.rev)
@@ -635,10 +662,15 @@ class ServerStorage extends Vue {
await this.setCachedRecentPatch(newRecentPatch); await this.setCachedRecentPatch(newRecentPatch);
if (needSaveRecentMod && newRecentMod.rev) if (needSaveRecentMod && newRecentMod.rev)
await this.setCachedRecentMod(newRecentMod); await this.setCachedRecentMod(newRecentMod);
} else {
this.prevItemKey = null;
} }
} finally { } finally {
this.savingRecent = false; this.lock.ret();
} }
if (needRecurseCall)
await this.saveRecent(itemKeys, true);
} }
async storageCheck(items) { async storageCheck(items) {
@@ -654,7 +686,7 @@ class ServerStorage extends Vue {
} }
async storageApi(action, items, force) { async storageApi(action, items, force) {
const request = {action, items}; const request = {action, identity: this.identity, items};
if (force) if (force)
request.force = true; request.force = true;
const encodedRequest = await this.encodeStorageItems(request); const encodedRequest = await this.encodeStorageItems(request);
@@ -726,13 +758,15 @@ class ServerStorage extends Vue {
const ids = id.split('.'); const ids = id.split('.');
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey)) if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
throw new Error(`decodeStorageItems: bad id - ${id}`); throw new Error(`decodeStorageItems: bad id - ${id}`);
items[utils.fromBase58(ids[1])] = decoded; items[utils.fromBase58(ids[1]).toString()] = decoded;
} }
} }
result.items = items; result.items = items;
return result; return result;
} }
} }
export default vueComponent(ServerStorage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>

View File

@@ -1,30 +1,32 @@
<template> <template>
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close"> <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template slot="header"> <template #header>
Установить позицию Установить позицию
</template> </template>
<div id="set-position-slider" class="slider q-px-md"> <div class="col column justify-center">
<q-slider <div id="set-position-slider" class="slider q-px-md column justify-center">
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0" <q-slider
v-model="sliderValue" v-model="sliderValue"
:max="sliderMax" thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
label
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)" :max="sliderMax"
color="primary" label
/> :label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
</div>
</div> </div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
}, },
@@ -34,8 +36,10 @@ export default @Component({
this.$emit('book-pos-changed', {bookPos: newValue}); this.$emit('book-pos-changed', {bookPos: newValue});
}, },
}, },
}) };
class SetPositionPage extends Vue { class SetPositionPage {
_options = componentOptions;
sliderValue = null; sliderValue = null;
sliderMax = null; sliderMax = null;
@@ -67,13 +71,16 @@ class SetPositionPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SetPositionPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
.slider { .slider {
margin: 20px; margin: 0 20px 0 20px;
background-color: #efefef; height: 35px;
background-color: var(--bg-input-color);
border-radius: 15px; border-radius: 15px;
} }
</style> </style>

View File

@@ -0,0 +1,145 @@
<template>
<div class="fit sets-tab-panel">
<!---------------------------------------------->
<div class="q-mt-sm column items-center">
<span>Настройки конвертирования применяются ко всем</span>
<span>вновь загружаемым или обновляемым файлам</span>
</div>
<!---------------------------------------------->
<div class="sets-part-header">
HTML, XML, TXT
</div>
<div class="sets-item row">
<div class="sets-label label">
Текст
</div>
<div class="col row">
<q-checkbox v-model="form.splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Опция принудительно включает эвристику разбиения текста на<br>
параграфы в случае, если формат файла определен как html,<br>
xml или txt. Возможна нечитабельная разметка текста.
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Сайты
</div>
<div class="col row">
<q-checkbox v-model="form.enableSitesFilter" size="xs" label="Включить html-фильтр для сайтов">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Html-фильтр вырезает лишние элементы со<br>
страницы для определенных сайтов, таких как:<br>
samlib.ru<br>
www.fanfiction.net<br>
archiveofourown.org<br>
и других
</q-tooltip>
</q-checkbox>
</div>
</div>
<!---------------------------------------------->
<div v-if="isExternalConverter">
<div class="sets-part-header">
PDF
</div>
<div class="sets-item row">
<div class="sets-label label">
Формат
</div>
<div class="col row">
<q-checkbox v-model="form.pdfAsText" size="xs" label="Извлекать текст из PDF">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
Размер получаемого fb2-файла при этом относительно небольшой.<br>
При отключении этой опции, pdf будет представлен как набор<br>
изображений (аналогично ковертированию djvu).
</q-tooltip>
</q-checkbox>
</div>
</div>
<div v-if="!form.pdfAsText" class="sets-item row">
<div class="sets-label label">
Качество
</div>
<div class="col row">
<NumInput v-model="form.pdfQuality" bg-color="input" class="col-5" :min="10" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
размер итогового файла. Если сервер отказывается конвертировать<br>
слишком большой файл, то попробуйте понизить качество.
</q-tooltip>
</NumInput>
</div>
</div>
</div>
<!---------------------------------------------->
<div v-if="isExternalConverter">
<div class="sets-part-header">
DJVU
</div>
<div class="sets-item row">
<div class="sets-label label">
Качество
</div>
<div class="col row">
<NumInput v-model="form.djvuQuality" bg-color="input" class="col-5" :min="10" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
размер итогового файла. Если сервер отказывается конвертировать<br>
слишком большой файл, то попробуйте понизить качество.
</q-tooltip>
</NumInput>
</div>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import NumInput from '../../../share/NumInput.vue';
const componentOptions = {
components: {
NumInput
},
};
class ConvertTab {
_options = componentOptions;
_props = {
form: Object,
};
created() {
}
mounted() {
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
}
}
export default vueComponent(ConvertTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 75px;
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="fit column">
<div class="bg-menu-1 row">
<q-tabs
v-model="selectedTab"
active-color="app"
active-bg-color="app"
indicator-color="bg-app"
dense
no-caps
class="bg-menu-2 text-menu"
>
<q-tab name="mouse" label="Мышь/тачскрин" />
<q-tab name="keyboard" label="Клавиатура" />
</q-tabs>
</div>
<div class="q-mb-sm" />
<div class="col sets-tab-panel">
<div v-if="selectedTab == 'mouse'">
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col row">
<q-checkbox v-model="form.clickControl" size="xs" label="Включить управление кликом" />
</div>
</div>
</div>
<div v-if="selectedTab == 'keyboard'">
<div class="sets-item row">
<UserHotKeys v-model="form.userHotKeys" />
</div>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
const componentOptions = {
components: {
UserHotKeys,
},
};
class KeysTab {
_options = componentOptions;
_props = {
form: Object,
};
selectedTab = 'mouse';
created() {
}
mounted() {
}
get mode() {
return this.$store.state.config.mode;
}
}
export default vueComponent(KeysTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
</style>

View File

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

View File

@@ -0,0 +1,134 @@
<template>
<div class="fit sets-tab-panel">
<!---------------------------------------------->
<div class="sets-part-header">
Подсказки, уведомления
</div>
<div class="sets-item row no-wrap">
<div class="sets-label label">
Подсказка
</div>
<q-checkbox v-model="form.showClickMapPage" size="xs" label="Показывать области управления кликом" :disable="!form.clickControl">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать или нет подсказку при каждой загрузке книги
</q-tooltip>
</q-checkbox>
</div>
<div class="sets-item row">
<div class="sets-label label">
Подсказка
</div>
<q-checkbox v-model="form.blinkCachedLoad" size="xs" label="Предупреждать о загрузке из кэша">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Мерцать сообщением в строке статуса и на кнопке<br>
обновления при загрузке книги из кэша
</q-tooltip>
</q-checkbox>
</div>
<div class="sets-item row no-wrap">
<div class="sets-label label">
Уведомление
</div>
<q-checkbox v-model="form.showServerStorageMessages" size="xs" label="Показывать сообщения синхронизации">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомления и ошибки от<br>
синхронизатора данных с сервером
</q-tooltip>
</q-checkbox>
</div>
<div class="sets-item row">
<div class="sets-label label">
Уведомление
</div>
<q-checkbox v-model="form.showWhatsNewDialog" size="xs">
Показывать уведомление "Что нового"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать уведомления "Что нового"<br>
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<!--div class="sets-item row">
<div class="sets-label label">
Уведомление
</div>
<q-checkbox v-model="form.showDonationDialog" size="xs">
Показывать форму доната
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Показывать диалог для сбора пожертвований
</q-tooltip>
</q-checkbox>
</div-->
<!---------------------------------------------->
<div class="sets-part-header">
Другое
</div>
<div class="sets-item row">
<div class="sets-label label">
Парам. в URL
</div>
<q-checkbox v-model="form.allowUrlParamBookPos" size="xs">
Добавлять параметр "__p"
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Добавление параметра "__p" в строке браузера<br>
позволяет передавать ссылку на книгу в читалке<br>
без потери текущей позиции. Однако в этом случае<br>
при листании забивается история браузера, т.к. на<br>
каждое изменение позиции происходит смена URL.
</q-tooltip>
</q-checkbox>
</div>
<div class="sets-item row">
<div class="sets-label label">
Копирование
</div>
<q-checkbox v-model="form.copyFullText" size="xs" label="Загружать весь текст">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Загружать весь текст в окно<br>
копирования текста со страницы
</q-tooltip>
</q-checkbox>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
const componentOptions = {
components: {
},
};
class OthersTab {
_options = componentOptions;
_props = {
form: Object,
};
created() {
}
mounted() {
}
}
export default vueComponent(OthersTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 100px;
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="fit sets-tab-panel">
<!---------------------------------------------->
<div class="sets-part-header">
Анимация
</div>
<div class="sets-item row">
<div class="sets-label label">
Тип
</div>
<q-select
v-model="form.pageChangeAnimation" bg-color="input" class="col-left" :options="pageChangeAnimationOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
</div>
<div class="sets-item row">
<div class="sets-label label">
Скорость
</div>
<NumInput v-model="form.pageChangeAnimationSpeed" bg-color="input" class="col-left" :min="0" :max="100" :disable="form.pageChangeAnimation == ''" />
</div>
<!---------------------------------------------->
<div class="sets-part-header">
Другое
</div>
<div class="sets-item row">
<div class="sets-label label">
Страница
</div>
<q-checkbox v-model="form.keepLastToFirst" size="xs" label="Переносить последнюю строку">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Переносить последнюю строку страницы<br>
в начало следующей при листании
</q-tooltip>
</q-checkbox>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import NumInput from '../../../share/NumInput.vue';
const componentOptions = {
components: {
NumInput,
},
};
class PageMoveTab {
_options = componentOptions;
_props = {
form: Object,
};
created() {
}
mounted() {
}
get pageChangeAnimationOptions() {
let result = [
{label: 'Нет', value: ''},
{label: 'Вверх-вниз', value: 'downShift'},
(!this.form.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
{label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'},
(this.form.wallpaper == '' && !this.form.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
];
result = result.filter(v => v);
return result;
}
}
export default vueComponent(PageMoveTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
.col-left {
width: 150px;
}
</style>

View File

@@ -0,0 +1,363 @@
<template>
<div class="fit sets-tab-panel">
<div class="sets-part-header">
Управление синхронизацией данных
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<q-checkbox v-model="serverSyncEnabled" class="col" size="xs" label="Включить синхронизацию с сервером" />
</div>
<div v-show="serverSyncEnabled">
<!---------------------------------------------->
<div class="sets-part-header">
Профили устройств
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="text col">
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Устройство
</div>
<div class="col">
<q-select
v-model="currentProfile" :options="currentProfileOptions"
style="width: 275px"
bg-color="input"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options display-value-sanitize options-sanitize
/>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="addProfile">
Добавить
</q-btn>
<q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="delProfile">
Удалить
</q-btn>
<q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="delAllProfiles">
Удалить все
</q-btn>
</div>
<!---------------------------------------------->
<div class="sets-part-header">
Ключ доступа
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="text col">
Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="showServerStorageKey">
<span v-show="serverStorageKeyVisible">Скрыть</span>
<span v-show="!serverStorageKeyVisible">Показать</span>
&nbsp;ключ доступа
</q-btn>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div v-if="!serverStorageKeyVisible" class="col">
<hr />
<b>{{ partialStorageKey }}</b> (часть вашего ключа)
<hr />
</div>
<div v-else class="col" style="line-height: 100%">
<hr />
<div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
<b>{{ serverStorageKey }}</b>
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon>
</div>
<div v-if="mode == 'omnireader' || mode == 'liberama'">
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
<br><div class="text-center" style="margin-top: 5px">
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
Скопировать
</q-tooltip>
</q-icon>
</div>
</div>
<hr />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="enterServerStorageKey">
Ввести ключ доступа
</q-btn>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="generateServerStorageKey">
Сгенерировать новый ключ
</q-btn>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="text col">
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
например, после переустановки ОС или чистки/смены браузера.<br>
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
и шифруются ключом доступа перед отправкой на сервер.
</div>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import _ from 'lodash';
import * as utils from '../../../../share/utils';
import rstore from '../../../../store/modules/reader';
const componentOptions = {
watch: {
},
};
class ProfilesTab {
_options = componentOptions;
_props = {
form: Object,
};
rstore = rstore;
serverStorageKeyVisible = false;
created() {
this.commit = this.$store.commit;
}
mounted() {
}
get mode() {
return this.$store.state.config.mode;
}
get serverSyncEnabled() {
return this.$store.state.reader.serverSyncEnabled;
}
set serverSyncEnabled(newValue) {
this.commit('reader/setServerSyncEnabled', newValue);
}
get currentProfile() {
return this.$store.state.reader.currentProfile;
}
set currentProfile(newValue) {
this.commit('reader/setCurrentProfile', newValue);
}
get profiles() {
return this.$store.state.reader.profiles;
}
get currentProfileOptions() {
const profNames = Object.keys(this.profiles)
profNames.sort();
let result = [{label: 'Нет', value: ''}];
profNames.forEach(name => {
result.push({label: name, value: name});
});
return result;
}
get partialStorageKey() {
return this.serverStorageKey.substr(0, 7) + '***';
}
get serverStorageKey() {
return this.$store.state.reader.serverStorageKey;
}
get setStorageKeyLink() {
return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
}
async addProfile() {
try {
if (Object.keys(this.profiles).length >= 100) {
this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
return;
}
const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
});
if (result && result.value) {
if (this.profiles[result.value]) {
this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
} else {
const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', newProfiles);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = result.value;
}
}
} catch (e) {
//
}
}
async delProfile() {
if (!this.currentProfile)
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
if (this.profiles[this.currentProfile]) {
const newProfiles = Object.assign({}, this.profiles);
delete newProfiles[this.currentProfile];
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', newProfiles);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = '';
}
}
} catch (e) {
//
}
}
async delAllProfiles() {
if (!Object.keys(this.profiles).length)
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', {});
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = '';
}
} catch (e) {
//
}
}
async showServerStorageKey() {
this.serverStorageKeyVisible = !this.serverStorageKeyVisible;
}
async enterServerStorageKey(key) {
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Изменение ключа доступа приведет к замене всех профилей и читаемых книг в читалке.` +
`<br><br>Введите новый ключ доступа:`, ' ', {
inputValidator: (str) => {
try {
if (str && utils.fromBase58(str).length == 32) {
return true;
}
} catch (e) {
//
}
return 'Неверный формат ключа';
},
inputValue: (key && _.isString(key) ? key : null),
});
if (result && result.value && utils.fromBase58(result.value).length == 32) {
this.commit('reader/setServerStorageKey', result.value);
}
} catch (e) {
//
}
}
async generateServerStorageKey() {
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
`<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
if (this.$root.generateNewServerStorageKey)
this.$root.generateNewServerStorageKey();
}
} catch (e) {
//
}
}
async copyToClip(text, prefix) {
const result = await utils.copyTextToClipboard(text);
const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
if (result)
this.$root.notify.success(msg);
else
this.$root.notify.error(msg);
}
}
export default vueComponent(ProfilesTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 75px;
}
.text {
font-size: 90%;
line-height: 130%;
}
.copy-icon {
margin-left: 5px;
cursor: pointer;
font-size: 120%;
color: var(--text-anchor-color);
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="fit sets-tab-panel">
<div class="sets-item row">
<q-btn class="col q-ma-sm" color="btn2" text-color="app" dense no-caps @click="setDefaults">
Установить по умолчанию
</q-btn>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
const componentOptions = {
components: {
},
};
class ResetTab {
_options = componentOptions;
_props = {
form: Object,
};
created() {
}
mounted() {
}
setDefaults() {
this.$emit('tab-event', {action: 'set-defaults'});
}
}
export default vueComponent(ResetTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
</style>

View File

@@ -1,6 +1,6 @@
<template lang="includer"> <template>
<Window ref="window" height="95%" width="600px" @close="close"> <Window ref="window" width="600px" @close="close">
<template slot="header"> <template #header>
Настройки Настройки
</template> </template>
@@ -8,180 +8,135 @@
<div class="full-height"> <div class="full-height">
<q-tabs <q-tabs
ref="tabs" ref="tabs"
class="bg-grey-3 text-black"
v-model="selectedTab" v-model="selectedTab"
class="bg-menu-1 text-menu"
style="max-width: 130px"
left-icon="la la-caret-up" left-icon="la la-caret-up"
right-icon="la la-caret-down" right-icon="la la-caret-down"
active-color="white" active-color="white"
active-bg-color="primary" active-bg-color="primary"
indicator-color="black" indicator-color="bg-app"
vertical vertical
no-caps no-caps
stretch stretch
inline-label inline-label
> >
<div v-show="tabsScrollable" class="q-pt-lg"/> <q-tab v-for="item in tabs" :key="item.name" class="tab row items-center" :name="item.name">
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" /> <q-icon :name="item.icon" :color="selectedTab == item.name ? 'yellow' : 'teal-7'" size="24px" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" /> <div class="q-ml-xs" style="font-size: 90%">
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" /> {{ item.label }}
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" /> </div>
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" /> </q-tab>
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
<div v-show="tabsScrollable" class="q-pt-lg"/>
</q-tabs> </q-tabs>
</div> </div>
<div class="col fit"> <div class="col fit">
<!-- Профили ---------------------------------------------------------------------> <!-- Профили --------------------------------------------------------------------->
<div v-if="selectedTab == 'profiles'" class="fit tab-panel"> <ProfilesTab v-if="selectedTab == 'profiles'" :form="form" />
@@include('./include/ProfilesTab.inc');
</div>
<!-- Вид -------------------------------------------------------------------------> <!-- Вид ------------------------------------------------------------------------->
<div v-if="selectedTab == 'view'" class="fit column"> <ViewTab v-if="selectedTab == 'view'" :form="form" @tab-event="tabEvent" />
@@include('./include/ViewTab.inc');
</div>
<!-- Кнопки ----------------------------------------------------------------------> <!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel"> <ToolBarTab v-if="selectedTab == 'toolbar'" :form="form" />
@@include('./include/ButtonsTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------> <!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column"> <KeysTab v-if="selectedTab == 'keys'" :form="form" />
@@include('./include/KeysTab.inc');
</div>
<!-- Листание --------------------------------------------------------------------> <!-- Листание -------------------------------------------------------------------->
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel"> <PageMoveTab v-if="selectedTab == 'pagemove'" :form="form" />
@@include('./include/PageMoveTab.inc');
</div>
<!-- Конвертирование -------------------------------------------------------------> <!-- Конвертирование ------------------------------------------------------------->
<div v-if="selectedTab == 'convert'" class="fit tab-panel"> <ConvertTab v-if="selectedTab == 'convert'" :form="form" />
@@include('./include/ConvertTab.inc'); <!-- Обновление ------------------------------------------------------------------>
</div> <UpdateTab v-if="selectedTab == 'update'" :form="form" />
<!-- Прочее ----------------------------------------------------------------------> <!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel"> <OthersTab v-if="selectedTab == 'others'" :form="form" />
@@include('./include/OthersTab.inc'); <!-- Сброс ----------------------------------------------------------------------->
</div> <ResetTab v-if="selectedTab == 'reset'" :form="form" @tab-event="tabEvent" />
<!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./include/ResetTab.inc');
</div>
</div> </div>
</div> </div>
</Window> </Window>
</template> </template>
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import vueComponent from '../../vueComponent.js';
import Component from 'vue-class-component'; import { reactive } from 'vue';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../share/utils'; //stuff
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import rstore from '../../../store/modules/reader'; import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/; //pages
import ProfilesTab from './ProfilesTab/ProfilesTab.vue';
import ViewTab from './ViewTab/ViewTab.vue';
import ToolBarTab from './ToolBarTab/ToolBarTab.vue';
import KeysTab from './KeysTab/KeysTab.vue';
import PageMoveTab from './PageMoveTab/PageMoveTab.vue';
import ConvertTab from './ConvertTab/ConvertTab.vue';
import UpdateTab from './UpdateTab/UpdateTab.vue';
import OthersTab from './OthersTab/OthersTab.vue';
import ResetTab from './ResetTab/ResetTab.vue';
export default @Component({ const componentOptions = {
components: { components: {
Window, Window,
NumInput, //pages
UserHotKeys, ProfilesTab,
}, ViewTab,
data: function() { ToolBarTab,
return Object.assign({}, rstore.settingDefaults); KeysTab,
PageMoveTab,
ConvertTab,
UpdateTab,
OthersTab,
ResetTab,
}, },
watch: { watch: {
settings: function() { settings: function() {
this.settingsChanged(); this.settingsChanged();//no await
}, },
form: function(newValue) { form: {
if (this.inited) handler() {
this.commit('reader/setSettings', newValue); if (this.inited && !this.isSetsChanged) {
}, this.debouncedCommitSettings();
fontBold: function(newValue) { }
this.fontWeight = (newValue ? 'bold' : ''); },
}, deep: true,
fontItalic: function(newValue) {
this.fontStyle = (newValue ? 'italic' : '');
},
vertShift: function(newValue) {
const font = (this.webFontName ? this.webFontName : this.fontName);
if (this.fontShifts[font] != newValue) {
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
this.fontVertShift = newValue;
}
},
fontName: function(newValue) {
const font = (this.webFontName ? this.webFontName : newValue);
this.vertShift = this.fontShifts[font] || 0;
},
webFontName: function(newValue) {
const font = (newValue ? newValue : this.fontName);
this.vertShift = this.fontShifts[font] || 0;
},
wallpaper: function(newValue) {
if (newValue != '' && this.pageChangeAnimation == 'flip')
this.pageChangeAnimation = '';
},
textColor: function(newValue) {
this.textColorFiltered = newValue;
},
textColorFiltered: function(newValue) {
if (hex.test(newValue))
this.textColor = newValue;
},
backgroundColor: function(newValue) {
this.bgColorFiltered = newValue;
},
bgColorFiltered: function(newValue) {
if (hex.test(newValue))
this.backgroundColor = newValue;
}, },
}, },
}) };
class SettingsPage extends Vue { class SettingsPage {
selectedTab = 'profiles'; _options = componentOptions;
selectedViewTab = 'color';
selectedKeysTab = 'mouse';
form = {}; form = {};
fontBold = false;
fontItalic = false;
vertShift = 0;
tabsScrollable = false;
textColorFiltered = '';
bgColorFiltered = '';
webFonts = []; tabs = [
fonts = []; { name: 'profiles', icon: 'la la-users', label: 'Профили' },
{ name: 'view', icon: 'la la-eye', label: 'Вид'},
{ name: 'toolbar', icon: 'la la-grip-horizontal', label: 'Панель'},
{ name: 'keys', icon: 'la la-gamepad', label: 'Управление'},
{ name: 'pagemove', icon: 'la la-school', label: 'Листание'},
{ name: 'convert', icon: 'la la-magic', label: 'Конвертир.'},
{ name: 'update', icon: 'la la-retweet', label: 'Обновление'},
{ name: 'others', icon: 'la la-list-ul', label: 'Прочее'},
{ name: 'reset', icon: 'la la-broom', label: 'Сброс'},
];
selectedTab = 'profiles';
serverStorageKeyVisible = false; isSetsChanged = false;
toolButtons = [];
rstore = {};
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
this.form = {}; this.debouncedCommitSettings = _.debounce(() => {
this.rstore = rstore; this.commit('reader/setSettings', _.cloneDeep(this.form));
this.toolButtons = rstore.toolButtons; }, 50);
this.settingsChanged();
this.settingsChanged();//no await
} }
mounted() { mounted() {
this.$watch(
'$refs.tabs.scrollable',
(newValue) => {
this.tabsScrollable = newValue && !this.$isMobileDevice;
}
);
} }
init() { init() {
@@ -189,180 +144,20 @@ class SettingsPage extends Vue {
this.inited = true; this.inited = true;
} }
settingsChanged() { async settingsChanged() {
if (_.isEqual(this.form, this.settings)) this.isSetsChanged = true;
return; try {
this.form = reactive(_.cloneDeep(this.settings));
this.form = Object.assign({}, this.settings); } finally {
if (!this.unwatch) await this.$nextTick();
this.unwatch = {}; this.isSetsChanged = false;
for (let prop in rstore.settingDefaults) {
if (this.unwatch && this.unwatch[prop])
this.unwatch[prop]();
this[prop] = this.form[prop];
this.unwatch[prop] = this.$watch(prop, (newValue) => {
this.form = Object.assign({}, this.form, {[prop]: newValue});
});
} }
this.fontBold = (this.fontWeight == 'bold');
this.fontItalic = (this.fontStyle == 'italic');
this.fonts = rstore.fonts;
this.webFonts = rstore.webFonts;
const font = (this.webFontName ? this.webFontName : this.fontName);
this.vertShift = this.fontShifts[font] || 0;
this.textColorFiltered = this.textColor;
this.bgColorFiltered = this.backgroundColor;
}
get mode() {
return this.$store.state.config.mode;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
} }
get settings() { get settings() {
return this.$store.state.reader.settings; return this.$store.state.reader.settings;
} }
get serverSyncEnabled() {
return this.$store.state.reader.serverSyncEnabled;
}
set serverSyncEnabled(newValue) {
this.commit('reader/setServerSyncEnabled', newValue);
}
get profiles() {
return this.$store.state.reader.profiles;
}
get currentProfileOptions() {
const profNames = Object.keys(this.profiles)
profNames.sort();
let result = [{label: 'Нет', value: ''}];
profNames.forEach(name => {
result.push({label: name, value: name});
});
return result;
}
get wallpaperOptions() {
let result = [{label: 'Нет', value: ''}];
for (let i = 1; i < 10; i++) {
result.push({label: i, value: `paper${i}`});
}
return result;
}
get fontsOptions() {
let result = [];
this.fonts.forEach(font => {
result.push({label: (font.label ? font.label : font.name), value: font.name});
});
return result;
}
get webFontsOptions() {
let result = [{label: 'Нет', value: ''}];
this.webFonts.forEach(font => {
result.push({label: font.name, value: font.name});
});
return result;
}
get pageChangeAnimationOptions() {
let result = [
{label: 'Нет', value: ''},
{label: 'Вверх-вниз', value: 'downShift'},
{label: 'Вправо-влево', value: 'rightShift'},
{label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'},
];
if (this.wallpaper == '')
result.push({label: 'Листание', value: 'flip'});
return result;
}
get currentProfile() {
return this.$store.state.reader.currentProfile;
}
set currentProfile(newValue) {
this.commit('reader/setCurrentProfile', newValue);
}
get partialStorageKey() {
return this.serverStorageKey.substr(0, 7) + '***';
}
get serverStorageKey() {
return this.$store.state.reader.serverStorageKey;
}
get setStorageKeyLink() {
return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
}
get predefineTextColors() {
return defPalette.concat([
'#ffffff',
'#000000',
'#202020',
'#323232',
'#aaaaaa',
'#00c0c0',
'#ebe2c9',
'#cfdc99',
'#478355',
'#909080',
]);
}
get predefineBackgroundColors() {
return defPalette.concat([
'#ffffff',
'#000000',
'#202020',
'#ebe2c9',
'#cfdc99',
'#478355',
'#a6caf0',
'#909080',
'#808080',
'#c8c8c8',
]);
}
colorPanStyle(type) {
let result = 'width: 30px; height: 30px; border: 1px solid black; border-radius: 4px;';
switch (type) {
case 'text':
result += `background-color: ${this.textColor};`
break;
case 'bg':
result += `background-color: ${this.backgroundColor};`
break;
}
return result;
}
needReload() {
this.$root.notify.warning('Необходимо обновить страницу (F5), чтобы изменения возымели эффект');
}
needTextReload() {
this.$root.notify.warning('Необходимо обновить книгу в обход кэша, чтобы изменения возымели эффект');
}
close() { close() {
this.$emit('do-action', {action: 'settings'}); this.$emit('do-action', {action: 'settings'});
} }
@@ -370,153 +165,23 @@ class SettingsPage extends Vue {
async setDefaults() { async setDefaults() {
try { try {
if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) { if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) {
this.form = Object.assign({}, rstore.settingDefaults); this.form = _.cloneDeep(rstore.settingDefaults);
for (let prop in rstore.settingDefaults) {
this[prop] = this.form[prop];
}
} }
} catch (e) { } catch (e) {
// //
} }
} }
changeShowToolButton(buttonName) { tabEvent(event) {
this.showToolButton = Object.assign({}, this.showToolButton, {[buttonName]: !this.showToolButton[buttonName]}); if (!event || !event.action)
}
async addProfile() {
try {
if (Object.keys(this.profiles).length >= 100) {
this.$root.stdDialog.alert('Достигнут предел количества профилей', 'Ошибка');
return;
}
const result = await this.$root.stdDialog.prompt('Введите произвольное название для профиля устройства:', ' ', {
inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
});
if (result && result.value) {
if (this.profiles[result.value]) {
this.$root.stdDialog.alert('Такой профиль уже существует', 'Ошибка');
} else {
const newProfiles = Object.assign({}, this.profiles, {[result.value]: 1});
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', newProfiles);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = result.value;
}
}
} catch (e) {
//
}
}
async delProfile() {
if (!this.currentProfile)
return; return;
try { switch (event.action) {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` + case 'set-defaults': this.setDefaults(); break;
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` + case 'night-mode': this.$emit('do-action', {action: 'nightMode'}); break;
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
if (this.profiles[this.currentProfile]) {
const newProfiles = Object.assign({}, this.profiles);
delete newProfiles[this.currentProfile];
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', newProfiles);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = '';
}
}
} catch (e) {
//
} }
} }
async delAllProfiles() {
if (!Object.keys(this.profiles).length)
return;
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление ВСЕХ профилей с настройками необратимо.` +
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.commit('reader/setAllowProfilesSave', true);
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setProfiles', {});
await this.$nextTick();//ждем обработчики watch
this.commit('reader/setAllowProfilesSave', false);
this.currentProfile = '';
}
} catch (e) {
//
}
}
async copyToClip(text, prefix) {
const result = await utils.copyTextToClipboard(text);
const suf = (prefix.substr(-1) == 'а' ? 'а' : '');
const msg = (result ? `${prefix} успешно скопирован${suf} в буфер обмена` : 'Копирование не удалось');
if (result)
this.$root.notify.success(msg);
else
this.$root.notify.error(msg);
}
async showServerStorageKey() {
this.serverStorageKeyVisible = !this.serverStorageKeyVisible;
}
async enterServerStorageKey(key) {
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Изменение ключа доступа приведет к замене всех профилей и читаемых книг в читалке.` +
`<br><br>Введите новый ключ доступа:`, ' ', {
inputValidator: (str) => {
try {
if (str && utils.fromBase58(str).length == 32) {
return true;
}
} catch (e) {
//
}
return 'Неверный формат ключа';
},
inputValue: (key && _.isString(key) ? key : null),
});
if (result && result.value && utils.fromBase58(result.value).length == 32) {
this.commit('reader/setServerStorageKey', result.value);
}
} catch (e) {
//
}
}
async generateServerStorageKey() {
try {
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Генерация нового ключа доступа приведет к удалению всех профилей и читаемых книг в читалке.` +
`<br><br>Введите 'да' для подтверждения генерации нового ключа:`, ' ', {
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
});
if (result && result.value && result.value.toLowerCase() == 'да') {
this.$root.$emit('generateNewServerStorageKey');
}
} catch (e) {
//
}
}
keyHook(event) { keyHook(event) {
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') { if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
this.close(); this.close();
@@ -524,6 +189,8 @@ class SettingsPage extends Vue {
return true; return true;
} }
} }
export default vueComponent(SettingsPage);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
@@ -531,15 +198,17 @@ class SettingsPage extends Vue {
.tab { .tab {
justify-content: initial; justify-content: initial;
} }
</style>
.tab-panel { <style>
.sets-tab-panel {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
font-size: 90%; font-size: 90%;
padding: 0 10px 15px 10px; padding: 0 10px 15px 10px;
} }
.part-header { .sets-part-header {
border-top: 2px solid #bbbbbb; border-top: 2px solid #bbbbbb;
font-weight: bold; font-weight: bold;
font-size: 110%; font-size: 110%;
@@ -547,25 +216,7 @@ class SettingsPage extends Vue {
margin-bottom: 5px; margin-bottom: 5px;
} }
.item { .sets-label {
width: 100%;
margin-top: 5px;
margin-bottom: 5px;
}
.label-1, .label-7 {
width: 75px;
}
.label-2, .label-3, .label-4, .label-5 {
width: 110px;
}
.label-6 {
width: 100px;
}
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@@ -574,33 +225,14 @@ class SettingsPage extends Vue {
overflow: hidden; overflow: hidden;
} }
.text { .sets-item {
font-size: 90%; width: 100%;
line-height: 130%; margin-top: 5px;
margin-bottom: 5px;
} }
.button { .sets-button {
margin: 3px 15px 3px 0; margin: 3px 15px 3px 0;
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
} }
.copy-icon {
margin-left: 5px;
cursor: pointer;
font-size: 120%;
color: blue;
}
.input {
max-width: 150px;
}
.no-mp {
margin: 0;
padding: 0;
}
.col-left {
width: 150px;
}
</style> </style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="fit sets-tab-panel">
<div class="sets-part-header">
Отображение
</div>
<div class="item row no-wrap">
<div class="sets-label label"></div>
<q-checkbox v-model="form.toolBarMultiLine" size="xs" label="Многострочная панель">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Размещать кнопки на панели в несколько рядов, если они не помещаются в одну строку
</q-tooltip>
</q-checkbox>
</div>
<div class="item row no-wrap">
<div class="sets-label label"></div>
<q-checkbox v-model="form.toolBarHideOnScroll" size="xs" label="Скрывать/показывать панель при прокрутке">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Скрывать/показывть панель при прокрутке текста вперед/назад
</q-tooltip>
</q-checkbox>
</div>
<div class="sets-part-header">
Показывать кнопки
</div>
<div v-for="item in rstore.toolButtons" :key="item.name">
<div class="sets-item row no-wrap">
<div class="sets-label label"></div>
<q-checkbox v-model="form.showToolButton[item.name]" size="xs" :label="rstore.readerActions[item.name]" />
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import rstore from '../../../../store/modules/reader';
const componentOptions = {
watch: {
},
};
class ToolBarTab {
_options = componentOptions;
_props = {
form: Object,
};
rstore = rstore;
created() {
}
mounted() {
}
get mode() {
return this.$store.state.config.mode;
}
}
export default vueComponent(ToolBarTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 75px;
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="fit sets-tab-panel">
<!---------------------------------------------->
<div class="sets-part-header">
Обновление читалки
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<q-checkbox v-model="form.showNeedUpdateNotify" size="xs">
Проверять наличие новой версии
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Напоминать о необходимости обновления страницы<br>
при появлении новой версии читалки
</q-tooltip>
</q-checkbox>
</div>
<!---------------------------------------------->
<div class="sets-part-header">
Обновление книг
</div>
<div v-show="!configBucEnabled" class="sets-item row">
<div class="sets-label label"></div>
<div>Сервер обновлений временно не работает</div>
</div>
<div v-show="configBucEnabled" class="sets-item row">
<div class="sets-label label"></div>
<q-checkbox v-model="form.bucEnabled" size="xs">
Проверять обновления книг
</q-checkbox>
</div>
<div v-show="configBucEnabled && form.bucEnabled" class="sets-item row">
<div class="sets-label label"></div>
<div class="col-4 column justify-center items-end q-pr-xs">
Разница размеров
</div>
<div class="col row">
<NumInput v-model="form.bucSizeDiff" bg-color="input" style="width: 200px" />
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Уведомлять о наличии обновления книги в списке загруженных<br>
при указанной разнице в размерах старого и нового файлов.<br>
Разница указывается в байтах и может быть отрицательной.
</q-tooltip>
</div>
</div>
<div v-show="configBucEnabled && form.bucEnabled" class="sets-item row">
<div class="sets-label label"></div>
<q-checkbox v-model="form.bucSetOnNew" size="xs">
Автопроверка для вновь загружаемых
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Автоматически устанавливать флаг проверки<br>
обновлений для всех вновь загружаемых книг
</q-tooltip>
</q-checkbox>
</div>
<div v-show="configBucEnabled && form.bucEnabled" class="sets-item row">
<div class="sets-label label"></div>
<q-checkbox v-model="form.bucCancelEnabled" size="xs">
Отменять проверку через {{ form.bucCancelDays }} дней{{ (form.bucCancelEnabled ? ':' : '') }}
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Снимать флаг проверки с книги, если не было<br>
обновлений в течение {{ form.bucCancelDays }} дней
</q-tooltip>
</q-checkbox>
</div>
<div v-show="configBucEnabled && form.bucEnabled && form.bucCancelEnabled" class="sets-item row">
<div class="sets-label label"></div>
<div class="col-4"></div>
<div class="col row">
<NumInput v-model="form.bucCancelDays" bg-color="input" :min="1" :max="10000" />
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Снимать флаг проверки с книги, если не было<br>
обновлений в течение {{ form.bucCancelDays }} дней
</q-tooltip>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import NumInput from '../../../share/NumInput.vue';
const componentOptions = {
components: {
NumInput
},
};
class UpdateTab {
_options = componentOptions;
_props = {
form: Object,
};
created() {
}
mounted() {
}
get configBucEnabled() {
return this.$store.state.config.bucEnabled;
}
}
export default vueComponent(UpdateTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 100px;
}
</style>

View File

@@ -0,0 +1,332 @@
<template>
<div>
<!---------------------------------------------->
<div class="hidden sets-part-header">
Цвет
</div>
<div class="sets-item row">
<div class="sets-label label">
Текст
</div>
<div class="col row">
<q-input
v-model="textColorFiltered"
class="col-left no-mp"
bg-color="input"
outlined dense
:rules="['hexColor']"
style="max-width: 150px"
>
<template #prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.textColor)">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color
v-model="form.textColor"
no-header default-view="palette" :palette="defPalette.predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="q-mt-md" />
<div class="sets-item row">
<div class="sets-label label">
Фон
</div>
<div class="col row">
<q-input
v-model="bgColorFiltered"
class="col-left no-mp"
bg-color="input"
outlined dense
:rules="['hexColor']"
style="max-width: 150px"
>
<template #prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.backgroundColor)">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color v-model="form.backgroundColor" no-header default-view="palette" :palette="defPalette.predefineBackgroundColors" />
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="q-mt-md" />
<div class="sets-item row">
<div class="sets-label label">
Обои
</div>
<div class="col row items-center">
<q-select
v-model="form.wallpaper"
class="col-left no-mp"
:options="wallpaperOptions"
bg-color="input"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<template #selected-item="scope">
<div>
{{ scope.opt.label }}
</div>
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
</template>
<template #option="scope">
<q-item
v-bind="scope.itemProps"
>
<q-item-section style="min-width: 50px;">
<q-item-label>
{{ scope.opt.label }}
</q-item-label>
</q-item-section>
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
</q-item>
</template>
</q-select>
<div class="q-px-xs" />
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить файл обоев
</q-tooltip>
</q-btn>
<q-btn v-show="form.wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Удалить выбранные обои
</q-tooltip>
</q-btn>
<q-btn v-show="form.wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Скачать выбранные обои
</q-tooltip>
</q-btn>
</div>
</div>
<div class="q-mt-sm" />
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col row items-center">
<q-checkbox v-model="form.wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
</div>
</div>
<input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />
<a ref="download" style="display: none;" target="_blank"></a>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../../vueComponent.js';
import _ from 'lodash';
import * as helper from '../helper';
import defPalette from '../defPalette';
import * as utils from '../../../../../share/utils';
import * as cryptoUtils from '../../../../../share/cryptoUtils';
import wallpaperStorage from '../../../share/wallpaperStorage';
import readerApi from '../../../../../api/reader';
const componentOptions = {
components: {
},
watch: {
form: {
handler() {
this.formChanged();//no await
},
deep: true,
},
textColorFiltered(newValue) {
if (!this.isFormChanged && this.helper.isHexColor(newValue))
this.form.textColor = newValue;
},
bgColorFiltered(newValue) {
if (!this.isFormChanged && this.helper.isHexColor(newValue))
this.form.backgroundColor = newValue;
},
},
};
class Color {
_options = componentOptions;
_props = {
form: Object,
};
helper = helper;
defPalette = defPalette;
isFormChanged = false;
textColorFiltered = '';
bgColorFiltered = '';
created() {
this.formChanged();//no await
}
mounted() {
}
async formChanged() {
this.isFormChanged = true;
try {
this.textColorFiltered = this.form.textColor;
this.bgColorFiltered = this.form.backgroundColor;
if (this.form.wallpaper != '' && this.form.pageChangeAnimation == 'flip')
this.form.pageChangeAnimation = '';
} finally {
await this.$nextTick();
this.isFormChanged = false;
}
}
get wallpaperOptions() {
let result = [{label: 'Нет', value: ''}];
const userWallpapers = _.cloneDeep(this.form.userWallpapers);
userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
for (const wp of userWallpapers) {
if (wallpaperStorage.keyExists(wp.cssClass))
result.push({label: wp.label, value: wp.cssClass});
}
for (let i = 1; i <= 17; i++) {
result.push({label: i, value: `paper${i}`});
}
return result;
}
loadWallpaperFileClick() {
this.$refs.file.click();
}
loadWallpaperFile() {
const file = this.$refs.file.files[0];
if (file.size > 10*1024*1024) {
this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
return;
}
if (file.type != 'image/png' && file.type != 'image/jpeg') {
this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
return;
}
if (this.form.userWallpapers.length >= 100) {
this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
return;
}
this.$refs.file.value = '';
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
(async() => {
const data = e.target.result;
const key = utils.toHex(cryptoUtils.sha256(data));
const label = `#${key.substring(0, 4)}`;
const cssClass = `user-paper${key}`;
const newUserWallpapers = _.cloneDeep(this.form.userWallpapers);
const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
if (index < 0)
newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass)) {
await wallpaperStorage.setData(cssClass, data);
//отправим data на сервер в файл `/upload/${key}`
try {
//const res =
await readerApi.uploadFileBuf(data);
//console.log(res);
} catch (e) {
console.error(e);
}
}
this.form.userWallpapers = newUserWallpapers;
this.form.wallpaper = cssClass;
})();
}
reader.readAsDataURL(file);
}
}
async delWallpaper() {
if (this.form.wallpaper.indexOf('user-paper') == 0) {
const newUserWallpapers = [];
for (const wp of this.form.userWallpapers) {
if (wp.cssClass != this.form.wallpaper) {
newUserWallpapers.push(wp);
}
}
await wallpaperStorage.removeData(this.form.wallpaper);
this.form.userWallpapers = newUserWallpapers;
this.form.wallpaper = '';
}
}
async downloadWallpaper() {
if (this.form.wallpaper.indexOf('user-paper') != 0)
return;
try {
const d = this.$refs.download;
const dataUrl = await wallpaperStorage.getData(this.form.wallpaper);
if (!dataUrl)
throw new Error('Файл обоев не найден');
d.href = dataUrl;
d.download = `wallpaper-#${this.form.wallpaper.replace('user-paper', '').substring(0, 4)}`;
d.click();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
}
}
export default vueComponent(Color);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
.col-left {
width: 145px;
}
.no-mp {
margin: 0;
padding: 0;
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<div>
<!---------------------------------------------->
<div class="hidden sets-part-header">
Шрифт
</div>
<div class="sets-item row">
<div class="sets-label label">
Локальный/веб
</div>
<div class="col row">
<q-select
v-model="form.fontName" class="col-left" bg-color="input" :options="fontsOptions" :disable="form.webFontName != ''"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
/>
<div class="q-px-sm" />
<q-select
v-model="form.webFontName" class="col" bg-color="input" :options="webFontsOptions"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Веб шрифты дают большое разнообразие,<br>
однако есть шанс, что шрифт будет загружаться<br>
очень медленно или вовсе не загрузится
</q-tooltip>
</q-select>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Размер
</div>
<div class="col row">
<NumInput v-model="form.fontSize" bg-color="input" class="col-left" :min="5" :max="200" />
<div class="col q-pt-xs text-right">
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
</div>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Сдвиг
</div>
<div class="col row">
<NumInput v-model="vertShift" bg-color="input" class="col-left" :min="-100" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сдвиг шрифта по вертикали в процентах от размера.<br>
Отрицательное значение сдвигает вверх, положительное -<br>
вниз. Значение зависит от метрики шрифта.
</q-tooltip>
</NumInput>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Стиль
</div>
<div class="col row">
<q-checkbox v-model="fontBold" size="xs" label="Жирный" />
<q-checkbox v-model="fontItalic" class="q-ml-sm" size="xs" label="Курсив" />
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../../vueComponent.js';
import NumInput from '../../../../share/NumInput.vue';
import rstore from '../../../../../store/modules/reader';
const componentOptions = {
components: {
NumInput,
},
watch: {
form: {
handler() {
this.formChanged();//no await
},
deep: true,
},
fontBold: function(newValue) {
if (!this.isFormChanged)
this.form.fontWeight = (newValue ? 'bold' : '');
},
fontItalic: function(newValue) {
if (!this.isFormChanged)
this.form.fontStyle = (newValue ? 'italic' : '');
},
vertShift: function(newValue) {
if (!this.isFormChanged) {
const font = (this.form.webFontName ? this.form.webFontName : this.form.fontName);
if (this.form.fontShifts[font] != newValue || this.form.fontVertShift != newValue) {
this.form.fontShifts = Object.assign({}, this.form.fontShifts, {[font]: newValue});
this.form.fontVertShift = newValue;
}
}
},
},
};
class Font {
_options = componentOptions;
_props = {
form: Object,
};
fontBold = false;
fontItalic = false;
vertShift = 0;
webFonts = [];
fonts = [];
created() {
this.formChanged();//no await
}
mounted() {
}
async formChanged() {
this.isFormChanged = true;
try {
this.fontBold = (this.form.fontWeight == 'bold');
this.fontItalic = (this.form.fontStyle == 'italic');
this.fonts = rstore.fonts;
this.webFonts = rstore.webFonts;
const font = (this.form.webFontName ? this.form.webFontName : this.form.fontName);
this.vertShift = this.form.fontShifts[font] || 0;
} finally {
await this.$nextTick();
this.isFormChanged = false;
}
}
get fontsOptions() {
let result = [];
this.fonts.forEach(font => {
result.push({label: (font.label ? font.label : font.name), value: font.name});
});
return result;
}
get webFontsOptions() {
let result = [{label: 'Нет', value: ''}];
this.webFonts.forEach(font => {
result.push({label: font.name, value: font.name});
});
return result;
}
}
export default vueComponent(Font);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
.col-left {
width: 145px;
}
</style>

View File

@@ -0,0 +1,244 @@
<template>
<div>
<!---------------------------------------------->
<div class="hidden sets-part-header">
Режим
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col row">
<q-checkbox v-model="nightMode" size="xs" label="Ночной режим" @update:modelValue="nightModeToggle" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col row">
<q-checkbox v-model="form.dualPageMode" size="xs" label="Двухстраничный режим" />
</div>
</div>
<div class="sets-part-header">
Страницы
</div>
<div class="sets-item row">
<div class="sets-label label">
Отступ границ
</div>
<div class="col row">
<NumInput v-model="form.indentLR" bg-color="input" class="col-left" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Слева/справа от края экрана
</q-tooltip>
</NumInput>
<div class="q-px-sm" />
<NumInput v-model="form.indentTB" bg-color="input" class="col" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сверху/снизу от края экрана
</q-tooltip>
</NumInput>
</div>
</div>
<div v-show="form.dualPageMode" class="sets-item row">
<div class="sets-label label">
Отступ внутри
</div>
<div class="col row">
<NumInput v-model="form.dualIndentLR" bg-color="input" class="col-left" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Слева/справа внутри страницы
</q-tooltip>
</NumInput>
</div>
</div>
<div v-show="form.dualPageMode">
<div class="sets-part-header">
Разделитель
</div>
<div class="sets-item row no-wrap">
<div class="sets-label label">
Цвет
</div>
<div class="col-left row">
<q-input
v-model="dualDivColorFiltered"
class="col-left no-mp"
bg-color="input"
outlined dense
:rules="['hexColor']"
style="max-width: 150px"
:disable="form.dualDivColorAsText"
>
<template #prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.dualDivColor)">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color
v-model="form.dualDivColor"
no-header default-view="palette" :palette="defPalette.predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="q-px-xs" />
<q-checkbox v-model="form.dualDivColorAsText" size="xs" label="Как у текста" />
</div>
<div class="sets-item row">
<div class="sets-label label">
Прозрачность
</div>
<div class="col row">
<NumInput v-model="form.dualDivColorAlpha" bg-color="input" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Ширина (px)
</div>
<div class="col row">
<NumInput v-model="form.dualDivWidth" bg-color="input" class="col-left" :min="0" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Ширина разделителя
</q-tooltip>
</NumInput>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Высота (%)
</div>
<div class="col row">
<NumInput v-model="form.dualDivHeight" bg-color="input" class="col-left" :min="0" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Высота разделителя
</q-tooltip>
</NumInput>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Пунктир
</div>
<div class="col row">
<NumInput v-model="form.dualDivStrokeFill" bg-color="input" class="col-left" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Заполнение пунктира
</q-tooltip>
</NumInput>
<div class="q-px-sm" />
<NumInput v-model="form.dualDivStrokeGap" bg-color="input" class="col" :min="0" :max="2000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Промежуток пунктира
</q-tooltip>
</NumInput>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Ширина тени
</div>
<div class="col row">
<NumInput v-model="form.dualDivShadowWidth" bg-color="input" class="col-left" :min="0" :max="100" />
</div>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../../vueComponent.js';
import NumInput from '../../../../share/NumInput.vue';
import * as helper from '../helper';
import defPalette from '../defPalette';
const componentOptions = {
components: {
NumInput
},
watch: {
form: {
handler() {
this.formChanged();//no await
},
deep: true,
},
dualDivColorFiltered(newValue) {
if (!this.isFormChanged && this.helper.isHexColor(newValue))
this.form.dualDivColor = newValue;
},
}
};
class Mode {
_options = componentOptions;
_props = {
form: Object,
};
helper = helper;
defPalette = defPalette;
isFormChanged = false;
dualDivColorFiltered = '';
nightMode = false;
created() {
this.formChanged();//no await
}
mounted() {
}
async formChanged() {
this.isFormChanged = true;
try {
this.dualDivColorFiltered = this.form.dualDivColor;
if (this.form.dualPageMode
&& (this.form.pageChangeAnimation == 'flip' || this.form.pageChangeAnimation == 'rightShift')
)
this.form.pageChangeAnimation = '';
this.nightMode = this.form.nightMode;
} finally {
await this.$nextTick();
this.isFormChanged = false;
}
}
nightModeToggle() {
this.$emit('tab-event', {action: 'night-mode'});
}
}
export default vueComponent(Mode);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
.col-left {
width: 145px;
}
.no-mp {
margin: 0;
padding: 0;
}
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div>
<!---------------------------------------------->
<div class="hidden sets-part-header">
Строка статуса
</div>
<div class="sets-item row">
<div class="sets-label label">
Статус
</div>
<div class="col row">
<q-checkbox v-model="form.showStatusBar" size="xs" label="Показывать" />
<q-checkbox v-show="form.showStatusBar" v-model="form.statusBarTop" class="q-ml-sm" size="xs" label="Вверху/внизу" />
</div>
</div>
<div v-show="form.showStatusBar" class="sets-item row no-wrap">
<div class="sets-label label">
Цвет
</div>
<div class="col-left row">
<q-input
v-model="statusBarColorFiltered"
class="col-left no-mp"
bg-color="input"
outlined dense
:rules="['hexColor']"
style="max-width: 150px"
:disable="form.statusBarColorAsText"
>
<template #prepend>
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="helper.colorPanStyle(form.statusBarColor)">
<q-popup-proxy anchor="bottom middle" self="top middle">
<div>
<q-color
v-model="form.statusBarColor"
no-header default-view="palette" :palette="defPalette.predefineTextColors"
/>
</div>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="q-px-xs" />
<q-checkbox v-model="form.statusBarColorAsText" size="xs" label="Как у текста" />
</div>
<div v-show="form.showStatusBar" class="sets-item row">
<div class="sets-label label">
Прозрачность
</div>
<div class="col row">
<NumInput v-model="form.statusBarColorAlpha" bg-color="input" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
</div>
</div>
<div v-show="form.showStatusBar" class="sets-item row">
<div class="sets-label label">
Высота
</div>
<div class="col row">
<NumInput v-model="form.statusBarHeight" bg-color="input" class="col-left" :min="5" :max="100" />
</div>
</div>
<div v-show="form.showStatusBar" class="sets-item row">
<div class="sets-label label"></div>
<div class="col row">
<q-checkbox v-model="form.statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
По клику на автора-название в строке статуса<br>
открывать оригинал произведения в новой вкладке
</q-tooltip>
</q-checkbox>
</div>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../../vueComponent.js';
import NumInput from '../../../../share/NumInput.vue';
import * as helper from '../helper';
import defPalette from '../defPalette';
const componentOptions = {
components: {
NumInput,
},
watch: {
form: {
handler() {
this.formChanged();//no await
},
deep: true,
},
statusBarColorFiltered(newValue) {
if (!this.isFormChanged && this.helper.isHexColor(newValue))
this.form.statusBarColor = newValue;
},
},
};
class Text {
_options = componentOptions;
_props = {
form: Object,
};
helper = helper;
defPalette = defPalette;
statusBarColorFiltered = '';
created() {
this.formChanged();//no await
}
mounted() {
}
async formChanged() {
this.isFormChanged = true;
try {
this.statusBarColorFiltered = this.form.statusBarColor;
} finally {
await this.$nextTick();
this.isFormChanged = false;
}
}
}
export default vueComponent(Text);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
.col-left {
width: 145px;
}
.no-mp {
margin: 0;
padding: 0;
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<div>
<!---------------------------------------------->
<div class="hidden sets-part-header">
Текст
</div>
<div class="sets-item row">
<div class="sets-label label">
Интервал
</div>
<div class="col row">
<NumInput v-model="form.lineInterval" bg-color="input" class="col-left" :min="0" :max="200" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Параграф
</div>
<div class="col row">
<NumInput v-model="form.p" bg-color="input" class="col-left" :min="0" :max="2000" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Сдвиг
</div>
<div class="col row">
<NumInput v-model="form.textVertShift" bg-color="input" class="col-left" :min="-100" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
Отрицательное значение сдвигает вверх, положительное -<br>
вниз.
</q-tooltip>
</NumInput>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Скроллинг
</div>
<div class="col row">
<NumInput v-model="form.scrollingDelay" bg-color="input" class="col-left" :min="1" :max="10000">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Замедление скроллинга в миллисекундах.<br>
Определяет время, за которое текст<br>
прокручивается на одну строку.
</q-tooltip>
</NumInput>
<div class="q-px-sm" />
<q-select
v-model="form.scrollingType" bg-color="input" class="col" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options
>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Вид скроллинга: линейный,<br>
ускорение-замедление и пр.
</q-tooltip>
</q-select>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label">
Выравнивание
</div>
<div class="col row">
<q-checkbox v-model="form.textAlignJustify" size="xs" label="По ширине" />
<q-checkbox v-model="form.wordWrap" class="q-ml-sm" size="xs" label="Перенос по слогам" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col-left column justify-center text-right">
Компактность
</div>
<div class="q-px-sm" />
<NumInput v-model="form.compactTextPerc" bg-color="input" class="col" :min="0" :max="100">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Степень компактности текста в процентах.<br>
Чем больше компактность, тем хуже выравнивание<br>
по правому краю.
</q-tooltip>
</NumInput>
</div>
<div class="sets-item row">
<div class="sets-label label">
Обработка
</div>
<div class="col row">
<q-checkbox v-model="form.cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col-left column justify-center text-right">
Добавлять пустые
</div>
<div class="q-px-sm" />
<NumInput v-model="form.addEmptyParagraphs" bg-color="input" class="col" :min="0" :max="2" />
</div>
<div class="sets-item row">
<div class="sets-label label">
Изображения
</div>
<div class="col row">
<q-checkbox v-model="form.showImages" size="xs" label="Показывать" />
<q-checkbox v-model="form.showInlineImagesInCenter" class="q-ml-sm" :disable="!form.showImages" size="xs" label="Инлайн в центр" @update:modelValue="needReload">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Выносить все изображения в центр экрана
</q-tooltip>
</q-checkbox>
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col row">
<q-checkbox v-model="form.imageFitWidth" size="xs" label="Ширина не более размера страницы" :disable="!form.showImages || form.dualPageMode" />
</div>
</div>
<div class="sets-item row">
<div class="sets-label label"></div>
<div class="col-left column justify-center text-right">
Высота не более
</div>
<div class="q-px-sm" />
<NumInput v-model="form.imageHeightLines" bg-color="input" class="col" :min="1" :max="100" :disable="!form.showImages">
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Определяет высоту изображения количеством строк.<br>
В случае превышения высоты, изображение будет<br>
уменьшено с сохранением пропорций так, чтобы<br>
помещаться в указанное количество строк.
</q-tooltip>
</NumInput>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../../vueComponent.js';
import NumInput from '../../../../share/NumInput.vue';
const componentOptions = {
components: {
NumInput,
},
watch: {
form: {
handler() {
this.formChanged();//no await
},
deep: true,
},
},
};
class Text {
_options = componentOptions;
_props = {
form: Object,
};
statusBarColorFiltered = '';
created() {
this.formChanged();//no await
}
mounted() {
}
async formChanged() {
this.isFormChanged = true;
try {
//
} finally {
await this.$nextTick();
this.isFormChanged = false;
}
}
needReload() {
this.$root.notify.warning('Необходимо обновить страницу (F5), чтобы изменения возымели эффект');
}
}
export default vueComponent(Text);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 110px;
}
.col-left {
width: 145px;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="fit column">
<q-tabs
v-model="selectedTab"
active-color="app"
active-bg-color="app"
indicator-color="bg-app"
dense
no-caps
class="no-mp bg-menu-2 text-menu"
>
<q-tab name="mode" label="Режим" />
<q-tab name="color" label="Цвет" />
<q-tab name="font" label="Шрифт" />
<q-tab name="text" label="Текст" />
<q-tab name="status" label="Строка статуса" />
</q-tabs>
<div class="q-mb-sm" />
<div class="col sets-tab-panel">
<Mode v-if="selectedTab == 'mode'" :form="form" @tab-event="tabEvent" />
<Color v-if="selectedTab == 'color'" :form="form" />
<Font v-if="selectedTab == 'font'" :form="form" />
<Text v-if="selectedTab == 'text'" :form="form" />
<Status v-if="selectedTab == 'status'" :form="form" />
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import Mode from './Mode/Mode.vue';
import Color from './Color/Color.vue';
import Font from './Font/Font.vue';
import Text from './Text/Text.vue';
import Status from './Status/Status.vue';
const componentOptions = {
components: {
Mode,
Color,
Font,
Text,
Status,
},
};
class ViewTab {
_options = componentOptions;
_props = {
form: Object,
};
selectedTab = 'mode';
created() {
}
mounted() {
}
tabEvent(event) {
if (!event || !event.action)
return;
switch (event.action) {
case 'night-mode': this.$emit('tab-event', {action: 'night-mode'}); break;
}
}
}
export default vueComponent(ViewTab);
//-----------------------------------------------------------------------------
</script>
<style scoped>
.label {
width: 75px;
}
</style>

View File

@@ -14,4 +14,32 @@ const defPalette = [
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)' 'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
]; ];
export default defPalette; export default {
predefinePalette: defPalette,
predefineTextColors: defPalette.concat([
'#ffffff',
'#000000',
'#202020',
'#323232',
'#aaaaaa',
'#00c0c0',
'#ebe2c9',
'#cfdc99',
'#478355',
'#909080',
]),
predefineBackgroundColors: defPalette.concat([
'#ffffff',
'#000000',
'#202020',
'#ebe2c9',
'#cfdc99',
'#478355',
'#a6caf0',
'#909080',
'#808080',
'#c8c8c8',
]),
};

View File

@@ -0,0 +1,9 @@
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
export function colorPanStyle(bgColor) {
return `width: 30px; height: 30px; border: 1px solid black; border-radius: 4px; background-color: ${bgColor}`;
}
export function isHexColor(value) {
return hex.test(value);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -3,24 +3,58 @@ import sax from '../../../../server/core/sax';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
const maxImageLineCount = 100; const maxImageLineCount = 100;
const maxParaLength = 10000;
const maxParaTextLength = 10000;
// defaults
const defaultSettings = {
p: 30, //px, отступ параграфа
w: 500, //px, ширина страницы
font: '', //css описание шрифта
fontSize: 20, //px, размер шрифта
wordWrap: false, //перенос по слогам
cutEmptyParagraphs: false, //убирать пустые параграфы
addEmptyParagraphs: 0, //добавлять n пустых параграфов перед непустым
maxWordLength: 500, //px, максимальная длина слова без пробелов
lineHeight: 26, //px, высота строки
showImages: true, //показыввать изображения
showInlineImagesInCenter: true, //выносить изображения в центр, работает на этапе первичного парсинга (parse)
imageHeightLines: 100, //кол-во строк, максимальная высота изображения
imageFitWidth: true, //ширина изображения не более ширины страницы
dualPageMode: false, //двухстраничный режим
compactTextPerc: 0, //проценты, степень компактности текста
testWidth: 0, //ширина тестовой строки, пересчитывается извне при изменении шрифта браузером
isTesting: false, //тестовый режим
//заглушка, измеритель ширины текста
measureText: (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
},
};
//for splitToSlogi()
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
const soglas = new Set([
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
]);
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
const alpha = new Set([...glas, ...soglas, ...znak]);
export default class BookParser { export default class BookParser {
constructor(settings) { constructor(settings = {}) {
if (settings) { this.sets = {};
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
}
// defaults this.setSettings(defaultSettings);
this.p = 30;// px, отступ параграфа this.setSettings(settings);
this.w = 300;// px, ширина страницы
this.wordWrap = false;// перенос по слогам
//заглушка
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
return text.length*20;
};
} }
setSettings(settings = {}) {
this.sets = Object.assign({}, this.sets, settings);
this.measureText = this.sets.measureText;
}
async parse(data, callback) { async parse(data, callback) {
if (!callback) if (!callback)
callback = () => {}; callback = () => {};
@@ -51,17 +85,25 @@ export default class BookParser {
let binaryId = ''; let binaryId = '';
let binaryType = ''; let binaryType = '';
let dimPromises = []; let dimPromises = [];
this.coverPageId = '';
this.images = [];
let imageNum = 0;
//примечания
this.notes = {};
let inNote = false;
let noteId = '';
let inNotesBody = false;
const noteTags = new Set(['p', 'poem', 'stanza', 'v', 'text-author', 'emphasis']);
//оглавление //оглавление
this.contents = []; this.contents = [];
this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []}; let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''}; let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false; let inTitle = false;
let inSubtitle = false; let inSubtitle = false;
let sectionLevel = 0; let sectionLevel = 0;
let bodyIndex = 0; let bodyIndex = 0;
let imageNum = 0;
let paraIndex = -1; let paraIndex = -1;
let paraOffset = 0; let paraOffset = 0;
@@ -76,6 +118,7 @@ export default class BookParser {
*/ */
const getImageDimensions = (binaryId, binaryType, data) => { const getImageDimensions = (binaryId, binaryType, data) => {
return new Promise ((resolve, reject) => { (async() => { return new Promise ((resolve, reject) => { (async() => {
data = data.replace(/[\n\r\s]/g, '');
const i = new Image(); const i = new Image();
let resolved = false; let resolved = false;
i.onload = () => { i.onload = () => {
@@ -120,14 +163,67 @@ export default class BookParser {
})().catch(reject); }); })().catch(reject); });
}; };
const newParagraph = (text, len, addIndex) => { const correctCurrentPara = () => {
//коррекция текущего параграфа
if (paraIndex >= 0) {
const prevParaIndex = paraIndex;
let p = para[paraIndex];
paraOffset -= p.length;
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
let newParaText = p.text.trim();
newParaText = (newParaText.length ? newParaText : ' ');
const ldiff = p.text.length - newParaText.length;
if (ldiff != 0) {
p.text = newParaText;
p.length -= ldiff;
}
//удаление параграфов, которые содержат только разметку, такого не должно быть
if (!p.length) {
delete para[paraIndex];
paraIndex--;
return;
}
//добавление пустых (не)видимых (addEmptyParagraphs) параграфов перед текущим непустым
if (p.text.trim() != '') {
for (let i = 0; i < 2; i++) {
para[paraIndex] = {
index: paraIndex,
offset: paraOffset,
length: 1,
text: ' ',
addIndex: i + 1,
};
paraIndex++;
paraOffset++;
}
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
}
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
paraOffset += p.length;
}
};
const newParagraph = (text = '', len = 0) => {
correctCurrentPara();
//новый параграф
paraIndex++; paraIndex++;
let p = { let p = {
index: paraIndex, index: paraIndex,
offset: paraOffset, offset: paraOffset,
length: len, length: len,//длина текста внутри параграфа без учета длины разметки
text: text, text: text,
addIndex: (addIndex ? addIndex : 0), addIndex: 0,
}; };
if (inSubtitle) { if (inSubtitle) {
@@ -137,53 +233,47 @@ export default class BookParser {
} }
para[paraIndex] = p; para[paraIndex] = p;
paraOffset += p.length; paraOffset += len;
}; };
const growParagraph = (text, len) => { const growParagraph = (text, len, textRaw) => {
//начальный параграф
if (paraIndex < 0) { if (paraIndex < 0) {
newParagraph(' ', 1); newParagraph();
growParagraph(text, len); growParagraph(text, len);
return; return;
} }
const prevParaIndex = paraIndex; //ограничение на размер куска текста в параграфе
let p = para[paraIndex]; if (textRaw && textRaw.length > maxParaTextLength) {
paraOffset -= p.length; while (textRaw.length > 0) {
//добавление пустых (addEmptyParagraphs) параграфов перед текущим const textPart = textRaw.substring(0, maxParaTextLength);
if (p.length == 1 && p.text[0] == ' ' && len > 0) { textRaw = textRaw.substring(maxParaTextLength);
paraIndex--;
for (let i = 0; i < 2; i++) { newParagraph();
newParagraph(' ', 1, i + 1); growParagraph(textPart, textPart.length);
} }
return;
paraIndex++;
p.index = paraIndex;
p.offset = paraOffset;
para[paraIndex] = p;
if (curTitle.paraIndex == prevParaIndex)
curTitle.paraIndex = paraIndex;
if (curSubtitle.paraIndex == prevParaIndex)
curSubtitle.paraIndex = paraIndex;
//уберем начальный пробел
p.length = 0;
p.text = p.text.substr(1);
} }
p.length += len;
p.text += text;
if (inSubtitle) { if (inSubtitle) {
curSubtitle.title += text; curSubtitle.title += text;
} else if (inTitle) { } else if (inTitle) {
curTitle.title += text; curTitle.title += text;
} }
para[paraIndex] = p; const p = para[paraIndex];
paraOffset += p.length;
//ограничение на размер параграфа
if (p.length > maxParaLength) {
newParagraph();
growParagraph(text, len);
return;
}
p.length += len;
p.text += text;
paraOffset += len;
}; };
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
@@ -196,8 +286,8 @@ export default class BookParser {
if (tag == 'binary') { if (tag == 'binary') {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : ''); binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
binaryType = (binaryType == 'image/jpg' ? 'image/jpeg' : binaryType); binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream') if (binaryType == 'image/jpeg' || binaryType == 'image/png')
binaryId = (attrs.id.value ? attrs.id.value : ''); binaryId = (attrs.id.value ? attrs.id.value : '');
} }
@@ -206,23 +296,32 @@ export default class BookParser {
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
const href = attrs.href.value; const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : ''); const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href); const {id, local} = this.hrefToId(href);
if (href[0] == '#') {//local if (local) {//local
imageNum++; imageNum++;
if (inPara && !this.showInlineImagesInCenter && !center) if (inPara && !this.sets.showInlineImagesInCenter && !center)
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0); growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
else else
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount); newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
this.images.push({paraIndex, num: imageNum, id, local, alt}); this.images.push({paraIndex, num: imageNum, id, local, alt});
if (inPara && this.showInlineImagesInCenter) if (inPara && this.sets.showInlineImagesInCenter)
newParagraph(' ', 1); newParagraph();
//coverpage
if (path == '/fictionbook/description/title-info/coverpage/image') {
this.coverPageId = id;
}
} else {//external } else {//external
imageNum++; imageNum++;
dimPromises.push(getExternalImageDimensions(href)); if (!this.sets.isTesting) {
dimPromises.push(getExternalImageDimensions(href));
} else {
dimPromises.push(this.sets.getExternalImageDimensions(this, href));
}
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount); newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
this.images.push({paraIndex, num: imageNum, id, local, alt}); this.images.push({paraIndex, num: imageNum, id, local, alt});
@@ -230,6 +329,23 @@ export default class BookParser {
} }
} }
if (tag == 'a') {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value && attrs.type && attrs.type.value === 'note') {//note
const href = attrs.href.value;
const {id, local} = this.hrefToId(href);
if (local) {
inNote = true;
growParagraph(`<note href="${id}" orig="1">`, 0);
if (!this.notes[id]) {
this.notes[id] = {id, linkParaIndex: paraIndex};
}
}
}
}
if (path == '/fictionbook/description/title-info/author') { if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author) if (!fb2.author)
fb2.author = []; fb2.author = [];
@@ -258,31 +374,61 @@ export default class BookParser {
if (path.indexOf('/fictionbook/body') == 0) { if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') { if (tag == 'body') {
let attrs = sax.getAttrsSync(tail);
if (attrs.name && attrs.name.value === 'notes') {//notes
inNotesBody = true;
}
if (isFirstBody && fb2.annotation) { if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v)); const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => { ann.forEach(a => {
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length); newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
}); });
if (ann.length) if (ann.length)
newParagraph(' ', 1); newParagraph();
} }
if (isFirstBody && fb2.sequence && fb2.sequence.length) { if (isFirstBody && fb2.sequence && fb2.sequence.length) {
const bt = utils.getBookTitle(fb2); const bt = utils.getBookTitle(fb2);
if (bt.sequence) { if (bt.sequence) {
newParagraph(bt.sequence, bt.sequence.length); newParagraph(bt.sequence, bt.sequence.length);
newParagraph(' ', 1); newParagraph();
} }
} }
if (!isFirstBody) if (!isFirstBody)
newParagraph(' ', 1); newParagraph();
isFirstBody = false; isFirstBody = false;
bodyIndex++; bodyIndex++;
} }
if (tag == 'section') {
if (!isFirstSection)
newParagraph();
isFirstSection = false;
sectionLevel++;
if (inNotesBody) {
let attrs = sax.getAttrsSync(tail);
if (attrs.id && attrs.id.value) {//notes
const id = attrs.id.value;
let note = this.notes[id];
if (!note) {
note = {id};
this.notes[id] = note;
}
note.noteParaIndex = paraIndex;
note.xml = '';
note.title = '';
noteId = id;
}
}
}
if (tag == 'title') { if (tag == 'title') {
newParagraph(' ', 1); newParagraph();
isFirstTitlePara = true; isFirstTitlePara = true;
bold = true; bold = true;
center = true; center = true;
@@ -292,28 +438,25 @@ export default class BookParser {
this.contents.push(curTitle); this.contents.push(curTitle);
} }
if (tag == 'section') {
if (!isFirstSection)
newParagraph(' ', 1);
isFirstSection = false;
sectionLevel++;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') { if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`<${tag}>`, 0); growParagraph(`<${tag}>`, 0);
} }
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) { if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
if (!(tag == 'p' && isFirstTitlePara)) if (!(tag == 'p' && isFirstTitlePara))
newParagraph(' ', 1); newParagraph();
if (tag == 'p') { if (tag == 'p') {
inPara = true; inPara = true;
isFirstTitlePara = false; isFirstTitlePara = false;
if (inTitle && inNotesBody && noteId) {
growParagraph(`<note href="${noteId}">`, 0);
}
} }
} }
if (tag == 'subtitle') { if (tag == 'subtitle') {
newParagraph(' ', 1); newParagraph();
isFirstTitlePara = true; isFirstTitlePara = true;
bold = true; bold = true;
center = true; center = true;
@@ -334,88 +477,109 @@ export default class BookParser {
} }
if (tag == 'poem') { if (tag == 'poem') {
newParagraph(' ', 1); newParagraph();
} }
if (tag == 'text-author') { if (tag == 'text-author') {
newParagraph(' ', 1); newParagraph();
bold = true;
space += 1; space += 1;
} }
if (!inTitle && inNotesBody && noteId && noteTags.has(tag)) {
this.notes[noteId].xml += `<${tag}>`;
}
} }
}; };
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
if (tag == elemName) { tag = elemName;
if (tag == 'binary') {
binaryId = ''; if (tag == 'a' && inNote) {
growParagraph('</note>', 0);
inNote = false;
}
if (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
inNotesBody = false;
} }
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
if (tag == 'section') { if (tag == 'title') {
sectionLevel--; isFirstTitlePara = false;
} bold = false;
center = false;
inTitle = false;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') { if (tag == 'section') {
growParagraph(`</${tag}>`, 0); sectionLevel--;
} }
if (tag == 'p') { if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
inPara = false; growParagraph(`</${tag}>`, 0);
} }
if (tag == 'subtitle') { if (tag == 'p') {
isFirstTitlePara = false; inPara = false;
bold = false;
center = false;
inSubtitle = false;
}
if (tag == 'epigraph' || tag == 'annotation') { if (inTitle && inNotesBody && noteId) {
italic = false; growParagraph('</note>', 0);
space -= 1;
if (tag == 'annotation')
newParagraph(' ', 1);
}
if (tag == 'stanza') {
newParagraph(' ', 1);
}
if (tag == 'text-author') {
space -= 1;
} }
} }
path = path.substr(0, path.length - tag.length - 1); if (tag == 'subtitle') {
let i = path.lastIndexOf('/'); isFirstTitlePara = false;
if (i >= 0) { bold = false;
tag = path.substr(i + 1); center = false;
} else { inSubtitle = false;
}
if (tag == 'epigraph' || tag == 'annotation') {
italic = false;
space -= 1;
newParagraph();
}
if (tag == 'stanza') {
newParagraph();
}
if (tag == 'text-author') {
bold = false;
space -= 1;
}
if (!inTitle && inNotesBody && noteId && noteTags.has(tag)) {
this.notes[noteId].xml += `</${tag}>`;
}
}
let i = path.lastIndexOf(tag);
if (i >= 0) {
path = path.substring(0, i - 1);
i = path.lastIndexOf('/');
if (i >= 0)
tag = path.substring(i + 1);
else
tag = path; tag = path;
}
} }
}; };
const onTextNode = (text) => {// eslint-disable-line no-unused-vars const onTextNode = (text) => {// eslint-disable-line no-unused-vars
text = he.decode(text); text = he.decode(text);
text = text.replace(/>/g, '&gt;'); text = text.replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/[\t\n\r\xa0]/g, ' ');
text = text.replace(/</g, '&lt;');
if (text && text.trim() == '') if (text && text.trim() == '')
text = (text.indexOf(' ') >= 0 ? ' ' : ''); text = ' ';
if (!text) if (!text)
return; return;
text = text.replace(/[\t\n\r\xa0]/g, ' ');
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0); const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
switch (path) { switch (path) {
case '/fictionbook/description/title-info/author/first-name': case '/fictionbook/description/title-info/author/first-name':
@@ -453,24 +617,39 @@ export default class BookParser {
fb2.annotation += text; fb2.annotation += text;
} }
let tOpen = (center ? '<center>' : ''); if (binaryId) {
tOpen += (bold ? '<strong>' : ''); if (!this.sets.isTesting) {
tOpen += (italic ? '<emphasis>' : ''); dimPromises.push(getImageDimensions(binaryId, binaryType, text));
tOpen += (space ? `<space w="${space}">` : ''); } else {
let tClose = (space ? '</space>' : ''); dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
tClose += (italic ? '</emphasis>' : ''); }
tClose += (bold ? '</strong>' : ''); }
tClose += (center ? '</center>' : '');
if (path.indexOf('/fictionbook/body/title') == 0 || if (path.indexOf('/fictionbook/body/title') == 0 ||
path.indexOf('/fictionbook/body/section') == 0 || path.indexOf('/fictionbook/body/section') == 0 ||
path.indexOf('/fictionbook/body/epigraph') == 0 path.indexOf('/fictionbook/body/epigraph') == 0
) { ) {
growParagraph(`${tOpen}${text}${tClose}`, text.length); let tOpen = (center ? '<center>' : '');
} tOpen += (bold ? '<strong>' : '');
tOpen += (italic ? '<emphasis>' : '');
tOpen += (space ? `<space w="${space}">` : '');
let tClose = (space ? '</space>' : '');
tClose += (italic ? '</emphasis>' : '');
tClose += (bold ? '</strong>' : '');
tClose += (center ? '</center>' : '');
if (binaryId) { if (text != ' ')
dimPromises.push(getImageDimensions(binaryId, binaryType, text)); growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
if (inNotesBody && noteId) {
if (inTitle) {
this.notes[noteId].title += text;
} else {
this.notes[noteId].xml += text;
}
}
} }
}; };
@@ -482,6 +661,7 @@ export default class BookParser {
await sax.parse(data, { await sax.parse(data, {
onStartNode, onEndNode, onTextNode, onProgress onStartNode, onEndNode, onTextNode, onProgress
}); });
correctCurrentPara();
if (dimPromises.length) { if (dimPromises.length) {
try { try {
@@ -502,7 +682,7 @@ export default class BookParser {
return {fb2}; return {fb2};
} }
imageHrefToId(id) { hrefToId(id) {
let local = false; let local = false;
if (id[0] == '#') { if (id[0] == '#') {
id = id.substr(1); id = id.substr(1);
@@ -535,16 +715,26 @@ export default class BookParser {
splitToStyle(s) { splitToStyle(s) {
let result = [];/*array of { let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number}, style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number, note: Object},
image: {local: Boolean, inline: Boolean, id: String}, image: {local: Boolean, inline: Boolean, id: String},
text: String, text: String,
}*/ }*/
let style = {}; let style = {};
let image = {}; let image = {};
//оптимизация по памяти
const copyStyle = (s) => {
const r = {};
for (const prop in s) {
if (s[prop])
r[prop] = s[prop];
}
return r;
};
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
result.push({ result.push({
style: Object.assign({}, style), style: copyStyle(style),
image, image,
text text
}); });
@@ -576,7 +766,7 @@ export default class BookParser {
case 'image': { case 'image': {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
image = this.imageHrefToId(attrs.href.value); image = this.hrefToId(attrs.href.value);
image.inline = false; image.inline = false;
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0); image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
} }
@@ -585,17 +775,24 @@ export default class BookParser {
case 'image-inline': { case 'image-inline': {
let attrs = sax.getAttrsSync(tail); let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) { if (attrs.href && attrs.href.value) {
const img = this.imageHrefToId(attrs.href.value); const img = this.hrefToId(attrs.href.value);
img.inline = true; img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0); img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({ result.push({
style: Object.assign({}, style), style: copyStyle(style),
image: img, image: img,
text: '' text: ''
}); });
} }
break; break;
} }
case 'note': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
style.note = {id: attrs.href.value, orig: attrs.orig?.value};
}
break;
}
} }
}; };
@@ -624,6 +821,9 @@ export default class BookParser {
break; break;
case 'image-inline': case 'image-inline':
break; break;
case 'note':
style.note = false;
break;
} }
}; };
@@ -632,7 +832,7 @@ export default class BookParser {
}); });
//длинные слова (или белиберду без пробелов) тоже разобьем //длинные слова (или белиберду без пробелов) тоже разобьем
const maxWordLength = this.maxWordLength; const maxWordLength = this.sets.maxWordLength;
const parts = result; const parts = result;
result = []; result = [];
for (const part of parts) { for (const part of parts) {
@@ -645,7 +845,7 @@ export default class BookParser {
spaceIndex = i; spaceIndex = i;
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 && if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) { this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.sets.w - this.sets.p) {
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)}); result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)}; p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
spaceIndex = -1; spaceIndex = -1;
@@ -663,86 +863,87 @@ export default class BookParser {
splitToSlogi(word) { splitToSlogi(word) {
let result = []; let result = [];
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
const soglas = new Set([
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
]);
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
const alpha = new Set([...glas, ...soglas, ...znak]);
let slog = '';
let slogLen = 0;
const len = word.length; const len = word.length;
word += ' '; if (len > 3) {
for (let i = 0; i < len; i++) { let slog = '';
slog += word[i]; let slogLen = 0;
if (alpha.has(word[i])) word += ' ';
slogLen++; for (let i = 0; i < len; i++) {
slog += word[i];
if (alpha.has(word[i]))
slogLen++;
if (slogLen > 1 && i < len - 2 && ( if (slogLen > 1 && i < len - 2 && (
//гласная, а следом не 2 согласные буквы //гласная, а следом не 2 согласные буквы
(glas.has(word[i]) && !(soglas.has(word[i + 1]) && (glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2]) alpha.has(word[i + 1]) && alpha.has(word[i + 2])
) || ) ||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы //предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && (alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
soglas.has(word[i]) && soglas.has(word[i + 1]) && ( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
alpha.has(word[i + 1]) && alpha.has(word[i + 2]) ) ||
) || //мягкий или твердый знак или Й
//мягкий или твердый знак или Й (znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) || (word[i] == '-')
(word[i] == '-') ) &&
) && //нельзя оставлять окончания на ь, ъ, й
//нельзя оставлять окончания на ь, ъ, й !(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
) { ) {
result.push(slog); result.push(slog);
slog = ''; slog = '';
slogLen = 0; slogLen = 0;
}
} }
if (slog)
result.push(slog);
} else {
result.push(word);
} }
if (slog)
result.push(slog);
return result; return result;
} }
parsePara(paraIndex) { parsePara(paraIndex) {
const para = this.para[paraIndex]; const para = this.para[paraIndex];
const s = this.sets;
//перераспарсиваем только при изменении одного из параметров
if (!this.force && if (!this.force &&
para.parsed && para.parsed &&
para.parsed.testWidth === this.testWidth && para.parsed.p === s.p &&
para.parsed.w === this.w && para.parsed.w === s.w &&
para.parsed.p === this.p && para.parsed.font === s.font &&
para.parsed.wordWrap === this.wordWrap && para.parsed.fontSize === s.fontSize &&
para.parsed.maxWordLength === this.maxWordLength && para.parsed.wordWrap === s.wordWrap &&
para.parsed.font === this.font && para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs && para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs && para.parsed.maxWordLength === s.maxWordLength &&
para.parsed.showImages === this.showImages && para.parsed.lineHeight === s.lineHeight &&
para.parsed.imageHeightLines === this.imageHeightLines && para.parsed.showImages === s.showImages &&
para.parsed.imageFitWidth === this.imageFitWidth && para.parsed.imageHeightLines === s.imageHeightLines &&
para.parsed.compactTextPerc === this.compactTextPerc para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
para.parsed.compactTextPerc === s.compactTextPerc &&
para.parsed.testWidth === s.testWidth
) )
return para.parsed; return para.parsed;
const parsed = { const parsed = {
testWidth: this.testWidth, p: s.p,
w: this.w, w: s.w,
p: this.p, font: s.font,
wordWrap: this.wordWrap, fontSize: s.fontSize,
maxWordLength: this.maxWordLength, wordWrap: s.wordWrap,
font: this.font, cutEmptyParagraphs: s.cutEmptyParagraphs,
cutEmptyParagraphs: this.cutEmptyParagraphs, addEmptyParagraphs: s.addEmptyParagraphs,
addEmptyParagraphs: this.addEmptyParagraphs, maxWordLength: s.maxWordLength,
showImages: this.showImages, lineHeight: s.lineHeight,
imageHeightLines: this.imageHeightLines, showImages: s.showImages,
imageFitWidth: this.imageFitWidth, imageHeightLines: s.imageHeightLines,
compactTextPerc: this.compactTextPerc, imageFitWidth: (s.imageFitWidth || s.dualPageMode),
compactTextPerc: s.compactTextPerc,
testWidth: s.testWidth,
visible: true, //вычисляется позже visible: true, //вычисляется позже
}; };
@@ -774,7 +975,7 @@ export default class BookParser {
let ofs = 0;//смещение от начала параграфа para.offset let ofs = 0;//смещение от начала параграфа para.offset
let imgW = 0; let imgW = 0;
let imageInPara = false; let imageInPara = false;
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100; const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения // тут начинается самый замес, перенос по слогам и стилизация, а также изображения
for (const part of parts) { for (const part of parts) {
style = part.style; style = part.style;
@@ -787,14 +988,14 @@ export default class BookParser {
if (!bin) if (!bin)
bin = {h: 1, w: 1}; bin = {h: 1, w: 1};
let lineCount = this.imageHeightLines; let lineCount = parsed.imageHeightLines;
let c = Math.ceil(bin.h/this.lineHeight); let c = Math.ceil(bin.h/parsed.lineHeight);
const maxH = lineCount*this.lineHeight; const maxH = lineCount*parsed.lineHeight;
let maxH2 = maxH; let maxH2 = maxH;
if (this.imageFitWidth && bin.w > this.w) { if (parsed.imageFitWidth && bin.w > parsed.w) {
maxH2 = bin.h*this.w/bin.w; maxH2 = bin.h*parsed.w/bin.w;
c = Math.ceil(maxH2/this.lineHeight); c = Math.ceil(maxH2/parsed.lineHeight);
} }
lineCount = (c < lineCount ? c : lineCount); lineCount = (c < lineCount ? c : lineCount);
@@ -834,10 +1035,10 @@ export default class BookParser {
continue; continue;
} }
if (part.image.id && part.image.inline && this.showImages) { if (part.image.id && part.image.inline && parsed.showImages) {
const bin = this.binary[part.image.id]; const bin = this.binary[part.image.id];
if (bin) { if (bin) {
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h); let imgH = (bin.h > parsed.fontSize ? parsed.fontSize : bin.h);
imgW += bin.w*imgH/bin.h; imgW += bin.w*imgH/bin.h;
line.parts.push({style, text: '', line.parts.push({style, text: '',
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}}); image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
@@ -952,11 +1153,11 @@ export default class BookParser {
//parsed.visible //parsed.visible
if (imageInPara) { if (imageInPara) {
parsed.visible = this.showImages; parsed.visible = parsed.showImages;
} else { } else {
parsed.visible = !( parsed.visible = !(
(para.addIndex > this.addEmptyParagraphs) || (para.addIndex > parsed.addEmptyParagraphs) ||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '') (para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
); );
} }

View File

@@ -1,10 +1,14 @@
import localForage from 'localforage'; import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser'; import BookParser from './BookParser';
import readerApi from '../../../api/reader';
import coversStorage from './coversStorage';
import * as utils from '../../../share/utils';
const maxDataSize = 500*1024*1024;//compressed bytes const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
//локальный кэш метаданных книг, ограничение maxDataSize //локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({ const bmMetaStore = localForage.createInstance({
@@ -17,9 +21,6 @@ const bmDataStore = localForage.createInstance({
}); });
//список недавно открытых книг //список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({ const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew' name: 'bmRecentStoreNew'
}); });
@@ -39,7 +40,7 @@ class BookManager {
this.saveRecentItem = _.debounce(() => { this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem); bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1); this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev); bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300}); }, 200, {maxWait: 300});
@@ -54,6 +55,9 @@ class BookManager {
if (this.recentItem) if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem; this.recent[this.recentItem.key] = this.recentItem;
//конвертируем в новые ключи
await this.convertRecent();
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key'); this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) { if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`); const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
@@ -63,48 +67,6 @@ class BookManager {
} }
await this.cleanRecentBooks(); await this.cleanRecentBooks();
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
{
await this.convertFileToDiskPrefix();
if (this.recentRev > 10)
await bmRecentStoreOld.clear();
}
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
if (this.recentLast) {
this.recent[this.recentLast.key] = this.recentLast;
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
if (_.isObject(meta)) {
this.books[meta.key] = meta;
}
}
let key = null;
const len = await bmRecentStoreOld.length();
for (let i = len - 1; i >= 0; i--) {
key = await bmRecentStoreOld.key(i);
if (key) {
let r = await bmRecentStoreOld.getItem(key);
if (_.isObject(r) && r.key) {
this.recent[r.key] = r;
}
} else {
await bmRecentStoreOld.removeItem(key);
}
}
//размножение для дебага
/*if (key) {
for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
}
}*/
await bmRecentStoreNew.setItem('recent', this.recent);
this.recentRev = 1;
await bmRecentStoreNew.setItem('rev', this.recentRev);
} }
this.recentChanged = true; this.recentChanged = true;
@@ -112,6 +74,40 @@ class BookManager {
this.loadStored();//no await this.loadStored();//no await
} }
//TODO: убрать в 2025г
async convertRecent() {
const converted = await bmRecentStoreNew.getItem('recent-converted');
if (converted)
return;
const newRecent = {};
for (const book of Object.values(this.recent)) {
if (!book.path) {
continue;
}
const newKey = this.keyFromPath(book.path);
newRecent[newKey] = _.cloneDeep(book);
newRecent[newKey].key = newKey;
if (!newRecent[newKey].loadTime)
newRecent[newKey].loadTime = newRecent[newKey].addTime;
}
this.recent = newRecent;
//console.log(converted);
(async() => {
await utils.sleep(3000);
this.saveRecent();
this.emit('recent-changed');
this.emit('set-recent');
await bmRecentStoreNew.setItem('recent-converted', true);
})();
}
//Ленивая асинхронная загрузка bmMetaStore //Ленивая асинхронная загрузка bmMetaStore
async loadStored() { async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
@@ -238,8 +234,12 @@ class BookManager {
async addBook(newBook, callback) { async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path}; let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now(); if (newBook.downloadSize !== undefined && newBook.downloadSize >= 0)
meta.downloadSize = newBook.downloadSize;
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => { const cb = (perc) => {
const p = Math.round(30*perc/100); const p = Math.round(30*perc/100);
@@ -274,10 +274,10 @@ class BookManager {
async hasBookParsed(meta) { async hasBookParsed(meta) {
if (!this.books) if (!this.books)
return false; return false;
if (!meta.url) if (!meta.path)
return false; return false;
if (!meta.key) if (!meta.key)
meta.key = this.keyFromUrl(meta.url); meta.key = this.keyFromPath(meta.path);
let book = this.books[meta.key]; let book = this.books[meta.key];
@@ -292,8 +292,12 @@ class BookManager {
async getBook(meta, callback) { async getBook(meta, callback) {
let result = undefined; let result = undefined;
if (!meta.path)
return;
if (!meta.key) if (!meta.key)
meta.key = this.keyFromUrl(meta.url); meta.key = this.keyFromPath(meta.path);
result = this.books[meta.key]; result = this.books[meta.key];
@@ -303,11 +307,6 @@ class BookManager {
this.books[meta.key] = result; this.books[meta.key] = result;
} }
//Если файл на сервере изменился, считаем, что в кеше его нету
if (meta.path && result && meta.path != result.path) {
return;
}
if (result && !result.parsed) { if (result && !result.parsed) {
let data = await bmDataStore.getItem(`bmData-${meta.key}`); let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5); callback(5);
@@ -352,9 +351,38 @@ class BookManager {
const parsed = new BookParser(this.settings); const parsed = new BookParser(this.settings);
const parsedMeta = await parsed.parse(data, callback); const parsedMeta = await parsed.parse(data, callback);
//cover page
let coverPageUrl = '';
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
const bin = parsed.binary[parsed.coverPageId];
let dataUrl = `data:${bin.type};base64,${bin.data}`;
try {
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
} catch (e) {
console.error(e);
}
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
//далее асинхронно
(async() => {
//отправим dataUrl на сервер в /upload
try {
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
} catch (e) {
console.error(e);
}
//сохраним в storage
await coversStorage.setData(coverPageUrl, dataUrl);
})();
}
const result = Object.assign({}, meta, parsedMeta, { const result = Object.assign({}, meta, parsedMeta, {
length: data.length, length: data.length,
textLength: parsed.textLength, textLength: parsed.textLength,
coverPageUrl,
parsed parsed
}); });
@@ -367,14 +395,24 @@ class BookManager {
return result; return result;
} }
keyFromUrl(url) { /*keyFromUrl(url) {
return utils.stringToHex(url); return utils.stringToHex(url);
}*/
keyFromPath(bookPath) {
return path.basename(bookPath);
} }
keysEqual(bookPath1, bookPath2) {
if (bookPath1 === undefined || bookPath2 === undefined)
return false;
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
}
//-- recent -------------------------------------------------------------- //-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) { async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev'); const rev = await bmRecentStoreNew.getItem('rev');
if (rev != this.recentRev && !skipCheck) { if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
const newRecent = await bmRecentStoreNew.getItem('recent'); const newRecent = await bmRecentStoreNew.getItem('recent');
Object.assign(this.recent, newRecent); Object.assign(this.recent, newRecent);
this.recentItem = await bmRecentStoreNew.getItem('recent-item'); this.recentItem = await bmRecentStoreNew.getItem('recent-item');
@@ -411,7 +449,10 @@ class BookManager {
async setRecentBook(value) { async setRecentBook(value) {
let result = this.metaOnly(value); let result = this.metaOnly(value);
result.touchTime = Date.now(); result.touchTime = Date.now();//время последнего чтения
if (!result.loadTime)
result.loadTime = Date.now();//время загрузки файла
result.deleted = 0; result.deleted = 0;
if (this.recent[result.key]) { if (this.recent[result.key]) {
@@ -426,10 +467,10 @@ class BookManager {
async getRecentBook(value) { async getRecentBook(value) {
return this.recent[value.key]; return this.recent[value.key];
} }
/*
async delRecentBook(value) { async delRecentBook(value, delFlag = 1) {
const item = this.recent[value.key]; const item = this.recent[value.key];
item.deleted = 1; item.deleted = delFlag;
if (this.recentLastKey == value.key) { if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null); await this.recentSetLastKey(null);
@@ -438,12 +479,68 @@ class BookManager {
await this.recentSetItem(item); await this.recentSetItem(item);
this.emit('recent-deleted', value.key); this.emit('recent-deleted', value.key);
} }
*/
async delRecentBooks(values, delFlag = 1) {
for (const value of values) {
const item = this.recent[value.key];
item.deleted = delFlag;
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
}
await this.recentSetItem(item);
}
this.emit('recent-deleted');
}
/*
async restoreRecentBook(value) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
*/
async restoreRecentBooks(values) {
for (const value of values) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
}
async setCheckBuc(value, checkBuc) {
const item = this.recent[value.key];
const updateItems = [];
if (item) {
if (item.sameBookKey !== undefined) {
const sorted = this.getSortedRecent();
for (const book of sorted) {
if (!book.deleted && book.sameBookKey === item.sameBookKey)
updateItems.push(book);
}
} else {
updateItems.push(item);
}
}
const now = Date.now();
for (const book of updateItems) {
book.checkBuc = checkBuc;
if (checkBuc)
book.checkBucTime = now;
await this.recentSetItem(book);
}
}
async cleanRecentBooks() { async cleanRecentBooks() {
const sorted = this.getSortedRecent(); const sorted = this.getSortedRecent();
let isDel = false; let isDel = false;
for (let i = 1000; i < sorted.length; i++) { for (let i = maxRecentLength; i < sorted.length; i++) {
delete this.recent[sorted[i].key]; delete this.recent[sorted[i].key];
isDel = true; isDel = true;
} }
@@ -455,33 +552,6 @@ class BookManager {
return isDel; return isDel;
} }
async convertFileToDiskPrefix() {
let isConverted = false;
const newRecent = {};
for (let key of Object.keys(this.recent)) {
let newKey = key;
let newUrl = this.recent[key].url;
if (newKey.indexOf('66696c65') == 0) {
newKey = newKey.replace(/^66696c65/, '6469736b');
if (newUrl)
newUrl = newUrl.replace(/^file/, 'disk');
isConverted = true;
}
newRecent[newKey] = this.recent[key];
newRecent[newKey].key = newKey;
if (newUrl)
newRecent[newKey].url = newUrl;
}
if (isConverted) {
this.recent = newRecent;
await this.recentSetItem(null, true);
}
return isConverted;
}
mostRecentBook() { mostRecentBook() {
if (this.recentLastKey) { if (this.recentLastKey) {
return this.recent[this.recentLastKey]; return this.recent[this.recentLastKey];
@@ -490,7 +560,7 @@ class BookManager {
let max = 0; let max = 0;
let result = null; let result = null;
for (let key in this.recent) { for (const key in this.recent) {
const book = this.recent[key]; const book = this.recent[key];
if (!book.deleted && book.touchTime > max) { if (!book.deleted && book.touchTime > max) {
max = book.touchTime; max = book.touchTime;
@@ -521,6 +591,43 @@ class BookManager {
return result; return result;
} }
findRecentByUrlAndPath(url, bookPath) {
if (bookPath) {
const key = this.keyFromPath(bookPath);
const book = this.recent[key];
if (book && !book.deleted)
return book;
}
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.url == url && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
findRecentBySameBookKey(sameKey) {
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
async setRecent(value) { async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent); const mergedRecent = _.cloneDeep(this.recent);

View File

@@ -0,0 +1,61 @@
import localForage from 'localforage';
//import _ from 'lodash';
import * as utils from '../../../share/utils';
const maxDataSize = 100*1024*1024;
const coversStore = localForage.createInstance({
name: 'coversStorage'
});
class CoversStorage {
constructor() {
}
async init() {
this.cleanCovers(); //no await
}
async setData(key, data) {
await coversStore.setItem(key, {addTime: Date.now(), data});
}
async getData(key) {
const item = await coversStore.getItem(key);
return (item ? item.data : undefined);
}
async removeData(key) {
await coversStore.removeItem(key);
}
async cleanCovers() {
await utils.sleep(10000);
while (1) {// eslint-disable-line no-constant-condition
let size = 0;
let min = Date.now();
let toDel = null;
for (const key of (await coversStore.keys())) {
const item = await coversStore.getItem(key);
size += item.data.length;
if (item.addTime < min) {
toDel = key;
min = item.addTime;
}
}
if (size > maxDataSize && toDel) {
await this.removeData(toDel);
} else {
break;
}
}
}
}
export default new CoversStorage();

View File

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

View File

@@ -1,7 +1,349 @@
export const versionHistory = [ export const versionHistory = [
{ {
version: '1.2.8',
releaseDate: '2025-06-04',
showUntil: '2025-06-03',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.7',
releaseDate: '2025-02-22',
showUntil: '2025-02-21',
content:
`
<ul>
<li>отключена форма для сбора донатов</li>
<li>мелкие оптимизации</li>
</ul>
`
},
{
version: '1.2.6',
releaseDate: '2024-10-03',
showUntil: '2024-10-02',
content:
`
<ul>
<li>исправления из-за нарушения авторских прав</li>
</ul>
`
},
{
version: '1.2.4',
releaseDate: '2024-08-27',
showUntil: '2024-08-26',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.3',
releaseDate: '2024-08-02',
showUntil: '2024-08-01',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.2',
releaseDate: '2024-07-28',
showUntil: '2024-07-27',
content:
`
<ul>
<li>добавлено отображение примечаний на месте, по клику на сноске (#50)</li>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.0',
releaseDate: '2024-03-25',
showUntil: '2024-03-24',
content:
`
<ul>
<li>в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий</li>
<li>добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47)</li>
</ul>
`
},
{
version: '1.1.3',
releaseDate: '2023-02-06',
showUntil: '2023-02-05',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.1.2',
releaseDate: '2023-01-22',
showUntil: '2023-01-21',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.1.1',
releaseDate: '2023-01-11',
showUntil: '2023-01-15',
content:
`
<ul>
<li>добавлена опция "Ночной режим" и кнопка на панель</li>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.0.0',
releaseDate: '2022-12-18',
showUntil: '2022-12-25',
content:
`
<ul>
<li>на мобильных устройствах переход в полноэкранный режим теперь возможен через двойной тап по центру</li>
<li>добавлено окно "Сетевая библиотека" для omnireader.ru</li>
<li>улучшена работа синхронизации с сервером при плохом качестве связи</li>
<li>добавлена сборка релизов читалки: <a href="https://github.com/bookpauk/liberama/releases" target="_blank">https://github.com/bookpauk/liberama/releases</a></li>
</ul>
`
},
{
version: '0.12.2',
releaseDate: '2022-09-04',
showUntil: '2022-09-11',
content:
`
<ul>
<li>исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц</li>
<li>автор приносит извинения за доставленные неудобства</li>
</ul>
`
},
{
version: '0.12.1',
releaseDate: '2022-09-01',
showUntil: '2022-08-30',
content:
`
<ul>
<li>добавлена форма для доната</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.12.0',
releaseDate: '2022-07-27',
showUntil: '2022-08-03',
content:
`
<ul>
<li>запущен сервер проверки обновлений книг:</li>
<ul>
<li>проверка обновления той или иной книги настраивается в списке загруженных (чекбокс)</li>
<li>для того, чтобы чекбокс появился у ранее загруженной, необходимо принудительно обновить книгу</li>
<li>в настройках можно указать разницу размеров, при которой требуется делать уведомление</li>
</ul>
</ul>
`
},
{
version: '0.11.8',
releaseDate: '2022-07-14',
showUntil: '2022-07-13',
content:
`
<ul>
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
<li>добавлена синхронизация обоев</li>
</ul>
`
},
{
version: '0.11.7',
releaseDate: '2022-07-12',
showUntil: '2022-07-19',
content:
`
<ul>
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
<li>изменения в окне загруженных книг:</li>
<ul>
<li>добавлена группировка по версиям файла одной и той же книги</li>
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
<li>добавлены различные методы сортировки списка загруженных книг</li>
<li>нумерация всегда осуществляется по времени загрузки</li>
</ul>
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.6',
releaseDate: '2022-07-02',
showUntil: '2022-07-01',
content:
`
<ul>
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
<li>актуализация используемых пакетов</li>
</ul>
`
},
{
version: '0.11.5',
releaseDate: '2022-04-15',
showUntil: '2022-04-14',
content:
`
<ul>
<li>небольшие дополнения интерфейса</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.1',
releaseDate: '2021-12-03',
showUntil: '2021-12-02',
content:
`
<ul>
<li>переход на JembaDb вместо SQLite</li>
</ul>
`
},
{
version: '0.11.0',
releaseDate: '2021-11-18',
showUntil: '2021-11-17',
content:
`
<ul>
<li>переход на Vue 3</li>
</ul>
`
},
{
version: '0.10.3',
releaseDate: '2021-10-24',
showUntil: '2021-10-23',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.2',
releaseDate: '2021-10-19',
showUntil: '2021-10-18',
content:
`
<ul>
<li>актуализация версий пакетов и стека используемых технологий</li>
</ul>
`
},
{
version: '0.10.1',
releaseDate: '2021-10-10',
showUntil: '2021-10-09',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.10.0',
releaseDate: '2021-02-09',
showUntil: '2021-02-16',
content:
`
<ul>
<li>добавлен двухстраничный режим</li>
<li>в настройки добавлены все кириллические веб-шрифты от google</li>
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
<li>немного улучшен парсинг fb2</li>
</ul>
`
},
{
version: '0.9.12',
releaseDate: '2020-12-18',
showUntil: '2020-12-17', showUntil: '2020-12-17',
header: '0.9.12 (2020-12-18)',
content: content:
` `
<ul> <ul>
@@ -10,23 +352,27 @@ export const versionHistory = [
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li> <li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
<li>улучшения работы конвертеров</li> <li>улучшения работы конвертеров</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.11',
releaseDate: '2020-12-09',
showUntil: '2020-12-08', showUntil: '2020-12-08',
header: '0.9.11 (2020-12-09)',
content: content:
` `
<ul> <ul>
<li>оптимизации, улучшения работы конвертеров</li> <li>оптимизации, улучшения работы конвертеров</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.10',
releaseDate: '2020-12-03',
showUntil: '2020-12-10', showUntil: '2020-12-10',
header: '0.9.10 (2020-12-03)',
content: content:
` `
<ul> <ul>
@@ -34,69 +380,81 @@ export const versionHistory = [
<li>добавлена поддержка Rar-архивов</li> <li>добавлена поддержка Rar-архивов</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.9',
releaseDate: '2020-11-21',
showUntil: '2020-11-20', showUntil: '2020-11-20',
header: '0.9.9 (2020-11-21)',
content: content:
` `
<ul> <ul>
<li>оптимизации, исправления багов</li> <li>оптимизации, исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.8',
releaseDate: '2020-11-13',
showUntil: '2020-11-12', showUntil: '2020-11-12',
header: '0.9.8 (2020-11-13)',
content: content:
` `
<ul> <ul>
<li>добавлено окно "Оглавление/закладки"</li> <li>добавлено окно "Оглавление/закладки"</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.7',
releaseDate: '2020-11-12',
showUntil: '2020-11-11', showUntil: '2020-11-11',
header: '0.9.7 (2020-11-12)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.6',
releaseDate: '2020-11-06',
showUntil: '2020-11-05', showUntil: '2020-11-05',
header: '0.9.6 (2020-11-06)',
content: content:
` `
<ul> <ul>
<li>завершена работа над новым окном "Библиотека"</li> <li>завершена работа над новым окном "Библиотека"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.5',
releaseDate: '2020-11-01',
showUntil: '2020-10-31', showUntil: '2020-10-31',
header: '0.9.5 (2020-11-01)',
content: content:
` `
<ul> <ul>
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li> <li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.4',
releaseDate: '2020-10-29',
showUntil: '2020-10-28', showUntil: '2020-10-28',
header: '0.9.4 (2020-10-29)',
content: content:
` `
<ul> <ul>
@@ -104,23 +462,27 @@ export const versionHistory = [
<li>для liberama.top добавлено новое окно: "Библиотека"</li> <li>для liberama.top добавлено новое окно: "Библиотека"</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.3',
releaseDate: '2020-05-21',
showUntil: '2020-05-20', showUntil: '2020-05-20',
header: '0.9.3 (2020-05-21)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.2',
releaseDate: '2020-03-15',
showUntil: '2020-04-25', showUntil: '2020-04-25',
header: '0.9.2 (2020-03-15)',
content: content:
` `
<ul> <ul>
@@ -128,119 +490,139 @@ export const versionHistory = [
<li>переход на Service Worker вместо AppCache для автономного режима работы</li> <li>переход на Service Worker вместо AppCache для автономного режима работы</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.1',
releaseDate: '2020-03-03',
showUntil: '2020-03-02', showUntil: '2020-03-02',
header: '0.9.1 (2020-03-03)',
content: content:
` `
<ul> <ul>
<li>улучшение работы серверной части</li> <li>улучшение работы серверной части</li>
<li>незначительные изменения интерфейса</li> <li>незначительные изменения интерфейса</li>
</ul> </ul>
` `
}, },
{ {
version: '0.9.0',
releaseDate: '2020-02-26',
showUntil: '2020-02-25', showUntil: '2020-02-25',
header: '0.9.0 (2020-02-26)',
content: content:
` `
<ul> <ul>
<li>переход на UI-фреймфорк Quasar</li> <li>переход на UI-фреймфорк Quasar</li>
<li>незначительные изменения интерфейса</li> <li>незначительные изменения интерфейса</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.4',
releaseDate: '2020-02-06',
showUntil: '2020-02-05', showUntil: '2020-02-05',
header: '0.8.4 (2020-02-06)',
content: content:
` `
<ul> <ul>
<li>добавлен paypal-адрес для пожертвований</li> <li>добавлен paypal-адрес для пожертвований</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.3',
releaseDate: '2020-01-28',
showUntil: '2020-01-27', showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content: content:
` `
<ul> <ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li> <li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li> <li>внутренние оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.2',
releaseDate: '2020-01-20',
showUntil: '2020-01-19', showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content: content:
` `
<ul> <ul>
<li>внутренние оптимизации</li> <li>внутренние оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.1',
releaseDate: '2020-01-07',
showUntil: '2020-01-06', showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',
content: content:
` `
<ul> <ul>
<li>добавлена частичная поддержка формата FB3</li> <li>добавлена частичная поддержка формата FB3</li>
<li>исправлен баг "Request path contains unescaped characters"</li> <li>исправлен баг "Request path contains unescaped characters"</li>
</ul> </ul>
` `
}, },
{ {
version: '0.8.0',
releaseDate: '2020-01-02',
showUntil: '2020-01-05', showUntil: '2020-01-05',
header: '0.8.0 (2020-01-02)',
content: content:
` `
<ul> <ul>
<li>окончательный переход на https</li> <li>окончательный переход на https</li>
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li> <li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.9',
releaseDate: '2019-11-27',
showUntil: '2019-11-26', showUntil: '2019-11-26',
header: '0.7.9 (2019-11-27)',
content: content:
` `
<ul> <ul>
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li> <li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.8',
releaseDate: '2019-11-25',
showUntil: '2019-11-24', showUntil: '2019-11-24',
header: '0.7.8 (2019-11-25)',
content: content:
` `
<ul> <ul>
<li>улучшение html-фильтров для сайтов</li> <li>улучшение html-фильтров для сайтов</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.7',
releaseDate: '2019-11-06',
showUntil: '2019-11-10', showUntil: '2019-11-10',
header: '0.7.7 (2019-11-06)',
content: content:
` `
<ul> <ul>
@@ -252,34 +634,40 @@ export const versionHistory = [
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li> <li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
</ul> </ul>
</ul> </ul>
` `
}, },
{ {
version: '0.7.6',
releaseDate: '2019-10-30',
showUntil: '2019-10-29', showUntil: '2019-10-29',
header: '0.7.6 (2019-10-30)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.5',
releaseDate: '2019-10-22',
showUntil: '2019-10-21', showUntil: '2019-10-21',
header: '0.7.5 (2019-10-22)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.3',
releaseDate: '2019-10-18',
showUntil: '2019-10-17', showUntil: '2019-10-17',
header: '0.7.3 (2019-10-18)',
content: content:
` `
<ul> <ul>
@@ -288,12 +676,14 @@ export const versionHistory = [
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li> <li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.1',
releaseDate: '2019-09-20',
showUntil: '2019-09-19', showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content: content:
` `
<ul> <ul>
@@ -301,12 +691,14 @@ export const versionHistory = [
<li>на панель управления добавлена кнопка "Автономный режим"</li> <li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li> <li>актуализирована справка</li>
</ul> </ul>
` `
}, },
{ {
version: '0.7.0',
releaseDate: '2019-09-07',
showUntil: '2019-10-01', showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content: content:
` `
<ul> <ul>
@@ -317,23 +709,27 @@ export const versionHistory = [
<li>немного улучшен внешний вид и управление на смартфонах</li> <li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li> <li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.10',
releaseDate: '2019-07-21',
showUntil: '2019-07-20', showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content: content:
` `
<ul> <ul>
<li>исправления багов</li> <li>исправления багов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.9',
releaseDate: '2019-06-23',
showUntil: '2019-06-22', showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content: content:
` `
<ul> <ul>
@@ -344,12 +740,14 @@ export const versionHistory = [
<li>улучшены прогрессбары</li> <li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li> <li>исправления недочетов, небольшие оптимизации</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.7',
releaseDate: '2019-05-30',
showUntil: '2019-06-05', showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content: content:
` `
<ul> <ul>
@@ -362,36 +760,42 @@ export const versionHistory = [
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li> <li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li> <li>исправления багов и недочетов</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.6',
releaseDate: '2019-03-29',
showUntil: '2019-03-29', showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content: content:
` `
<ul> <ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li> <li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li> <li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul> </ul>
` `
}, },
{ {
version: '0.6.4',
releaseDate: '2019-03-24',
showUntil: '2019-03-24', showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content: content:
` `
<ul> <ul>
<li>исправления багов, оптимизации</li> <li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li> <li>добавлена возможность синхронизации данных между устройствами</li>
</ul> </ul>
` `
}, },
{ {
version: '0.5.4',
releaseDate: '2019-03-04',
showUntil: '2019-03-04', showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content: content:
` `
<ul> <ul>
@@ -400,12 +804,14 @@ export const versionHistory = [
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li> <li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li> <li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul> </ul>
` `
}, },
{ {
version: '0.3.0',
releaseDate: '2019-02-17',
showUntil: '2019-02-17', showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content: content:
` `
<ul> <ul>
@@ -413,12 +819,14 @@ export const versionHistory = [
<li>улучшено распознавание текста</li> <li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li> <li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul> </ul>
` `
}, },
{ {
version: '0.1.7',
releaseDate: '2019-02-14',
showUntil: '2019-02-14', showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content: content:
` `
<ul> <ul>
@@ -428,17 +836,20 @@ export const versionHistory = [
<li>добавлена возможность сброса настроек</li> <li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li> <li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul> </ul>
` `
}, },
{ {
version: '0.1.0',
releaseDate: '2019-02-12',
showUntil: '2019-02-12', showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content: content:
` `
<ul> <ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li> <li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul> </ul>
` `
}, },

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Settings в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Settings extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,20 +0,0 @@
<template>
<div>
Раздел Sources в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
export default @Component({
})
class Sources extends Vue {
created() {
}
}
//-----------------------------------------------------------------------------
</script>

View File

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

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