Compare commits

...

186 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
119 changed files with 12882 additions and 8719 deletions

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

163
README.md
View File

@@ -1,43 +1,160 @@
# Liberama # Liberama
Браузерная онлайн-читалка книг и децентрализованная библиотека. Браузерная онлайн-читалка книг.
Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[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)
При запуске приложения, по умолчанию веб-сервер доступен по адресу [http://127.0.0.1:44080](http://127.0.0.1:44080)
Для указания местоположения рабочей директории, воспользуйтесь [параметрами командной строки](#cli).
Дополнительные параметры сервера настраиваются в [конфигурационном файле](#config).
[Отблагодарить автора проекта](https://donatty.com/liberama)
##
* [Возможности читалки](#capabilities)
* [Использование](#usage)
* [Параметры командной строки](#cli)
* [Конфигурация](#config)
* [Разворачивание на VPS](#vps)
* [Сборка проекта](#build)
* [Разработка](#development)
<a id="capabilities" />
## Возможности читалки
- загрузка любой страницы интернета
- синхронизация данных (настроек и читаемых книг) между различными устройствами
- работа в автономном режиме (без связи)
- изменение цвета фона, текста, размер и тип шрифта и прочее
- установка и запоминание текущей позиции и настроек в браузере и на сервере
- кэширование файлов книг на клиенте и на сервере
- открытие книг с локального диска
- плавный скроллинг текста
- анимация перелистывания
- поиск по тексту и копирование фрагмента
- запоминание недавних книг, скачивание книги из читалки в формате fb2
- управление кликом и с клавиатуры
- регистрация не требуется
- поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий
- релизы сервера под 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
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md) Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
## Сборка проекта <a id="build" />
Необходима версия node.js не ниже 14.
``` ### Сборка проекта
$ git clone https://github.com/bookpauk/liberama Сборка только в среде Linux.
$ cd liberama Необходима версия node.js не ниже 16.
$ npm i
Для сборки linux-arm64 необходимо предварительно установить [QEMU](https://wiki.debian.org/QemuUserEmulation).
```sh
git clone https://github.com/bookpauk/liberama
cd liberama
npm i
``` ```
### Windows #### Релизы
``` ```sh
$ npm run build:win npm run release
``` ```
### Linux Результат сборки будет доступен в каталоге `dist/release`
```
$ npm run build:linux
```
Результат сборки будет доступен в каталоге `dist/linux|win` в виде исполнимого (standalone) файла <a id="development" />
### Разработка ### Разработка
``` ```sh
$ npm run dev npm run dev
``` ```
## Помочь проекту Связаться с автором проекта: [bookpauk@gmail.com](mailto:bookpauk@gmail.com)
* bitcoin: bc1q3tyumaj648pp2e69jalsez2lnt462ttc33nup9
* litecoin: MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ
* monero: 8BQPnvHcPSHM5gMQsmuypDgx9NNsYqwXKfDDuswEyF2Q2ewQSfd2pkK6ydH2wmMyq2JViZvy9DQ35hLMx7g72mFWNJTPtnz

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,51 +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 axios = require('axios');
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);
//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';
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
await pipeline(res.data, 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

@@ -30,10 +30,6 @@ module.exports = {
} }
}*/ }*/
}, },
{
resourceQuery: /^\?vue/,
use: path.resolve(__dirname, 'includer.js')
},
{ {
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader', loader: 'babel-loader',

View File

@@ -1,5 +1,6 @@
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');
@@ -8,16 +9,15 @@ 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',
clean: true
}, },
module: { module: {

View File

@@ -17,9 +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',
clean: true
}, },
module: { module: {
rules: [ rules: [

View File

@@ -1,45 +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 axios = require('axios');
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);
//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';
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
await pipeline(res.data, 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,29 +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', 'acceptFileExt', 'bucEnabled', '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)));
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query))); if (config.error)
if (config.error) throw new Error(config.error);
throw new Error(config.error);
return config;
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http return config;
const response = await api.post('/config', query);
return response.data;
} }
} }

View File

@@ -7,9 +7,9 @@ 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() {
@@ -19,58 +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});
const requestId = await 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);
@@ -181,22 +146,13 @@ 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 {
response = await wsc.message(await 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 (state == 'error') {
throw new Error(response.error); throw new Error(response.error);
}
if (!response.state)
throw new Error('Неверный ответ api');
return response; return response;
} }

View File

@@ -20,7 +20,6 @@ import StdDialog from './share/StdDialog.vue';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import miscApi from '../api/misc'; import miscApi from '../api/misc';
import * as utils from '../share/utils';
const componentOptions = { const componentOptions = {
components: { components: {
@@ -31,7 +30,10 @@ const componentOptions = {
mode: function() { mode: function() {
this.setAppTitle(); this.setAppTitle();
this.redirectIfNeeded(); this.redirectIfNeeded();
} },
nightMode() {
this.setNightMode();
},
}, },
}; };
@@ -39,22 +41,40 @@ class App {
_options = componentOptions; _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 = '';
@@ -66,7 +86,7 @@ class App {
} }
return cachedRoute; return cachedRoute;
} };
this.$router.beforeEach((to, from, next) => { this.$router.beforeEach((to, from, next) => {
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница //распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
@@ -122,6 +142,8 @@ class App {
window.addEventListener('resize', (event) => { window.addEventListener('resize', (event) => {
this.$root.eventHook('resize', event); this.$root.eventHook('resize', event);
}); });
this.setNightMode();
} }
mounted() { mounted() {
@@ -130,10 +152,13 @@ class App {
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) {
//проверим, не получен ли конфиг ранее //проверим, не получен ли конфиг ранее
@@ -155,38 +180,6 @@ class App {
})(); })();
} }
toggleCollapse() {
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
this.$root.eventHook('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;
} }
@@ -195,14 +188,23 @@ class App {
return this.$root.getRootRoute(); 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.rootRoute]}`; document.title = `Универсальная читалка книг и ресурсов интернета`;
} }
} else { } else {
document.title = title; document.title = title;
@@ -217,33 +219,15 @@ class App {
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 = url;
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
}
} }
} }
} }
@@ -253,26 +237,155 @@ 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;
} }
.notify-margin { .q-notifications__list--top {
margin-top: 55px; top: 55px !important;
} }
.dborder { .dborder {

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Book в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../vueComponent.js';
class Book {
created() {
}
}
export default vueComponent(Book);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Card в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../vueComponent.js';
class Card {
created() {
}
}
export default vueComponent(Card);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,93 +0,0 @@
<template>
<div>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../vueComponent.js';
import _ from 'lodash';
const selfRoute = '/cardindex';
const tab2Route = [
'/cardindex/search',
'/cardindex/card',
'/cardindex/book',
'/cardindex/history',
];
let lastActiveTab = null;
const componentOptions = {
watch: {
selectedTab: function(newValue) {
lastActiveTab = newValue;
this.setRouteByTab(newValue);
},
curRoute: function(newValue) {
this.setTabByRoute(newValue);
},
},
};
class CardIndex {
_options = componentOptions;
selectedTab = null;
created() {
this.$watch(
() => this.$route.path,
(newValue) => {
if (newValue == '/cardindex' && this.isReader) {
this.$router.replace({ path: '/reader' });
}
}
)
}
mounted() {
this.setTabByRoute(this.curRoute);
}
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 mode() {
return this.$store.state.config.mode;
}
get curRoute() {
const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
return (m ? m[1] : this.$route.path);
}
get isReader() {
return (this.mode !== null && (this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top'));
}
}
export default vueComponent(CardIndex);
//-----------------------------------------------------------------------------
</script>
<style scoped>
</style>

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел History в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../vueComponent.js';
class History {
created() {
}
}
export default vueComponent(History);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Search в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../vueComponent.js';
class Search {
created() {
}
}
export default vueComponent(Search);
//-----------------------------------------------------------------------------
</script>

View File

@@ -5,13 +5,13 @@
</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 :disabled="!selected" class="q-mr-md" round dense color="blue" icon="la la-check" size="16px" @click.stop="openSelected"> <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 :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть выбранную закладку Открыть выбранную закладку
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти"> <q-input ref="search" v-model="search" bg-color="input" class="col" outlined dense placeholder="Найти">
<template #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>
@@ -19,7 +19,7 @@
</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" size="14px" @click.stop="addBookmark"> <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 :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить закладку Добавить закладку
@@ -62,6 +62,7 @@
v-model:ticked="ticked" v-model:ticked="ticked"
v-model:expanded="expanded" 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"
@@ -347,6 +348,7 @@ export default vueComponent(BookmarkSettings);
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

@@ -29,6 +29,7 @@
ref="rootLink" ref="rootLink"
v-model="rootLink" v-model="rootLink"
class="q-mr-sm" class="q-mr-sm"
bg-color="input"
:options="rootLinkOptions" :options="rootLinkOptions"
style="width: 230px" style="width: 230px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
@@ -58,6 +59,7 @@
ref="selectedLink" ref="selectedLink"
v-model="selectedLink" v-model="selectedLink"
class="q-mr-sm" class="q-mr-sm"
bg-color="input"
:options="selectedLinkOptions" :options="selectedLinkOptions"
style="width: 50px" style="width: 50px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
@@ -73,8 +75,8 @@
ref="input" ref="input"
v-model="bookUrl" v-model="bookUrl"
class="col q-mr-sm" class="col q-mr-sm"
bg-color="input"
outlined dense outlined dense
bg-color="white"
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'" placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown" @focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
> >
@@ -99,7 +101,7 @@
</template> </template>
</q-input> </q-input>
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl"> <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 :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть в читалке Открыть в читалке
@@ -108,9 +110,9 @@
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div ref="frameBox" class="col fit" 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>
@@ -133,8 +135,8 @@
ref="bookmarkLink" ref="bookmarkLink"
v-model="bookmarkLink" v-model="bookmarkLink"
class="col q-mr-sm" class="col q-mr-sm"
bg-color="input"
outlined dense outlined dense
bg-color="white"
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown" placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
> >
</q-input> </q-input>
@@ -143,6 +145,7 @@
ref="defaultRootLink" ref="defaultRootLink"
v-model="defaultRootLink" v-model="defaultRootLink"
class="q-mr-sm" class="q-mr-sm"
bg-color="input"
:options="defaultRootLinkOptions" :options="defaultRootLinkOptions"
style="width: 50px" style="width: 50px"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
@@ -159,8 +162,8 @@
ref="bookmarkDesc" ref="bookmarkDesc"
v-model="bookmarkDesc" v-model="bookmarkDesc"
class="col q-mr-sm" class="col q-mr-sm"
bg-color="input"
outlined dense outlined dense
bg-color="white"
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown" placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
> >
</q-input> </q-input>
@@ -304,7 +307,12 @@ class ExternalLibs {
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.addEventHook('key', this.keyHook); this.$root.addEventHook('key', this.keyHook);
@@ -321,8 +329,6 @@ class ExternalLibs {
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() {
@@ -334,10 +340,7 @@ class ExternalLibs {
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();
@@ -348,17 +351,28 @@ class ExternalLibs {
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);
}); });
@@ -389,7 +403,10 @@ class ExternalLibs {
} }
} 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'});
} }
@@ -403,6 +420,30 @@ class ExternalLibs {
})(); })();
} }
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('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
@@ -410,6 +451,11 @@ class ExternalLibs {
} }
} }
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});
} }
@@ -458,11 +504,24 @@ class ExternalLibs {
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));
}
} }
result = result.filter(s => s).join(' | ');
this.$root.setAppTitle(result); this.$root.setAppTitle(result);
return result; return result;
} }
@@ -532,7 +591,7 @@ class ExternalLibs {
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});
}); });
@@ -561,6 +620,11 @@ class ExternalLibs {
} }
goToLink(link) { goToLink(link) {
this.inpxReady = false;
this.inpxTitle = '';
this.inpxUrl = '';
this.inpxOrigin = false;
if (!this.ready || !link) if (!this.ready || !link)
return; return;
@@ -576,6 +640,7 @@ class ExternalLibs {
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();
} }
@@ -648,13 +713,17 @@ class ExternalLibs {
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();
} }
@@ -668,6 +737,12 @@ class ExternalLibs {
} 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;
@@ -679,10 +754,10 @@ class ExternalLibs {
} }
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 = '';
@@ -837,20 +912,22 @@ class ExternalLibs {
<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'});

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Help в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../vueComponent.js';
class Help {
created() {
}
}
export default vueComponent(Help);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Income в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../vueComponent.js';
class Income {
created() {
}
}
export default vueComponent(Income);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
Страница не найдена
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../vueComponent.js';
class NotFound404 {
created() {
}
}
export default vueComponent(NotFound404);
//-----------------------------------------------------------------------------
</script>

View File

@@ -4,20 +4,20 @@
Оглавление/закладки Оглавление/закладки
</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>
@@ -80,13 +80,13 @@
<div class="image-num"> <div class="image-num">
{{ item.num }} {{ item.num }}
</div> </div>
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center"> <div v-show="item.type == 'image/jpeg'" class="image-type text-black it-jpg-color row justify-center">
JPG JPG
</div> </div>
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center"> <div v-show="item.type == 'image/png'" class="image-type text-black it-png-color row justify-center">
PNG PNG
</div> </div>
<div v-show="!item.local" class="image-type it-net-color row justify-center"> <div v-show="!item.local" class="image-type text-black it-net-color row justify-center">
INET INET
</div> </div>
</div> </div>
@@ -250,7 +250,7 @@ class ContentsPage {
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);
@@ -466,27 +466,31 @@ export default vueComponent(ContentsPage);
} }
.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 {
@@ -535,6 +539,7 @@ export default vueComponent(ContentsPage);
.image-thumb { .image-thumb {
height: 50px; height: 50px;
background-color: white;
} }
.loading-img-icon { .loading-img-icon {

View File

@@ -52,18 +52,21 @@ class CopyTextPage {
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;

View File

@@ -24,7 +24,7 @@
</p> </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>
@@ -59,7 +59,7 @@ class CommonHelpPage {
} }
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) {
@@ -88,6 +88,6 @@ export default vueComponent(CommonHelpPage);
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>

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

View File

@@ -1,20 +1,20 @@
<template> <template>
<Window @close="close" style="z-index: 200"> <Window style="z-index: 200" @close="close">
<template #header> <template #header>
Справка Справка
</template> </template>
<div class="col column" style="min-width: 600px"> <div class="col column" style="min-width: 600px">
<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="bg-grey-4 text-grey-7" class="bg-menu-2 text-menu"
> >
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" /> <q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
</q-tabs> </q-tabs>
@@ -51,7 +51,7 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'], ['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'], ['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'], ['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'], //['DonateHelpPage', 'Помочь проекту'],
]; ];
const componentOptions = { const componentOptions = {

View File

@@ -19,7 +19,7 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js'; import vueComponent from '../../../vueComponent.js';
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue'; import UserHotKeys from '../../SettingsPage/KeysTab/UserHotKeys/UserHotKeys.vue';
const componentOptions = { const componentOptions = {
components: { components: {

View File

@@ -13,7 +13,7 @@
<li>Жесты для тачскрина:</li> <li>Жесты для тачскрина:</li>
<ul> <ul>
<li style="list-style-type: square"> <li style="list-style-type: square">
от центра вверх: на весь экран от центра вверх/двойной тап по центру: на весь экран
</li> </li>
<li style="list-style-type: square"> <li style="list-style-type: square">
от центра вниз: плавный скроллинг от центра вниз: плавный скроллинг

View File

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

@@ -8,7 +8,7 @@ import vueComponent from '../../vueComponent.js';
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'; import _ from 'lodash';
const componentOptions = { const componentOptions = {
@@ -28,13 +28,18 @@ class LibsPage {
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}`;
@@ -114,8 +119,12 @@ class LibsPage {
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: _.cloneDeep(this.libs)}); this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs), sets: {nightMode: this.nightMode}});
} }
close() { close() {

View File

@@ -1,6 +1,6 @@
<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" corner-color="#1B695F" git-color="#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">
@@ -14,7 +14,7 @@
<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 <q-input
ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px" ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px"
outlined dense bg-color="white" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown" outlined dense bg-color="input" placeholder="Ссылка на книгу или веб-страницу" @keydown="onInputKeydown"
> >
<template #append> <template #append>
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" /> <q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
@@ -29,13 +29,13 @@
/> />
<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-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-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена Из буфера обмена
</q-btn> </q-btn>
@@ -55,7 +55,6 @@
</div> </div>
<div class="col column justify-end items-center no-wrap overflow-hidden"> <div class="col column justify-end items-center no-wrap overflow-hidden">
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
<span class="bottom-span clickable" @click="openHelp">Справка</span> <span class="bottom-span clickable" @click="openHelp">Справка</span>
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span> <span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
@@ -64,18 +63,6 @@
</div> </div>
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage> <PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
<Dialog ref="dialog1" v-model="findBookVisible">
<template #header>
Подсказка ;-)
</template>
<div style="word-break: normal">
Если вы хотите найти определенную книгу, добро пожаловать в
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
<a href="https://liberama.top" target="_blank">liberama.top</a>
</div>
</Dialog>
</div> </div>
</template> </template>
@@ -103,7 +90,6 @@ class LoaderPage {
bookUrl = null; bookUrl = null;
loadPercent = 0; loadPercent = 0;
pasteTextActive = false; pasteTextActive = false;
findBookVisible = false;
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -122,7 +108,7 @@ class LoaderPage {
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 'Универсальная читалка книг и ресурсов интернета.';
@@ -172,7 +158,7 @@ class LoaderPage {
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});
} }
} }
@@ -193,10 +179,6 @@ class LoaderPage {
this.$emit('do-action', {action: 'donate'}); this.$emit('do-action', {action: 'donate'});
} }
findBook() {
this.findBookVisible = true;
}
openComments() { openComments() {
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank'); window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
} }
@@ -213,9 +195,6 @@ class LoaderPage {
} }
keyHook(event) { keyHook(event) {
if (this.$refs.dialog1.active)
return true;
if (this.pasteTextActive) { if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event); return this.$refs.pasteTextPage.keyHook(event);
} }
@@ -238,7 +217,7 @@ export default vueComponent(LoaderPage);
} }
.clickable { .clickable {
color: blue; color: var(--text-anchor-color);
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }

View File

@@ -8,9 +8,11 @@
</span> </span>
</template> </template>
<q-input v-model="bookTitle" class="q-px-sm" dense borderless 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>
@@ -39,6 +41,10 @@ class PasteTextPage {
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");
@@ -60,7 +66,7 @@ class PasteTextPage {
calcTitle(event) { calcTitle(event) {
if (this.bookTitle == '') { if (this.bookTitle == '') {
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`; this.bookTitle = `Из буфера обмена ${utils.dateFormat(new Date())}`;
if (event) { if (event) {
let text = event.clipboardData.getData('text'); let text = event.clipboardData.getData('text');
this.bookTitle += ': ' + _.compact([ this.bookTitle += ': ' + _.compact([
@@ -115,6 +121,11 @@ export default vueComponent(PasteTextPage);
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,143 +1,148 @@
<template> <template>
<div class="column no-wrap"> <div class="column no-wrap">
<div v-show="toolBarActive" ref="header" class="header"> <div v-show="toolBarActive" ref="header" class="header">
<div ref="buttons" class="row justify-between no-wrap"> <div ref="buttons" class="row" :class="{'no-wrap': !toolBarMultiLine}">
<div class="row no-wrap"> <button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"> <q-icon name="la la-arrow-left" size="32px" />
<q-icon name="la la-arrow-left" size="32px" /> <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%"> {{ rstore.readerActions['loader'] }}
{{ rstore.readerActions['loader'] }} </q-tooltip>
</q-tooltip> </button>
</button> <button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')"> <q-icon name="la la-caret-square-up" size="32px" />
<q-icon name="la la-caret-square-up" size="32px" /> <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%"> {{ rstore.readerActions['loadFile'] }}
{{ rstore.readerActions['loadFile'] }} </q-tooltip>
</q-tooltip> </button>
</button> <button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')"> <q-icon name="la la-comment" size="32px" />
<q-icon name="la la-comment" size="32px" /> <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%"> {{ rstore.readerActions['loadBuffer'] }}
{{ rstore.readerActions['loadBuffer'] }} </q-tooltip>
</q-tooltip> </button>
</button> <button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')"> <q-icon name="la la-question" size="32px" />
<q-icon name="la la-question" size="32px" /> <q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%"> {{ rstore.readerActions['help'] }}
{{ rstore.readerActions['help'] }} </q-tooltip>
</q-tooltip> </button>
</button>
</div>
<div class="row no-wrap"> <div class="col"></div>
<div class="space"></div>
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')"> <div class="space"></div>
<q-icon name="la la-angle-left" size="32px" /> <button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-angle-left" size="32px" />
{{ rstore.readerActions['undoAction'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['undoAction'] }}
</button> </q-tooltip>
<button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')"> </button>
<q-icon name="la la-angle-right" size="32px" /> <button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-angle-right" size="32px" />
{{ rstore.readerActions['redoAction'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['redoAction'] }}
</button> </q-tooltip>
<div class="space"></div> </button>
<button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"> <div class="space"></div>
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" /> <button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
{{ rstore.readerActions['fullScreen'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['fullScreen'] }}
</button> </q-tooltip>
<button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"> </button>
<q-icon name="la la-film" size="32px" /> <button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-film" size="32px" />
{{ rstore.readerActions['scrolling'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['scrolling'] }}
</button> </q-tooltip>
<button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"> </button>
<q-icon name="la la-angle-double-right" size="32px" /> <button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-angle-double-right" size="32px" />
{{ rstore.readerActions['setPosition'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['setPosition'] }}
</button> </q-tooltip>
<button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"> </button>
<q-icon name="la la-search" size="32px" /> <button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-search" size="32px" />
{{ rstore.readerActions['search'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['search'] }}
</button> </q-tooltip>
<button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"> </button>
<q-icon name="la la-copy" size="32px" /> <button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-copy" size="32px" />
{{ rstore.readerActions['copyText'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['copyText'] }}
</button> </q-tooltip>
<button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')"> </button>
<q-icon name="la la-magic" size="32px" /> <button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-magic" size="32px" />
{{ rstore.readerActions['convOptions'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['convOptions'] }}
</button> </q-tooltip>
<button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')"> </button>
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" /> <button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
{{ rstore.readerActions['refresh'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['refresh'] }}
</button> </q-tooltip>
<div class="space"></div> </button>
<button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')"> <div v-show="showToolButton['libs']" class="space"></div>
<q-icon name="la la-list" size="32px" /> <button v-show="showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-sitemap" size="32px" />
{{ rstore.readerActions['contents'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['libs'] }}
</button> </q-tooltip>
<button v-show="mode == 'liberama.top' && showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')"> </button>
<q-icon name="la la-sitemap" size="32px" /> <div class="space"></div>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
{{ rstore.readerActions['libs'] }} <q-icon name="la la-list" size="32px" />
</q-tooltip> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</button> {{ rstore.readerActions['contents'] }}
<button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"> </q-tooltip>
<div v-show="bothBucEnabled && needBookUpdateCount > 0" style="position: absolute"> </button>
<div class="need-book-update-count"> <button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
{{ needBookUpdateCount }} <div v-show="bothBucEnabled && needBookUpdateCount > 0" style="position: absolute">
</div> <div class="need-book-update-count">
{{ needBookUpdateCount }}
</div> </div>
</div>
<q-icon name="la la-book-open" size="32px" /> <q-icon name="la la-book-open" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['recentBooks'] }} {{ rstore.readerActions['recentBooks'] }}
</q-tooltip> </q-tooltip>
</button> </button>
<div class="space"></div> <div class="space"></div>
</div>
<div class="row no-wrap"> <div class="col"></div>
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
<q-icon name="la la-mouse" size="32px" /> <button v-show="showToolButton['nightMode']" ref="nightMode" v-ripple class="tool-button" :class="buttonActiveClass('nightMode')" @click="buttonClick('nightMode')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-moon" size="32px" />
{{ rstore.readerActions['clickControl'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['nightMode'] }}
</button> </q-tooltip>
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"> </button>
<q-icon name="la la-unlink" size="32px" /> <button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-icon name="la la-mouse" size="32px" />
{{ rstore.readerActions['offlineMode'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['clickControl'] }}
</button> </q-tooltip>
<button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')"> </button>
<q-icon name="la la-cog" size="32px" /> <button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%"> <q-icon name="la la-unlink" size="32px" />
{{ rstore.readerActions['settings'] }} <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
</q-tooltip> {{ rstore.readerActions['offlineMode'] }}
</button> </q-tooltip>
</div> </button>
<button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
<q-icon name="la la-cog" size="32px" />
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">
{{ rstore.readerActions['settings'] }}
</q-tooltip>
</button>
</div> </div>
</div> </div>
<div class="main col row relative-position"> <div class="col row relative-position main">
<keep-alive> <keep-alive>
<component <component
:is="activePage" :is="activePage"
@@ -291,6 +296,8 @@ class Reader {
contentsActive = false; contentsActive = false;
libsActive = false; libsActive = false;
recentBooksActive = false; recentBooksActive = false;
nightModeActive = false;
clickControlActive = false; clickControlActive = false;
settingsActive = false; settingsActive = false;
@@ -304,6 +311,8 @@ class Reader {
showRefreshIcon = true; showRefreshIcon = true;
mostRecentBookReactive = null; mostRecentBookReactive = null;
showToolButton = {}; showToolButton = {};
toolBarHideOnScroll = false;
toolBarMultiLine = false;
actionList = []; actionList = [];
actionCur = -1; actionCur = -1;
@@ -384,6 +393,9 @@ class Reader {
this.recentItemKeys = []; this.recentItemKeys = [];
//сохранение в удаленном хранилище //сохранение в удаленном хранилище
await this.$refs.serverStorage.saveRecent(itemKeys); await this.$refs.serverStorage.saveRecent(itemKeys);
//periodicTasks
this.periodicTasks();//no await
} catch (e) { } catch (e) {
if (!this.offlineModeActive) if (!this.offlineModeActive)
this.$root.notify.error(e.message); this.$root.notify.error(e.message);
@@ -433,26 +445,15 @@ class Reader {
this.$refs.recentBooksPage.init(); this.$refs.recentBooksPage.init();
})(); })();
//проверки обновлений читалки //единственный запуск periodicTasks при инициализации
//дальнейшие запуски periodicTasks выполняются из debouncedSaveRecent
//т.е. только по действию пользователя
(async() => { (async() => {
await utils.sleep(15*1000);
this.isFirstNeedUpdateNotify = true; this.isFirstNeedUpdateNotify = true;
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
while (1) {// eslint-disable-line no-constant-condition
await this.checkNewVersionAvailable();
await utils.sleep(60*60*1000); //каждый час
}
//дальше хода нет
})();
//проверки обновлений книг this.allowPeriodicTasks = true;
(async() => { this.periodicTasks();//no await
await utils.sleep(15*1000); //подождем неск. секунд перед первым запросом
//вечный цикл, запрашиваем периодически обновления
while (1) {// eslint-disable-line no-constant-condition
await this.checkBuc();
await utils.sleep(70*60*1000); //каждые 70 минут
}
//дальше хода нет
})(); })();
} }
@@ -461,11 +462,12 @@ class Reader {
this.allowUrlParamBookPos = settings.allowUrlParamBookPos; this.allowUrlParamBookPos = settings.allowUrlParamBookPos;
this.copyFullText = settings.copyFullText; this.copyFullText = settings.copyFullText;
this.showClickMapPage = settings.showClickMapPage; this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl; this.nightModeActive = settings.nightMode;
this.clickControlActive = this.clickControl; this.clickControlActive = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton; this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll; this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.toolBarMultiLine = settings.toolBarMultiLine;
this.enableSitesFilter = settings.enableSitesFilter; this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify; this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara; this.splitToPara = settings.splitToPara;
@@ -543,35 +545,63 @@ class Reader {
//обновим settings, если загружали обои из /upload/ //обновим settings, если загружали обои из /upload/
if (updated) { if (updated) {
const newSettings = _.cloneDeep(this.settings); this.commit('reader/setSettings', {});
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
this.commit('reader/setSettings', newSettings);
} }
dynamicCss.replace('wallpapers', newCss); dynamicCss.replace('wallpapers', newCss);
} }
} }
async checkNewVersionAvailable() { async periodicTasks() {
if (!this.checkingNewVersion && this.showNeedUpdateNotify) { if (!this.allowPeriodicTasks || this.doingPeriodicTasks)
this.checkingNewVersion = true; return;
try {
await utils.sleep(15*1000); //подождем 15 секунд, чтобы прогрузился ServiceWorker при выходе новой версии
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
let againMes = ''; this.doingPeriodicTasks = true;
if (this.isFirstNeedUpdateNotify) { try {
againMes = ' еще один раз'; if (!this.taskList) {
const taskArr = [
[this.checkNewVersionAvailable, 60], //проверки обновлений читалки, каждый час
[this.checkBuc, 70], //проверки обновлений книг, каждые 70 минут
];
this.taskList = [];
for (const task of taskArr) {
const [method, period] = task;
this.taskList.push({method, period, lastRunTime: 0});
} }
}
for (const task of this.taskList) {
if (Date.now() - task.lastRunTime >= task.period*60*1000) {
try {
//console.log('task run', task.method.name);
await task.method();
} catch (e) {
console.error(e);
}
task.lastRunTime = Date.now();
}
}
} catch (e) {
console.error(e);
} finally {
this.doingPeriodicTasks = false;
}
}
async checkNewVersionAvailable() {
if (this.showNeedUpdateNotify) {
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
let againMes = '';
if (this.isFirstNeedUpdateNotify) {
againMes = ' еще один раз';
}
if (this.version != this.clientVersion)
this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
if (this.version != this.clientVersion)
this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
} catch(e) {
console.error(e);
} finally {
this.checkingNewVersion = false;
}
this.isFirstNeedUpdateNotify = false; this.isFirstNeedUpdateNotify = false;
} }
} }
@@ -580,82 +610,78 @@ class Reader {
if (!this.bothBucEnabled) if (!this.bothBucEnabled)
return; return;
try { const sorted = bookManager.getSortedRecent();
const sorted = bookManager.getSortedRecent();
//выберем все кандидиаты на обновление //выберем все кандидиаты на обновление
const updateUrls = new Set(); const updateUrls = new Set();
for (const book of sorted) { for (const book of sorted) {
if (!book.deleted && book.checkBuc && book.url && book.url.indexOf('disk://') !== 0) if (!book.deleted && book.checkBuc && book.url && book.url.indexOf('disk://') !== 0)
updateUrls.add(book.url); updateUrls.add(book.url);
}
//теперь по кусочкам запросим сервер
const arr = Array.from(updateUrls);
const bucSize = {};
const chunkSize = 100;
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
const data = await readerApi.checkBuc(chunk);
for (const item of data) {
bucSize[item.id] = item.size;
} }
//теперь по кусочкам запросим сервер await utils.sleep(1000);//чтобы не ддосить сервер
const arr = Array.from(updateUrls); }
const bucSize = {};
const chunkSize = 100;
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
const data = await readerApi.checkBuc(chunk); const checkSetTime = {};
//проставим новые размеры у книг
for (const item of data) { for (const book of sorted) {
bucSize[item.id] = item.size; if (book.deleted)
} continue;
await utils.sleep(1000);//чтобы не ддосить сервер //размер 0 считаем отсутствующим
if (book.url && bucSize[book.url] && bucSize[book.url] !== book.bucSize) {
book.bucSize = bucSize[book.url];
await bookManager.recentSetItem(book);
} }
const checkSetTime = {}; //подготовка к следующему шагу, ищем книгу по url с максимальной датой установки checkBucTime/loadTime
//проставим новые размеры у книг //от этой даты будем потом отсчитывать bucCancelDays
for (const book of sorted) { if (updateUrls.has(book.url)) {
if (book.deleted) let rec = checkSetTime[book.url] || {time: 0, loadTime: 0};
continue;
//размер 0 считаем отсутствующим
if (book.url && bucSize[book.url] && bucSize[book.url] !== book.bucSize) {
book.bucSize = bucSize[book.url];
await bookManager.recentSetItem(book);
}
//подготовка к следующему шагу, ищем книгу по url с максимальной датой установки checkBucTime/loadTime const time = (book.checkBucTime ? book.checkBucTime : (rec.loadTime || 0));
//от этой даты будем потом отсчитывать bucCancelDays if (time > rec.time || (time == rec.time && (book.loadTime > rec.loadTime)))
if (updateUrls.has(book.url)) { rec = {time, loadTime: book.loadTime, key: book.key};
let rec = checkSetTime[book.url] || {time: 0, loadTime: 0};
const time = (book.checkBucTime ? book.checkBucTime : (rec.loadTime || 0)); checkSetTime[book.url] = rec;
if (time > rec.time || (time == rec.time && (book.loadTime > rec.loadTime)))
rec = {time, loadTime: book.loadTime, key: book.key};
checkSetTime[book.url] = rec;
}
} }
}
//bucCancelEnabled и bucCancelDays //bucCancelEnabled и bucCancelDays
//снимем флаг checkBuc у необновлявшихся bucCancelDays //снимем флаг checkBuc у необновлявшихся bucCancelDays
if (this.bucCancelEnabled) { if (this.bucCancelEnabled) {
for (const rec of Object.values(checkSetTime)) { for (const rec of Object.values(checkSetTime)) {
if (rec.time && Date.now() - rec.time > this.bucCancelDays*24*3600*1000) { if (rec.time && Date.now() - rec.time > this.bucCancelDays*24*3600*1000) {
const book = await bookManager.getRecentBook({key: rec.key}); const book = await bookManager.getRecentBook({key: rec.key});
const needBookUpdate = const needBookUpdate =
book.checkBuc book.checkBuc
&& book.bucSize && book.bucSize
&& utils.hasProp(book, 'downloadSize') && utils.hasProp(book, 'downloadSize')
&& book.bucSize !== book.downloadSize && book.bucSize !== book.downloadSize
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff) && (book.bucSize - book.downloadSize >= this.bucSizeDiff)
; ;
if (book && !needBookUpdate) { if (book && !needBookUpdate) {
await bookManager.setCheckBuc(book, undefined);//!!! await bookManager.setCheckBuc(book, undefined);//!!!
}
} }
} }
} }
await this.$refs.recentBooksPage.updateTableData();
} catch (e) {
console.error(e);
} }
await this.$refs.recentBooksPage.updateTableData();
} }
updateCountChanged(event) { updateCountChanged(event) {
@@ -744,6 +770,10 @@ class Reader {
return this.$store.state.config.bucEnabled && this.bucEnabled; return this.$store.state.config.bucEnabled && this.bucEnabled;
} }
get restricted() {
return this.$store.state.config.restricted;
}
get routeParamUrl() { get routeParamUrl() {
let result = ''; let result = '';
const path = this.$route.fullPath; const path = this.$route.fullPath;
@@ -807,7 +837,7 @@ class Reader {
} }
get offlineModeActive() { get offlineModeActive() {
return this.reader.offlineModeActive; return this.reader.offlineModeActive;
} }
mostRecentBook() { mostRecentBook() {
@@ -840,8 +870,7 @@ class Reader {
} }
fullScreenToggle() { fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive; if (!this.$q.fullscreen.isActive) {
if (this.fullScreenActive) {
this.$q.fullscreen.request(); this.$q.fullscreen.request();
} else { } else {
this.$q.fullscreen.exit(); this.$q.fullscreen.exit();
@@ -1007,23 +1036,33 @@ class Reader {
} }
libsToogle() { libsToogle() {
if (this.config.networkLibraryLink) {
window.open(this.config.networkLibraryLink, '_blank');
return;
}
this.libsActive = !this.libsActive; this.libsActive = !this.libsActive;
if (this.libsActive) { if (this.libsActive) {
this.$refs.libsPage.init(); this.$refs.libsPage.init();//no await
} else { } else {
this.$refs.libsPage.done(); this.$refs.libsPage.done();
} }
} }
nightModeToggle() {
if (!this.nightModeActive && !utils.hasProp(this.settings.nightColorSets, 'textColor')) {
this.$root.notify.warning(`Ночной режим активирован впервые. Цвета заданы по умолчанию.`);
}
this.commit('reader/nightModeToggle');
}
clickControlToggle() { clickControlToggle() {
const newSettings = _.cloneDeep(this.settings); this.commit('reader/setSettings', {clickControl: !this.clickControlActive});
newSettings.clickControl = !this.clickControl;
this.commit('reader/setSettings', newSettings);
} }
offlineModeToggle() { offlineModeToggle() {
this.commit('reader/setOfflineModeActive', !this.offlineModeActive); this.commit('reader/setOfflineModeActive', !this.offlineModeActive);
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
} }
settingsToggle() { settingsToggle() {
@@ -1121,6 +1160,7 @@ class Reader {
case 'contents': case 'contents':
case 'libs': case 'libs':
case 'recentBooks': case 'recentBooks':
case 'nightMode':
case 'clickControl': case 'clickControl':
case 'offlineMode': case 'offlineMode':
case 'settings': case 'settings':
@@ -1169,7 +1209,7 @@ class Reader {
} }
async activateClickMapPage() { async activateClickMapPage() {
if (this.clickControl && this.showClickMapPage && !this.clickMapActive) { if (this.clickControlActive && this.showClickMapPage && !this.clickMapActive) {
this.clickMapActive = true; this.clickMapActive = true;
await this.$refs.clickMapPage.slowDisappear(); await this.$refs.clickMapPage.slowDisappear();
this.clickMapActive = false; this.clickMapActive = false;
@@ -1227,6 +1267,19 @@ class Reader {
return result; return result;
} }
isUrlAllowed(url) {
const restrictedSites = this.restricted?.sites;
if (restrictedSites) {
url = url.toLowerCase();
for (const site of restrictedSites) {
if (url.indexOf(site) === 0)
return false;
}
}
return true;
}
async _loadBook(opts) { async _loadBook(opts) {
if (!opts || !opts.url) { if (!opts || !opts.url) {
this.mostRecentBook(); this.mostRecentBook();
@@ -1237,6 +1290,11 @@ class Reader {
let url = encodeURI(decodeURI(opts.url)); let url = encodeURI(decodeURI(opts.url));
if (!this.isUrlAllowed(url)) {
this.$root.stdDialog.alert('Книга не загружена, причина: нарушение авторских прав.<br>Приносим извинения за неудобство.', '', {color: 'negative'});
return;
}
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) && if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('disk://') != 0)) (url.indexOf('disk://') != 0))
url = 'http://' + url; url = 'http://' + url;
@@ -1348,6 +1406,7 @@ class Reader {
found = (found ? _.cloneDeep(found) : found); found = (found ? _.cloneDeep(found) : found);
if (found) { if (found) {
//если такой файл уже не загружен (path не совпадают)
if (wasOpened.sameBookKey != found.sameBookKey) { if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы //спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) || const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
@@ -1396,8 +1455,6 @@ class Reader {
if (!this.showHelpOnErrorIfNeeded(url)) { if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'}); this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
} }
} finally {
this.checkNewVersionAvailable();
} }
} }
@@ -1527,6 +1584,9 @@ class Reader {
case 'recentBooks': case 'recentBooks':
this.recentBooksToggle(); this.recentBooksToggle();
break; break;
case 'nightMode':
this.nightModeToggle();
break;
case 'clickControl': case 'clickControl':
this.clickControlToggle(); this.clickControlToggle();
break; break;
@@ -1652,46 +1712,41 @@ export default vueComponent(Reader);
<style scoped> <style scoped>
.header { .header {
height: 50px; padding: 5px 5px 0px 5px;
padding-left: 5px;
padding-right: 5px;
background-color: #1B695F; background-color: #1B695F;
color: #000; color: #000;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-color: #c49a60 #e4e4e4; scrollbar-color: #c4aa60 #e4e4e4;
} }
.header::-webkit-scrollbar { .header::-webkit-scrollbar {
height: 10px; height: 5px;
} }
.header::-webkit-scrollbar-track { .header::-webkit-scrollbar-track {
background-color: #e4e4e4; background-color: #1B695F;
border-radius: 4px; border-radius: 1px;
} }
.header::-webkit-scrollbar-thumb { .header::-webkit-scrollbar-thumb {
background-color: #c49a60; background-color: #c4aa60;
border-radius: 4px; border-radius: 1px;
border: 2px solid #e4e4e4; border: 1px solid #1B695F;
}
.header::-webkit-scrollbar-thumb:hover {
background-color: #b48a50;
} }
.main { .main {
background-color: #EBE2C9; background-color: var(--bg-loader-color);
color: #000; color: var(--text-app-color);
} }
.tool-button { .tool-button {
margin: 0px 2px 0 2px; margin: 0px 2px 7px 2px;
padding: 0; padding: 0;
color: #3E843E; color: var(--text-tb-normal);
background-color: #E6EDF4; background-color: var(--bg-tb-normal);
margin-top: 5px; min-height: 38px;
min-width: 38px;
height: 38px; height: 38px;
width: 38px; width: 38px;
border: 0; border: 0;
@@ -1701,34 +1756,33 @@ export default vueComponent(Reader);
} }
.tool-button:hover { .tool-button:hover {
background-color: white; background-color: var(--bg-tb-hover);
cursor: pointer; cursor: pointer;
} }
.tool-button-active { .tool-button-active {
box-shadow: 0 0 0; box-shadow: 0 0 0;
color: white; color: var(--text-tb-active);
background-color: #8AB45F; background-color: var(--bg-tb-active);
position: relative; position: relative;
top: 1px; top: 1px;
left: 1px; left: 1px;
} }
.tool-button-active:hover { .tool-button-active:hover {
color: white; background-color: var(--bg-tb-active-hover);
background-color: #81C581;
cursor: pointer; cursor: pointer;
} }
.tool-button-disabled { .tool-button-disabled {
color: lightgray; color: var(--text-tb-disabled);
background-color: gray; background-color: var(--bg-tb-disabled);
cursor: default; cursor: default;
} }
.tool-button-disabled:hover { .tool-button-disabled:hover {
color: lightgray; color: var(--text-tb-disabled);
background-color: gray; background-color: var(--bg-tb-disabled);
cursor: default; cursor: default;
} }

View File

@@ -12,19 +12,19 @@
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span> <span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
<template #footer> <template #footer>
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable"> <q-btn class="q-px-md" color="btn2" text-color="app" dense no-caps @click="whatsNewDisable">
Больше не показывать Больше не показывать
</q-btn> </q-btn>
</template> </template>
</Dialog> </Dialog>
<q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss> <q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
<div class="column bg-white no-wrap q-pa-md"> <div class="column bg-dialog no-wrap q-pa-md">
<div class="row justify-center q-mb-md" style="font-size: 110%"> <div class="row justify-center q-mb-md">
Здравствуйте, дорогие читатели! Здравствуйте, дорогие читатели!
</div> </div>
<div class="q-mx-md column" style="word-break: normal"> <div class="q-mx-md column" style="font-size: 90%; word-break: normal">
<div> <div>
Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br> Вот уже много лет мы все вместе пользуемся нашей любимой читалкой.<br><br>
@@ -43,19 +43,31 @@
Однако на оплату хостинга читалки и сервера обновлений автор тратит свои Однако на оплату хостинга читалки и сервера обновлений автор тратит свои
собственные средства, а также тратит свое время и силы на улучшение проекта. собственные средства, а также тратит свое время и силы на улучшение проекта.
<br><br> <br><br>
Поддержим же материально наш ресурс, чтобы и дальше спокойно существовать и развиваться: Давайте поддержим наш ресурс, чтобы и дальше спокойно существовать и развиваться:
</div> </div>
<q-btn style="margin: 10px 50px 10px 50px" color="green-8" size="14px" no-caps @click="makeDonation"> <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-icon class="q-mr-xs" name="la la-donate" size="24px" />
Поддержать проект Поддержать проект
</q-btn> </q-btn>
<q-btn style="margin: 0 50px 20px 50px" size="14px" no-caps @click="donationDialogRemind"> <div class="row justify-center q-mt-sm">
Напомнить в следующем месяце Напомнить снова через:
</q-btn> </div>
<div class="row justify-center"> <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 class="q-px-sm clickable" style="font-size: 80%" @click="openDonate">
Помочь проекту можно в любое время Помочь проекту можно в любое время
</div> </div>
@@ -71,13 +83,8 @@
</template> </template>
<div style="word-break: normal"> <div style="word-break: normal">
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в Если вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте <q-btn no-caps dense class="q-px-sm" color="btn1" size="13px" @click="loadBufferClick">
<a href="https://liberama.top" target="_blank">liberama.top</a>
<br><br>
Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
<q-icon class="q-mr-xs" name="la la-comment" size="24px" /> <q-icon class="q-mr-xs" name="la la-comment" size="24px" />
Из буфера обмена Из буфера обмена
</q-btn> </q-btn>
@@ -94,6 +101,7 @@ import vueComponent from '../../vueComponent.js';
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';
const componentOptions = { const componentOptions = {
components: { components: {
@@ -123,7 +131,7 @@ class ReaderDialogs {
async init() { async init() {
await this.showWhatsNew(); await this.showWhatsNew();
await this.showDonation(); //await this.showDonation();
} }
loadSettings() { loadSettings() {
@@ -135,7 +143,7 @@ class ReaderDialogs {
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') &&
this.whatsNewHeader != this.whatsNewContentHash) { this.whatsNewHeader != this.whatsNewContentHash) {
await utils.sleep(2000); await utils.sleep(2000);
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content; this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
@@ -144,9 +152,7 @@ class ReaderDialogs {
} }
async showDonation() { async showDonation() {
const today = utils.formatDate(new Date(), 'coMonth'); if ((this.mode == 'omnireader' || this.mode == 'liberama') && this.showDonationDialog && this.donationNextPopup <= Date.now()) {
if ((this.mode == 'omnireader' || this.mode == 'liberama.top') && this.showDonationDialog && this.donationRemindDate != today) {
await utils.sleep(3000); await utils.sleep(3000);
this.donationVisible = true; this.donationVisible = true;
} }
@@ -161,14 +167,15 @@ class ReaderDialogs {
this.urlHelpVisible = false; this.urlHelpVisible = false;
} }
donationDialogRemind() { donationDialogRemindLater(remindAfter = 30) {
this.donationVisible = false; this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coMonth'));
this.commit('reader/setDonationNextPopup', Date.now() + rstore.dayMs*remindAfter);
} }
makeDonation() { makeDonation() {
utils.makeDonation(); utils.makeDonation();
this.donationDialogRemind(); this.donationDialogRemindLater();
} }
openDonate() { openDonate() {
@@ -209,8 +216,8 @@ class ReaderDialogs {
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() {
@@ -226,7 +233,7 @@ export default vueComponent(ReaderDialogs);
<style scoped> <style scoped>
.clickable { .clickable {
color: blue; color: var(--text-anchor-color);
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }

View File

@@ -36,29 +36,29 @@
<a ref="download" style="display: none;" target="_blank"></a> <a ref="download" style="display: none;" target="_blank"></a>
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col"> <div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
<div ref="header" class="scroll-header row bg-blue-2"> <div ref="header" class="scroll-header row bg-header-3">
<q-btn class="tool-button" round @click="showSameBookClick"> <q-btn class="tool-button" color="btn2" round @click="showSameBookClick">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" /> <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Показать/скрыть версии книг Показать/скрыть версии книг
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn class="tool-button" round @click="scrollToBegin"> <q-btn class="tool-button" color="btn2" round @click="scrollToBegin">
<q-icon name="la la-arrow-up" color="green-8" size="24px" /> <q-icon name="la la-arrow-up" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В начало списка В начало списка
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn class="tool-button" round @click="scrollToEnd"> <q-btn class="tool-button" color="btn2" round @click="scrollToEnd">
<q-icon name="la la-arrow-down" color="green-8" size="24px" /> <q-icon name="la la-arrow-down" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В конец списка В конец списка
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn class="tool-button" round @click="scrollToActiveBook"> <q-btn class="tool-button" color="btn2" round @click="scrollToActiveBook">
<q-icon name="la la-location-arrow" color="green-8" size="24px" /> <q-icon name="la la-location-arrow" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
На текущую книгу На текущую книгу
@@ -71,7 +71,7 @@
class="q-ml-sm q-mt-xs" class="q-ml-sm q-mt-xs"
outlined dense outlined dense
style="width: 185px" style="width: 185px"
bg-color="white" bg-color="input"
placeholder="Найти" placeholder="Найти"
@click.stop @click.stop
> >
@@ -86,7 +86,7 @@
class="q-ml-sm q-mt-xs" class="q-ml-sm q-mt-xs"
:options="sortMethodOptions" :options="sortMethodOptions"
style="width: 180px" style="width: 180px"
bg-color="white" bg-color="input"
dropdown-icon="la la-angle-down la-sm" dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options display-value-sanitize options-sanitize outlined dense emit-value map-options display-value-sanitize options-sanitize
options-html display-value-html options-html display-value-html
@@ -140,7 +140,7 @@
class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;" class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
:style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }" :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
> >
<div class="text-green-10" style="font-size: 80%"> <div :class="dark ? 'text-lime-4' : 'text-green-10'" style="font-size: 80%">
{{ item.desc.author }} {{ item.desc.author }}
</div> </div>
<div style="font-size: 75%"> <div style="font-size: 75%">
@@ -201,7 +201,7 @@
<div <div
class="del-button self-end row justify-center items-center clickable" class="del-button self-end row justify-center items-center clickable"
@click="handleDel(item.key)" @click="handleDel(item)"
> >
<q-icon class="la la-times" size="12px" /> <q-icon class="la la-times" size="12px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -212,7 +212,7 @@
<div <div
v-show="showArchive" v-show="showArchive"
class="restore-button self-start row justify-center items-center clickable" class="restore-button self-start row justify-center items-center clickable"
@click="handleRestore(item.key)" @click="handleRestore(item)"
> >
<q-icon class="la la-arrow-left" size="14px" /> <q-icon class="la la-arrow-left" size="14px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%"> <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -349,6 +349,10 @@ class RecentBooksPage {
return this.$store.state.config.bucEnabled && this.bucEnabled; return this.$store.state.config.bucEnabled && this.bucEnabled;
} }
get dark() {
return this.$store.state.reader.settings.nightMode;
}
async updateTableData() { async updateTableData() {
if (!this.inited) if (!this.inited)
return; return;
@@ -367,10 +371,10 @@ class RecentBooksPage {
let d = new Date(); let d = new Date();
d.setTime(book.touchTime); d.setTime(book.touchTime);
const touchTime = utils.formatDate(d); const touchTime = utils.dateFormat(d, 'DD.MM.YYYY HH:mm');
const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime); const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
d.setTime(loadTimeRaw); d.setTime(loadTimeRaw);
const loadTime = utils.formatDate(d); const loadTime = utils.dateFormat(d, 'DD.MM.YYYY HH:mm');
let readPart = 0; let readPart = 0;
let perc = ''; let perc = '';
@@ -589,26 +593,51 @@ class RecentBooksPage {
} }
} }
async handleDel(key) { async handleDel(item) {
if (!this.showArchive) { if (item.group?.length) {
await bookManager.delRecentBook({key}); const keys = [{key: item.key}];
this.$root.notify.info('Перенесено в архив'); for (const book of item.group)
keys.push({key: book.key});
if (!this.showArchive) {
await bookManager.delRecentBooks(keys);
this.$root.notify.info(`Группа книг (всего ${keys.length}) перенесена в архив`);
} else {
if (await this.$root.stdDialog.confirm(`Подтвердите удаление группы книг (всего ${keys.length}) из архива:`, ' ')) {
await bookManager.delRecentBooks(keys, 2);
this.$root.notify.info('Группа книг удалена безвозвратно');
}
}
} else { } else {
if (await this.$root.stdDialog.confirm('Подтвердите удаление из архива:', ' ')) { if (!this.showArchive) {
await bookManager.delRecentBook({key}, 2); await bookManager.delRecentBooks([{key: item.key}]);
this.$root.notify.info('Удалено безвозвратно'); this.$root.notify.info('Книга перенесена в архив');
} else {
if (await this.$root.stdDialog.confirm('Подтвердите удаление книги из архива:', ' ')) {
await bookManager.delRecentBooks([{key: item.key}], 2);
this.$root.notify.info('Книга удалена безвозвратно');
}
} }
} }
} }
async handleRestore(key) { async handleRestore(item) {
await bookManager.restoreRecentBook({key}); if (item.group?.length) {
this.$root.notify.info('Восстановлено из архива'); const keys = [{key: item.key}];
for (const book of item.group)
keys.push({key: book.key});
await bookManager.restoreRecentBooks(keys);
this.$root.notify.info(`Группа книг (всего ${keys.length}) восстановлена из архива`);
} else {
await bookManager.restoreRecentBooks([{key: item.key}]);
this.$root.notify.info('Книга восстановлена из архива');
}
} }
async loadBook(item, force = false) { async loadBook(item, force = false) {
if (item.deleted) if (item.deleted)
await this.handleRestore(item.key); await this.handleRestore(item);
this.$emit('load-book', {url: item.url, path: item.path, force}); this.$emit('load-book', {url: item.url, path: item.path, force});
this.close(); this.close();
@@ -847,7 +876,7 @@ export default vueComponent(RecentBooksPage);
position: sticky; position: sticky;
z-index: 1; z-index: 1;
top: 0; top: 0;
border-bottom: 2px solid #aaaaaa; border-bottom: 2px solid var(--bg-menu-color2);
padding-left: 5px; padding-left: 5px;
} }
@@ -870,15 +899,15 @@ export default vueComponent(RecentBooksPage);
} }
.even { .even {
background-color: #f2f2f2; background-color: var(--bg-menu-color1);
} }
.active-book { .active-book {
background-color: #b0f0b0 !important; background-color: var(--bg-selected-item-color1) !important;
} }
.active-parent-book { .active-parent-book {
background-color: #ffbbbb !important; background-color: var(--bg-selected-item-color2) !important;
} }
.icon { .icon {
@@ -895,7 +924,6 @@ export default vueComponent(RecentBooksPage);
min-height: 30px; min-height: 30px;
height: 30px; height: 30px;
margin: 10px 6px 0px 3px; margin: 10px 6px 0px 3px;
background-color: white;
} }
.row-info-bottom { .row-info-bottom {

View File

@@ -11,6 +11,7 @@
<q-input <q-input
ref="input" v-model="needle" ref="input" v-model="needle"
class="col" outlined dense class="col" outlined dense
bg-color="input"
placeholder="Найти" placeholder="Найти"
@keydown="inputKeyDown" @keydown="inputKeyDown"
/> />
@@ -20,10 +21,10 @@
</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-btn class="button" dense stretch @click="showNext">
<q-icon style="top: -6px" name="la la-angle-down" dense size="22px" /> <q-icon style="top: -2px" name="la la-angle-down" dense size="22px" />
</q-btn> </q-btn>
<q-btn class="button" dense stretch @click="showPrev"> <q-btn class="button" dense stretch @click="showPrev">
<q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" /> <q-icon name="la la-angle-up" dense size="22px" />
</q-btn> </q-btn>
</q-btn-group> </q-btn-group>
</div> </div>
@@ -108,10 +109,15 @@ class SearchPage {
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}`;
@@ -149,7 +155,8 @@ class SearchPage {
} else { } else {
this.$emit('stop-text-search'); this.$emit('stop-text-search');
} }
this.$refs.input.focus();
this.focusInput();
} }
showPrev() { showPrev() {
@@ -165,7 +172,8 @@ class SearchPage {
} else { } else {
this.$emit('stop-text-search'); this.$emit('stop-text-search');
} }
this.$refs.input.focus();
this.focusInput();
} }
close() { close() {

View File

@@ -22,10 +22,12 @@ const ssCacheStore = localForage.createInstance({
const componentOptions = { 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();
@@ -49,6 +51,7 @@ class ServerStorage {
this.keyInited = false; this.keyInited = false;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.prevServerStorageKey = null; this.prevServerStorageKey = null;
this.identity = utils.randomHexString(20);
this.lock = new LockQueue(100); this.lock = new LockQueue(100);
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()}; this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
@@ -84,6 +87,13 @@ class ServerStorage {
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();
@@ -122,6 +132,7 @@ class ServerStorage {
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);
} }
@@ -140,6 +151,10 @@ class ServerStorage {
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;
@@ -204,6 +219,10 @@ class ServerStorage {
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', '');
@@ -643,6 +662,8 @@ class ServerStorage {
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.lock.ret(); this.lock.ret();
@@ -665,7 +686,7 @@ class ServerStorage {
} }
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);

View File

@@ -80,7 +80,7 @@ export default vueComponent(SetPositionPage);
.slider { .slider {
margin: 0 20px 0 20px; margin: 0 20px 0 20px;
height: 35px; height: 35px;
background-color: #efefef; background-color: var(--bg-input-color);
border-radius: 15px; border-radius: 15px;
} }
</style> </style>

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

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

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

@@ -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,10 +2,10 @@
<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 class="desc q-pa-sm bg-header-3">
Команда Команда
</div> </div>
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap"> <div class="hotKeys col q-pa-sm bg-header-3 row no-wrap">
<div style="width: 80px"> <div style="width: 80px">
Сочетание клавиш Сочетание клавиш
</div> </div>
@@ -14,7 +14,7 @@
v-model="search" v-model="search"
class="q-ml-sm col" class="q-ml-sm col"
outlined dense outlined dense
bg-color="grey-4" bg-color="input"
placeholder="Найти" placeholder="Найти"
@click.stop @click.stop
/> />
@@ -73,10 +73,9 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js'; import vueComponent from '../../../../vueComponent.js';
import rstore from '../../../../store/modules/reader'; import rstore from '../../../../../store/modules/reader';
//import * as utils from '../../share/utils';
const componentOptions = { const componentOptions = {
watch: { watch: {
@@ -116,7 +115,7 @@ class UserHotKeys {
} }
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) => {
@@ -235,11 +234,11 @@ export default vueComponent(UserHotKeys);
} }
.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

@@ -1,91 +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="showDonationDialog">
Показывать форму доната
<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

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

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

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

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

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

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

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

@@ -5,113 +5,51 @@
</template> </template>
<div class="col row"> <div class="col row">
<a ref="download" style="display: none;" target="_blank"></a>
<div class="full-height"> <div class="full-height">
<q-tabs <q-tabs
ref="tabs" ref="tabs"
v-model="selectedTab" v-model="selectedTab"
class="bg-grey-3 text-black" 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="toolbar" 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="update" icon="la la-sync" 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('./ProfilesTab.inc');
</div>
<!-- Вид -------------------------------------------------------------------------> <!-- Вид ------------------------------------------------------------------------->
<div v-if="selectedTab == 'view'" class="fit column"> <ViewTab v-if="selectedTab == 'view'" :form="form" @tab-event="tabEvent" />
<q-tabs
v-model="selectedViewTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
class="no-mp bg-grey-4 text-grey-7"
>
<q-tab name="mode" label="Режим" />
<q-tab name="color" label="Цвет" />
<q-tab name="font" label="Шрифт" />
<q-tab name="text" label="Текст" />
<q-tab name="status" label="Строка статуса" />
</q-tabs>
<div class="q-mb-sm" />
<div class="col tab-panel">
<div v-if="selectedViewTab == 'mode'">
@@include('./ViewTab/Mode.inc');
</div>
<div v-if="selectedViewTab == 'color'">
@@include('./ViewTab/Color.inc');
</div>
<div v-if="selectedViewTab == 'font'">
@@include('./ViewTab/Font.inc');
</div>
<div v-if="selectedViewTab == 'text'">
@@include('./ViewTab/Text.inc');
</div>
<div v-if="selectedViewTab == 'status'">
@@include('./ViewTab/Status.inc');
</div>
</div>
</div>
<!-- Кнопки ----------------------------------------------------------------------> <!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel"> <ToolBarTab v-if="selectedTab == 'toolbar'" :form="form" />
@@include('./ToolBarTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------> <!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column"> <KeysTab v-if="selectedTab == 'keys'" :form="form" />
@@include('./KeysTab.inc');
</div>
<!-- Листание --------------------------------------------------------------------> <!-- Листание -------------------------------------------------------------------->
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel"> <PageMoveTab v-if="selectedTab == 'pagemove'" :form="form" />
@@include('./PageMoveTab.inc');
</div>
<!-- Конвертирование -------------------------------------------------------------> <!-- Конвертирование ------------------------------------------------------------->
<div v-if="selectedTab == 'convert'" class="fit tab-panel"> <ConvertTab v-if="selectedTab == 'convert'" :form="form" />
@@include('./ConvertTab.inc');
</div>
<!-- Обновление ------------------------------------------------------------------> <!-- Обновление ------------------------------------------------------------------>
<div v-if="selectedTab == 'update'" class="fit tab-panel"> <UpdateTab v-if="selectedTab == 'update'" :form="form" />
@@include('./UpdateTab.inc');
</div>
<!-- Прочее ----------------------------------------------------------------------> <!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel"> <OthersTab v-if="selectedTab == 'others'" :form="form" />
@@include('./OthersTab.inc'); <!-- Сброс ----------------------------------------------------------------------->
</div> <ResetTab v-if="selectedTab == 'reset'" :form="form" @tab-event="tabEvent" />
<!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./ResetTab.inc');
</div>
</div> </div>
</div> </div>
</Window> </Window>
@@ -119,152 +57,86 @@
<script> <script>
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import { ref, watch } from 'vue';
import vueComponent from '../../vueComponent.js'; import vueComponent from '../../vueComponent.js';
import { reactive } from 'vue';
import _ from 'lodash'; import _ from 'lodash';
import * as utils from '../../../share/utils'; //stuff
import * as cryptoUtils from '../../../share/cryptoUtils';
import Window from '../../share/Window.vue'; import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
import readerApi from '../../../api/reader';
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';
const componentOptions = { 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', _.cloneDeep(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.fontVertShift != 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 = '';
},
dualPageMode(newValue) {
if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
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;
},
dualDivColor(newValue) {
this.dualDivColorFiltered = newValue;
},
dualDivColorFiltered(newValue) {
if (hex.test(newValue))
this.dualDivColor = newValue;
},
statusBarColor(newValue) {
this.statusBarColorFiltered = newValue;
},
statusBarColorFiltered(newValue) {
if (hex.test(newValue))
this.statusBarColor = newValue;
}, },
}, },
}; };
class SettingsPage { class SettingsPage {
_options = componentOptions; _options = componentOptions;
form = {};
tabs = [
{ 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'; selectedTab = 'profiles';
selectedViewTab = 'mode';
selectedKeysTab = 'mouse';
fontBold = false;
fontItalic = false;
vertShift = 0;
tabsScrollable = false;
textColorFiltered = '';
bgColorFiltered = '';
dualDivColorFiltered = '';
webFonts = []; isSetsChanged = false;
fonts = [];
serverStorageKeyVisible = false;
toolButtons = [];
rstore = {};
setup() {
const settingsProps = { form: ref({}) };
for (let prop in rstore.settingDefaults) {
settingsProps[prop] = ref(_.cloneDeep(rstore.settingDefaults[prop]));
watch(settingsProps[prop], (newValue) => {
settingsProps.form.value = Object.assign({}, settingsProps.form.value, {[prop]: newValue});
}, {deep: true});
}
return settingsProps;
}
created() { 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.$root.isMobileDevice;
}
);
} }
init() { init() {
@@ -272,194 +144,20 @@ class SettingsPage {
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 {
for (const prop in rstore.settingDefaults) { await this.$nextTick();
this[prop] = _.cloneDeep(this.form[prop]); this.isSetsChanged = false;
} }
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;
this.dualDivColorFiltered = this.dualDivColor;
this.statusBarColorFiltered = this.statusBarColor;
}
get mode() {
return this.$store.state.config.mode;
}
get isExternalConverter() {
return this.$store.state.config.useExternalBookConverter;
} }
get settings() { 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 configBucEnabled() {
return this.$store.state.config.bucEnabled;
}
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: ''}];
const userWallpapers = _.cloneDeep(this.userWallpapers);
userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
for (const wp of userWallpapers) {
if (wallpaperStorage.keyExists(wp.cssClass))
result.push({label: wp.label, value: wp.cssClass});
}
for (let i = 1; i <= 17; i++) {
result.push({label: i, value: `paper${i}`});
}
return result;
}
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'},
(!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
{label: 'Протаивание', value: 'thaw'},
{label: 'Мерцание', value: 'blink'},
{label: 'Вращение', value: 'rotate'},
(this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
];
result = result.filter(v => v);
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;
case 'div':
result += `background-color: ${this.dualDivColor};`
break;
case 'statusbar':
result += `background-color: ${this.statusBarColor};`
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'});
} }
@@ -467,242 +165,20 @@ class SettingsPage {
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) {
// //
} }
} }
async addProfile() { tabEvent(event) {
try { if (!event || !event.action)
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.$root.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() == 'да') {
if (this.$root.generateNewServerStorageKey)
this.$root.generateNewServerStorageKey();
}
} catch (e) {
//
}
}
loadWallpaperFileClick() {
this.$refs.file.click();
}
loadWallpaperFile() {
const file = this.$refs.file.files[0];
if (file.size > 10*1024*1024) {
this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
return;
}
if (file.type != 'image/png' && file.type != 'image/jpeg') {
this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
return;
}
if (this.userWallpapers.length >= 100) {
this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
return;
}
this.$refs.file.value = '';
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
(async() => {
const data = e.target.result;
const key = utils.toHex(cryptoUtils.sha256(data));
const label = `#${key.substring(0, 4)}`;
const cssClass = `user-paper${key}`;
const newUserWallpapers = _.cloneDeep(this.userWallpapers);
const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
if (index < 0)
newUserWallpapers.push({label, cssClass});
if (!wallpaperStorage.keyExists(cssClass)) {
await wallpaperStorage.setData(cssClass, data);
//отправим data на сервер в файл `/upload/${key}`
try {
//const res =
await readerApi.uploadFileBuf(data);
//console.log(res);
} catch (e) {
console.error(e);
}
}
this.userWallpapers = newUserWallpapers;
this.wallpaper = cssClass;
})();
}
reader.readAsDataURL(file);
}
}
async delWallpaper() {
if (this.wallpaper.indexOf('user-paper') == 0) {
const newUserWallpapers = [];
for (const wp of this.userWallpapers) {
if (wp.cssClass != this.wallpaper) {
newUserWallpapers.push(wp);
}
}
await wallpaperStorage.removeData(this.wallpaper);
this.userWallpapers = newUserWallpapers;
this.wallpaper = '';
}
}
async downloadWallpaper() {
if (this.wallpaper.indexOf('user-paper') != 0)
return;
try {
const d = this.$refs.download;
const dataUrl = await wallpaperStorage.getData(this.wallpaper);
if (!dataUrl)
throw new Error('Файл обоев не найден');
d.href = dataUrl;
d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
d.click();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
} }
} }
@@ -722,15 +198,17 @@ export default vueComponent(SettingsPage);
.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%;
@@ -738,25 +216,7 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px; margin-bottom: 5px;
} }
.item { .sets-label {
width: 100%;
margin-top: 5px;
margin-bottom: 5px;
}
.label-1, .label-3, .label-7 {
width: 75px;
}
.label-2, .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;
@@ -765,33 +225,14 @@ export default vueComponent(SettingsPage);
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

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

View File

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

@@ -1,76 +0,0 @@
<!---------------------------------------------->
<div class="part-header">Обновление читалки</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="part-header">Обновление книг</div>
<div v-show="!configBucEnabled" class="item row">
<div class="label-6"></div>
<div>Сервер обновлений временно не работает</div>
</div>
<div v-show="configBucEnabled" class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="bucEnabled">
Проверять обновления книг
</q-checkbox>
</div>
<div v-show="configBucEnabled && bucEnabled" class="item row">
<div class="label-6"></div>
<div class="col-5 column justify-center items-end q-pr-xs">Разница размеров</div>
<div class="col row">
<NumInput class="col-left" v-model="bucSizeDiff" />
<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 && bucEnabled" class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="bucSetOnNew">
Автопроверка для вновь загружаемых
<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 && bucEnabled" class="item row">
<div class="label-6"></div>
<q-checkbox size="xs" v-model="bucCancelEnabled">
Отменять проверку через {{ bucCancelDays }} дней{{ (bucCancelEnabled ? ':' : '') }}
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Снимать флаг проверки с книги, если не было<br>
обновлений в течение {{ bucCancelDays }} дней
</q-tooltip>
</q-checkbox>
</div>
<div v-show="configBucEnabled && bucEnabled && bucCancelEnabled" class="item row">
<div class="label-6"></div>
<div class="col-5"></div>
<div class="col row">
<NumInput class="col-left" v-model="bucCancelDays" :min="1" :max="10000"/>
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Снимать флаг проверки с книги, если не было<br>
обновлений в течение {{ bucCancelDays }} дней
</q-tooltip>
</div>
</div>

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

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

View File

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

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

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

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

View File

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

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

View File

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

@@ -1,127 +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="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" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
</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

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

@@ -14,6 +14,11 @@ 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;
@@ -46,7 +51,22 @@ export default class DrawHelper {
tOpen += (part.style.italic ? '<i>' : ''); 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.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">' : ''); 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 = ''; let tClose = '';
tClose += (part.style.note ? '</span>' : '');
tClose += (part.style.sub ? '</span>' : ''); tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : ''); tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : ''); tClose += (part.style.italic ? '</i>' : '');

View File

@@ -4,34 +4,30 @@
<div class="absolute" v-html="background"></div> <div class="absolute" v-html="background"></div>
<div class="absolute" v-html="pageDivider"></div> <div class="absolute" v-html="pageDivider"></div>
</div> </div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox1" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd"> <div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div @copy.prevent="copyText" v-html="page1"></div> <div @copy.prevent="copyText" v-html="page1"></div>
</div> </div>
</div> </div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel"> <div ref="scrollBox2" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd"> <div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div @copy.prevent="copyText" v-html="page2"></div> <div @copy.prevent="copyText" v-html="page2"></div>
</div> </div>
</div> </div>
<div v-show="showStatusBar" ref="statusBar" class="layout"> <div v-show="showStatusBar" ref="statusBar" class="layout" :class="{'no-events': clickControl}">
<div v-html="statusBar"></div> <div v-html="statusBar"></div>
</div> </div>
<div <div
v-show="clickControl" ref="layoutEvents" class="layout events" v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;" oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@mouseover.prevent.stop="onMouseEvent" @mouseout.prevent.stop="onMouseEvent" @mousemove.prevent.stop="onMouseEvent"
@wheel.prevent.stop="onMouseWheel" @wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel" @touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
> >
<div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div> </div>
<div <div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-show="showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick" @click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable" v-html="statusBarClickable"
@@ -40,6 +36,29 @@
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты --> <!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas> <canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div> <div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
<!-- Примечание -->
<Dialog ref="dialog1" v-model="noteDialogVisible">
<template #header>
{{ noteTitle }}
</template>
<div class="column col" style="line-height: 20px; max-width: 400px; max-height: 200px; overflow-x: hidden; overflow-y: auto">
<div v-html="noteHtml"></div>
</div>
<template #footer>
<div class="row col">
<q-btn class="q-px-md q-mr-md" color="btn2" text-color="app" dense no-caps @click="goToNotes">
В примечания
</q-btn>
</div>
<q-btn class="q-px-md" color="btn2" text-color="app" dense no-caps @click="noteDialogVisible = false">
OK
</q-btn>
</template>
</Dialog>
</div> </div>
</template> </template>
@@ -51,6 +70,7 @@ import {loadCSS} from 'fg-loadcss';
import _ from 'lodash'; import _ from 'lodash';
import he from 'he'; import he from 'he';
import Dialog from '../../share/Dialog.vue';
import './TextPage.css'; import './TextPage.css';
import * as utils from '../../../share/utils'; import * as utils from '../../../share/utils';
@@ -62,7 +82,19 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100; const minLayoutWidth = 100;
//обработчик кликов по примечаниям, см. DrawHelper
//коряво, но иначе придется сильно усложнять рендеринг страниц (через Vue)
window.onNoteClickLiberama = (noteId, orig) => {
const textPage = window.textPageLiberama;
if (textPage) {
textPage.showNote(noteId, orig);
}
}
const componentOptions = { const componentOptions = {
components: {
Dialog
},
watch: { watch: {
bookPos: function() { bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen}); this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -81,9 +113,6 @@ const componentOptions = {
settings: function() { settings: function() {
this.debouncedLoadSettings(); this.debouncedLoadSettings();
}, },
toggleLayout: function() {
this.updateLayout();
},
inAnimation: function() { inAnimation: function() {
this.updateLayout(); this.updateLayout();
}, },
@@ -92,8 +121,8 @@ const componentOptions = {
class TextPage { class TextPage {
_options = componentOptions; _options = componentOptions;
toggleLayout = false;
showStatusBar = false; showStatusBar = false;
statusBarClickOpen = false;
clickControl = true; clickControl = true;
background = null; background = null;
@@ -118,6 +147,11 @@ class TextPage {
meta = null; meta = null;
noteDialogVisible = false;
noteId = '';
noteTitle = '';
noteHtml = '';
created() { created() {
this.drawHelper = new DrawHelper(); this.drawHelper = new DrawHelper();
@@ -130,10 +164,6 @@ class TextPage {
this.startClickRepeat(x, y); this.startClickRepeat(x, y);
}, 800); }, 800);
this.debouncedPrepareNextPage = _.debounce(() => {
this.prepareNextPage();
}, 100);
this.debouncedDrawStatusBar = _.throttle(() => { this.debouncedDrawStatusBar = _.throttle(() => {
this.drawStatusBar(); this.drawStatusBar();
}, 60); }, 60);
@@ -147,17 +177,11 @@ class TextPage {
}, 50); }, 50);
this.debouncedUpdatePage = _.debounce(async(lines) => { this.debouncedUpdatePage = _.debounce(async(lines) => {
if (!this.pageChangeAnimation) if (this.pageChangeAnimation) {
this.toggleLayout = !this.toggleLayout;
else {
this.page2 = this.page1; this.page2 = this.page1;
this.toggleLayout = true;
} }
if (this.toggleLayout) this.page1 = this.drawHelper.drawPage(lines);
this.page1 = this.drawHelper.drawPage(lines);
else
this.page2 = this.drawHelper.drawPage(lines);
await this.doPageAnimation(); await this.doPageAnimation();
}, 10); }, 10);
@@ -167,6 +191,8 @@ class TextPage {
await utils.sleep(200); await utils.sleep(200);
this.$nextTick(this.onResize); this.$nextTick(this.onResize);
}); });
window.textPageLiberama = this;
} }
mounted() { mounted() {
@@ -174,7 +200,12 @@ class TextPage {
} }
hex2rgba(hex, alpha = 1) { hex2rgba(hex, alpha = 1) {
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)); let [r, g, b] = [0, 0, 0];
if (hex.length <= 4) {
[r, g, b] = hex.match(/\w/g).map(x => parseInt(x + x, 16));
} else {
[r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
}
return `rgba(${r},${g},${b},${alpha})`; return `rgba(${r},${g},${b},${alpha})`;
} }
@@ -306,6 +337,8 @@ class TextPage {
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0); top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
let page1 = this.$refs.scrollBox1.style; let page1 = this.$refs.scrollBox1.style;
let page2 = this.$refs.scrollBox2.style; let page2 = this.$refs.scrollBox2.style;
page1.pointerEvents = page2.pointerEvents = (this.clickControl ? 'none' : 'auto');
page1.perspective = page2.perspective = '3072px'; page1.perspective = page2.perspective = '3072px';
@@ -425,7 +458,6 @@ class TextPage {
showBook() { showBook() {
this.$refs.main.focus(); this.$refs.main.focus();
this.toggleLayout = false;
this.updateLayout(); this.updateLayout();
this.book = null; this.book = null;
this.meta = null; this.meta = null;
@@ -443,10 +475,6 @@ class TextPage {
if (this.lastBook) { if (this.lastBook) {
(async() => { (async() => {
try { try {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await utils.sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook); const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) { if (!isParsed) {
return; return;
@@ -470,8 +498,6 @@ class TextPage {
await this.calcPropsAndLoadFonts(); await this.calcPropsAndLoadFonts();
this.refreshTime(); this.refreshTime();
if (this.lazyParseEnabled)
this.lazyParsePara();
} catch (e) { } catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'}); this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
} }
@@ -483,12 +509,9 @@ class TextPage {
if (this.inAnimation) { if (this.inAnimation) {
this.$refs.scrollBox1.style.visibility = 'visible'; this.$refs.scrollBox1.style.visibility = 'visible';
this.$refs.scrollBox2.style.visibility = 'visible'; this.$refs.scrollBox2.style.visibility = 'visible';
} else if (this.toggleLayout) { } else {
this.$refs.scrollBox1.style.visibility = 'visible'; this.$refs.scrollBox1.style.visibility = 'visible';
this.$refs.scrollBox2.style.visibility = 'hidden'; this.$refs.scrollBox2.style.visibility = 'hidden';
} else {
this.$refs.scrollBox1.style.visibility = 'hidden';
this.$refs.scrollBox2.style.visibility = 'visible';
} }
} }
@@ -589,28 +612,25 @@ class TextPage {
const transitionFinish = this.generateWaitingFunc('resolveTransition1Finish', 'stopScrolling'); const transitionFinish = this.generateWaitingFunc('resolveTransition1Finish', 'stopScrolling');
if (!this.toggleLayout)
this.page1 = this.page2;
this.toggleLayout = true;
await this.$nextTick();
await utils.sleep(50);
this.cachedPos = -1; this.cachedPos = -1;
this.draw(); this.draw();
const page = this.$refs.scrollingPage1; const page = this.$refs.scrollingPage1;
let i = 0; let i = 0;
while (!this.stopScrolling) { while (!this.stopScrolling) {
page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
page.style.transform = `translateY(-${this.lineHeight}px)`;
if (i > 0) { if (i > 0) {
this.doDown(); this.doDown();
await utils.sleep(1);
await this.$nextTick();
if (this.linesDown.length <= this.pageLineCount + 1) { if (this.linesDown.length <= this.pageLineCount + 1) {
this.stopScrolling = true; this.stopScrolling = true;
} }
} }
page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
page.style.transform = `translateY(-${this.lineHeight}px)`;
await transitionFinish(this.scrollingDelay); await transitionFinish(this.scrollingDelay);
page.style.transition = ''; page.style.transition = '';
page.style.transform = 'none'; page.style.transform = 'none';
page.offsetHeight; page.offsetHeight;
@@ -678,21 +698,11 @@ class TextPage {
return; return;
} }
//fast draw prepared const lines = this.getLines(this.bookPos);
if (!this.pageChangeAnimation && this.pageChangeDirectionDown && this.pagePrepared && this.bookPos == this.bookPosPrepared) { this.linesDown = lines.linesDown;
this.toggleLayout = !this.toggleLayout; this.linesUp = lines.linesUp;
this.linesDown = this.linesDownNext; this.debouncedUpdatePage(lines.linesDown);
this.linesUp = this.linesUpNext;
} else {//normal debounced draw
const lines = this.getLines(this.bookPos);
this.linesDown = lines.linesDown;
this.linesUp = lines.linesUp;
this.debouncedUpdatePage(lines.linesDown);
}
this.pagePrepared = false;
if (!this.pageChangeAnimation)
this.debouncedPrepareNextPage();
this.debouncedDrawStatusBar(); this.debouncedDrawStatusBar();
this.debouncedDrawPageDividerAndOrnament(); this.debouncedDrawPageDividerAndOrnament();
@@ -864,36 +874,6 @@ class TextPage {
this.drawStatusBar(); this.drawStatusBar();
} }
async lazyParsePara() {
if (!this.parsed || this.doingLazyParse)
return;
this.doingLazyParse = true;
let j = 0;
let k = 0;
let prevPerc = 0;
this.stopLazyParse = false;
for (let i = 0; i < this.parsed.para.length; i++) {
j++;
if (j > 1) {
await utils.sleep(1);
j = 0;
}
if (this.stopLazyParse)
break;
this.parsed.parsePara(i);
k++;
if (k > 100) {
let perc = Math.round(i/this.parsed.para.length*100);
if (perc != prevPerc)
this.drawStatusBar(`Обработка текста ${perc}%`);
prevPerc = perc;
k = 0;
}
}
this.drawStatusBar();
this.doingLazyParse = false;
}
async refreshTime() { async refreshTime() {
if (!this.timeRefreshing) { if (!this.timeRefreshing) {
this.timeRefreshing = true; this.timeRefreshing = true;
@@ -907,30 +887,6 @@ class TextPage {
} }
} }
prepareNextPage() {
// подготовка следующей страницы заранее
if (!this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1)
return;
let i = this.pageLineCount;
if (this.keepLastToFirst)
i--;
if (i >= 0 && this.linesDown.length > i) {
this.bookPosPrepared = this.linesDown[i].begin;
const lines = this.getLines(this.bookPosPrepared);
this.linesDownNext = lines.linesDown;
this.linesUpNext = lines.linesUp;
if (this.toggleLayout)
this.page2 = this.drawHelper.drawPage(lines.linesDown);//наоборот
else
this.page1 = this.drawHelper.drawPage(lines.linesDown);
this.pagePrepared = true;
}
}
doDown() { doDown() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) { if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true; this.userBookPosChange = true;
@@ -999,6 +955,22 @@ class TextPage {
} }
} }
doPara(paraIndex) {
const para = this.parsed.para[paraIndex];
if (para && this.pageLineCount > 0) {
const lines = this.parsed.getLines(para.offset, this.pageLineCount);
if (lines.length >= this.pageLineCount) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = lines[0].begin;
} else
this.doEnd();
}
}
doToolBarToggle(event) { doToolBarToggle(event) {
this.$emit('do-action', {action: 'switchToolbar', event}); this.$emit('do-action', {action: 'switchToolbar', event});
} }
@@ -1102,6 +1074,7 @@ class TextPage {
if (this.startTouch) { if (this.startTouch) {
event.preventDefault(); event.preventDefault();
} }
this.endClickRepeat();
} }
onTouchEnd(event) { onTouchEnd(event) {
@@ -1117,6 +1090,7 @@ class TextPage {
if (this.startTouch) { if (this.startTouch) {
const dy = this.startTouch.y - y; const dy = this.startTouch.y - y;
const dx = this.startTouch.x - x; const dx = this.startTouch.x - x;
this.startTouch = null;
const moveDelta = 30; const moveDelta = 30;
const touchDelta = 15; const touchDelta = 15;
if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) { if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
@@ -1132,10 +1106,23 @@ class TextPage {
//движение вправо //движение вправо
this.doScrollingSpeedUp(); this.doScrollingSpeedUp();
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) { } else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
this.doToolBarToggle(event); if (this.touchMode) {
} this.touchMode = 2;
return;
}
this.startTouch = null; (async() => {
this.touchMode = 1;
let i = 20;
while (i-- > 0 && this.touchMode === 1)
await utils.sleep(10);
if (this.touchMode === 1)
this.doToolBarToggle();
else
this.doFullScreenToggle();
this.touchMode = 0;
})();
}
} }
} }
} }
@@ -1172,6 +1159,9 @@ class TextPage {
onMouseWheel(event) { onMouseWheel(event) {
if (this.$root.isMobileDevice) if (this.$root.isMobileDevice)
return; return;
this.endClickRepeat();
if (event.deltaY > 0) { if (event.deltaY > 0) {
this.doDown(); this.doDown();
} else if (event.deltaY < 0) { } else if (event.deltaY < 0) {
@@ -1179,6 +1169,12 @@ class TextPage {
} }
} }
onMouseEvent() {
if (this.$root.isMobileDevice)
return;
this.endClickRepeat();
}
onStatusBarClick() { onStatusBarClick() {
const url = this.meta.url; const url = this.meta.url;
if (url && url.indexOf('disk://') != 0) { if (url && url.indexOf('disk://') != 0) {
@@ -1281,6 +1277,43 @@ class TextPage {
event.clipboardData.setData('text/plain', filtered); event.clipboardData.setData('text/plain', filtered);
} }
showNote(noteId, orig) {
const note = this.parsed.notes[noteId];
if (note) {
if (orig) {//show dialog
this.noteId = noteId;
this.noteTitle = `[${note.title?.trim()}]`;
this.noteHtml = note.xml
.replace(/<p>/g, '<p class="note-para">')
.replace(/<stanza>/g, '<br>').replace(/<\/stanza>/g, '')
.replace(/<v>/g, '<p style="margin: 0">').replace(/<\/v>/g, '</p>')
.replace(/<emphasis>/g, '<em>').replace(/<\/emphasis>/g, '</em>')
.replace(/<text-author>/g, '<br>').replace(/<\/text-author>/g, '')
;
this.noteDialogVisible = true;
} else {//go to orig
this.goToOrigNote(noteId);
}
}
}
goToNotes() {
const note = this.parsed.notes[this.noteId];
if (note && note.noteParaIndex >= 0) {
this.doPara(note.noteParaIndex);
this.noteDialogVisible = false;
}
}
goToOrigNote(noteId) {
const note = this.parsed.notes[noteId];
if (note && note.linkParaIndex >= 0) {
this.doPara(note.linkParaIndex);
this.noteDialogVisible = false;
}
}
} }
export default vueComponent(TextPage); export default vueComponent(TextPage);
@@ -1316,8 +1349,18 @@ export default vueComponent(TextPage);
} }
.events { .events {
z-index: 20; z-index: 9;
background-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);
} }
.no-events {
pointer-events: none;
}
</style> </style>
<style>
.note-para {
margin: 0;
padding: 0;
margin-bottom: 10px;
}
</style>

View File

@@ -86,17 +86,24 @@ export default class BookParser {
let binaryType = ''; let binaryType = '';
let dimPromises = []; let dimPromises = [];
this.coverPageId = ''; 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;
@@ -289,7 +296,7 @@ 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 (local) {//local if (local) {//local
imageNum++; imageNum++;
@@ -322,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 = [];
@@ -350,6 +374,11 @@ 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 => {
@@ -373,6 +402,31 @@ export default class BookParser {
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(); newParagraph();
isFirstTitlePara = true; isFirstTitlePara = true;
@@ -384,13 +438,6 @@ export default class BookParser {
this.contents.push(curTitle); this.contents.push(curTitle);
} }
if (tag == 'section') {
if (!isFirstSection)
newParagraph();
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);
} }
@@ -401,6 +448,10 @@ export default class BookParser {
if (tag == 'p') { if (tag == 'p') {
inPara = true; inPara = true;
isFirstTitlePara = false; isFirstTitlePara = false;
if (inTitle && inNotesBody && noteId) {
growParagraph(`<note href="${noteId}">`, 0);
}
} }
} }
@@ -434,65 +485,88 @@ export default class BookParser {
bold = true; 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;
newParagraph();
}
if (tag == 'stanza') {
newParagraph();
}
if (tag == 'text-author') {
bold = false;
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;
}
} }
}; };
@@ -568,6 +642,14 @@ export default class BookParser {
growParagraph(`${tOpen}${text}${tClose}`, text.length, text); growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else else
growParagraph(' ', 1); growParagraph(' ', 1);
if (inNotesBody && noteId) {
if (inTitle) {
this.notes[noteId].title += text;
} else {
this.notes[noteId].xml += text;
}
}
} }
}; };
@@ -600,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);
@@ -633,7 +715,7 @@ 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,
}*/ }*/
@@ -684,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);
} }
@@ -693,7 +775,7 @@ 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({
@@ -704,6 +786,13 @@ export default class BookParser {
} }
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;
}
} }
}; };
@@ -732,6 +821,9 @@ export default class BookParser {
break; break;
case 'image-inline': case 'image-inline':
break; break;
case 'note':
style.note = false;
break;
} }
}; };

View File

@@ -467,7 +467,7 @@ class BookManager {
async getRecentBook(value) { async getRecentBook(value) {
return this.recent[value.key]; return this.recent[value.key];
} }
/*
async delRecentBook(value, delFlag = 1) { async delRecentBook(value, delFlag = 1) {
const item = this.recent[value.key]; const item = this.recent[value.key];
item.deleted = delFlag; item.deleted = delFlag;
@@ -479,13 +479,37 @@ 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) { async restoreRecentBook(value) {
const item = this.recent[value.key]; const item = this.recent[value.key];
item.deleted = 0; item.deleted = 0;
await this.recentSetItem(item); 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) { async setCheckBuc(value, checkBuc) {
const item = this.recent[value.key]; const item = this.recent[value.key];

View File

@@ -1,4 +1,154 @@
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', version: '0.12.2',
releaseDate: '2022-09-04', releaseDate: '2022-09-04',

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Settings в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../vueComponent.js';
class Settings {
created() {
}
}
export default vueComponent(Settings);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,19 +0,0 @@
<template>
<div>
Раздел Sources в разработке
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../vueComponent.js';
class Sources {
created() {
}
}
export default vueComponent(Sources);
//-----------------------------------------------------------------------------
</script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide"> <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>

View File

@@ -27,7 +27,6 @@ class Notify {
icon, icon,
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}], actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
html: true, html: true,
classes: 'notify-margin',
message: message:
`<div style="max-width: 350px"> `<div style="max-width: 350px">

View File

@@ -4,17 +4,28 @@
outlined dense outlined dense
input-style="text-align: center" input-style="text-align: center"
class="no-mp" class="no-mp"
:class="(error ? 'error' : '')"
:disable="disable" :disable="disable"
:mask="mask"
:error="error"
> >
<slot></slot> <slot></slot>
<template #prepend> <template #prepend>
<q-icon
v-show="mmButtons"
v-ripple="modelValue != min"
style="font-size: 100%"
:class="(modelValue != min ? '' : 'disable')"
name="la la-angle-double-left"
class="button"
@click="toMin"
/>
<q-icon <q-icon
v-ripple="validate(modelValue - step)" v-ripple="validate(modelValue - step)"
:class="(validate(modelValue - step) ? '' : 'disable')" :class="(validate(modelValue - step) ? '' : 'disable')"
name="la la-minus-circle" :name="minusIcon"
class="button" class="button"
@click="minus" @click="onClick('minus')"
@mousedown.prevent.stop="onMouseDown($event, 'minus')" @mousedown.prevent.stop="onMouseDown($event, 'minus')"
@mouseup.prevent.stop="onMouseUp" @mouseup.prevent.stop="onMouseUp"
@mouseout.prevent.stop="onMouseUp" @mouseout.prevent.stop="onMouseUp"
@@ -27,9 +38,9 @@
<q-icon <q-icon
v-ripple="validate(modelValue + step)" v-ripple="validate(modelValue + step)"
:class="(validate(modelValue + step) ? '' : 'disable')" :class="(validate(modelValue + step) ? '' : 'disable')"
name="la la-plus-circle" :name="plusIcon"
class="button" class="button"
@click="plus" @click="onClick('plus')"
@mousedown.prevent.stop="onMouseDown($event, 'plus')" @mousedown.prevent.stop="onMouseDown($event, 'plus')"
@mouseup.prevent.stop="onMouseUp" @mouseup.prevent.stop="onMouseUp"
@mouseout.prevent.stop="onMouseUp" @mouseout.prevent.stop="onMouseUp"
@@ -37,6 +48,16 @@
@touchend.stop="onTouchEnd" @touchend.stop="onTouchEnd"
@touchcancel.prevent.stop="onTouchEnd" @touchcancel.prevent.stop="onTouchEnd"
/> />
<q-icon
v-show="mmButtons"
v-ripple="modelValue != max"
style="font-size: 100%"
:class="(modelValue != max ? '' : 'disable')"
name="la la-angle-double-right"
class="button"
@click="toMax"
/>
</template> </template>
</q-input> </q-input>
</template> </template>
@@ -49,17 +70,18 @@ import * as utils from '../../share/utils';
const componentOptions = { const componentOptions = {
watch: { watch: {
filteredValue: function(newValue) { filteredValue() {
if (this.validate(newValue)) { this.checkErrorAndEmit(true);
this.error = false;
this.$emit('update:modelValue', this.string2number(newValue));
} else {
this.error = true;
}
}, },
modelValue: function(newValue) { modelValue(newValue) {
this.filteredValue = newValue; this.filteredValue = newValue;
}, },
min() {
this.checkErrorAndEmit();
},
max() {
this.checkErrorAndEmit();
}
} }
}; };
class NumInput { class NumInput {
@@ -70,7 +92,11 @@ class NumInput {
max: { type: Number, default: Number.MAX_VALUE }, max: { type: Number, default: Number.MAX_VALUE },
step: { type: Number, default: 1 }, step: { type: Number, default: 1 },
digits: { type: Number, default: 0 }, digits: { type: Number, default: 0 },
disable: Boolean disable: Boolean,
minusIcon: {type: String, default: 'la la-minus-circle'},
plusIcon: {type: String, default: 'la la-plus-circle'},
mmButtons: Boolean,
mask: String,
}; };
filteredValue = 0; filteredValue = 0;
@@ -95,6 +121,16 @@ class NumInput {
return true; return true;
} }
checkErrorAndEmit(emit = false) {
if (this.validate(this.filteredValue)) {
this.error = false;
if (emit)
this.$emit('update:modelValue', this.string2number(this.filteredValue));
} else {
this.error = true;
}
}
plus() { plus() {
const newValue = this.modelValue + this.step; const newValue = this.modelValue + this.step;
if (this.validate(newValue)) if (this.validate(newValue))
@@ -107,23 +143,42 @@ class NumInput {
this.filteredValue = newValue; this.filteredValue = newValue;
} }
onClick(way) {
if (this.clickRepeat)
return;
if (way == 'plus') {
this.plus();
} else {
this.minus();
}
}
onMouseDown(event, way) { onMouseDown(event, way) {
this.startClickRepeat = true; this.startClickRepeat = true;
this.clickRepeat = false; this.clickRepeat = false;
if (event.button == 0) { if (event.button == 0) {
(async() => { (async() => {
await utils.sleep(300); if (this.inRepeatFunc)
if (this.startClickRepeat) { return;
this.clickRepeat = true;
while (this.clickRepeat) { this.inRepeatFunc = true;
if (way == 'plus') { try {
this.plus(); await utils.sleep(300);
} else { if (this.startClickRepeat) {
this.minus(); this.clickRepeat = true;
while (this.clickRepeat) {
if (way == 'plus') {
this.plus();
} else {
this.minus();
}
await utils.sleep(100);
} }
await utils.sleep(50);
} }
} finally {
this.inRepeatFunc = false;
} }
})(); })();
} }
@@ -133,7 +188,12 @@ class NumInput {
if (this.inTouch) if (this.inTouch)
return; return;
this.startClickRepeat = false; this.startClickRepeat = false;
this.clickRepeat = false; if (this.clickRepeat) {
(async() => {
await utils.sleep(50);
this.clickRepeat = false;
})();
}
} }
onTouchStart(event, way) { onTouchStart(event, way) {
@@ -151,6 +211,14 @@ class NumInput {
this.inTouch = false; this.inTouch = false;
this.onMouseUp(); this.onMouseUp();
} }
toMin() {
this.filteredValue = this.min;
}
toMax() {
this.filteredValue = this.max;
}
} }
export default vueComponent(NumInput); export default vueComponent(NumInput);
@@ -165,24 +233,19 @@ export default vueComponent(NumInput);
.button { .button {
font-size: 130%; font-size: 130%;
border-radius: 20px; border-radius: 15px;
color: #bbb; width: 30px;
height: 30px;
color: var(--text-ubtn-color);
cursor: pointer; cursor: pointer;
} }
.button:hover { .button:hover {
color: #616161; filter: invert(100%);
background-color: #efebe9;
}
.error {
background-color: #ffabab;
border-radius: 3px;
} }
.disable, .disable:hover { .disable, .disable:hover {
cursor: not-allowed; cursor: not-allowed;
color: #bbb; filter: invert(0%);
background-color: white;
} }
</style> </style>

View File

@@ -3,7 +3,7 @@
<slot></slot> <slot></slot>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'alert'" class="bg-white no-wrap"> <div v-show="type == 'alert'" class="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">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
@@ -28,7 +28,7 @@
</div> </div>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'confirm'" class="bg-white no-wrap"> <div v-show="type == 'confirm'" class="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">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
@@ -56,7 +56,7 @@
</div> </div>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'askYesNo'" class="bg-white no-wrap"> <div v-show="type == 'askYesNo'" class="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">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
@@ -84,7 +84,7 @@
</div> </div>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'prompt'" class="bg-white no-wrap"> <div v-show="type == 'prompt'" class="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">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
@@ -116,7 +116,7 @@
</div> </div>
<!---------------------------------------------------> <!--------------------------------------------------->
<div v-show="type == 'hotKey'" class="bg-white no-wrap"> <div v-show="type == 'hotKey'" class="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">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon> <q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>

View File

@@ -10,7 +10,9 @@
@touchend.stop="onTouchEnd" @touchend.stop="onTouchEnd"
@touchmove.stop="onTouchMove" @touchmove.stop="onTouchMove"
> >
<span class="header-text col"><slot name="header"></slot></span> <div class="header-text col" style="width: 0">
<slot name="header"></slot>
</div>
<slot name="buttons"></slot> <slot name="buttons"></slot>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span> <span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
</div> </div>
@@ -146,14 +148,14 @@ export default vueComponent(Window);
.window { .window {
margin: 10px; margin: 10px;
background-color: #ffffff; background-color: var(--bg-app-color);
border: 3px double black; border: 3px double var(--text-app-color);
border-radius: 4px; border-radius: 4px;
box-shadow: 3px 3px 5px black; box-shadow: 3px 3px 5px black;
} }
.header { .header {
background: linear-gradient(to bottom right, #007000, #59B04F); background: linear-gradient(to bottom right, var(--bg-header-color1), var(--bg-header-color2));
align-items: center; align-items: center;
height: 30px; height: 30px;
} }

View File

@@ -17,7 +17,7 @@ export default function(componentClass) {
} }
} }
} else if (prop === '_props') { } else if (prop === '_props') {
comp['props'] = obj[prop]; comp.props = obj[prop];
} }
} else {//usual prop } else {//usual prop
data[prop] = obj[prop]; data[prop] = obj[prop];
@@ -26,23 +26,32 @@ export default function(componentClass) {
comp.data = () => _.cloneDeep(data); comp.data = () => _.cloneDeep(data);
//methods //methods
const classProto = Object.getPrototypeOf(obj);
const classMethods = Object.getOwnPropertyNames(classProto);
const methods = {}; const methods = {};
const computed = {}; const computed = {};
for (const method of classMethods) {
const desc = Object.getOwnPropertyDescriptor(classProto, method); let classProto = Object.getPrototypeOf(obj);
if (desc.get) {//has getter, computed while (classProto) {
computed[method] = {get: desc.get}; const classMethods = Object.getOwnPropertyNames(classProto);
if (desc.set) for (const method of classMethods) {
computed[method].set = desc.set; const desc = Object.getOwnPropertyDescriptor(classProto, method);
} else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks if (desc.get) {//has getter, computed
'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks if (!computed[method]) {
'setup'].includes(method) ) { computed[method] = {get: desc.get};
comp[method] = obj[method]; if (desc.set)
} else if (method !== 'constructor') {//usual computed[method].set = desc.set;
methods[method] = obj[method]; }
} else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',
'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',
'setup'].includes(method) ) {//life cycle hooks
if (!comp[method])
comp[method] = obj[method];
} else if (method !== 'constructor') {//usual
if (!methods[method])
methods[method] = obj[method];
}
} }
classProto = Object.getPrototypeOf(classProto);
} }
comp.methods = methods; comp.methods = methods;
comp.computed = computed; comp.computed = computed;

View File

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

View File

@@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import dayjs from 'dayjs';
import baseX from 'base-x'; import baseX from 'base-x';
import PAKO from 'pako'; import PAKO from 'pako';
import {Buffer} from 'safe-buffer'; import {Buffer} from 'safe-buffer';
@@ -35,24 +36,6 @@ export function randomHexString(len) {
return Buffer.from(randomArray(len)).toString('hex'); return Buffer.from(randomArray(len)).toString('hex');
} }
export function formatDate(d, format) {
if (!format)
format = 'normal';
switch (format) {
case 'normal':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` +
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
case 'coDate':
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
case 'coMonth':
return `${(d.getMonth() + 1).toString().padStart(2, '0')}`;
case 'noDate':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
}
}
export function fallbackCopyTextToClipboard(text) { export function fallbackCopyTextToClipboard(text) {
let textArea = document.createElement('textarea'); let textArea = document.createElement('textarea');
textArea.value = text; textArea.value = text;
@@ -416,3 +399,7 @@ export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
export function makeDonation() { export function makeDonation() {
window.open('https://donatty.com/liberama', '_blank'); window.open('https://donatty.com/liberama', '_blank');
} }
export function dateFormat(date, format = 'DD.MM.YYYY') {
return dayjs(date).format(format);
}

View File

@@ -3,7 +3,6 @@ import { createStore } from 'vuex';
import VuexPersistence from 'vuex-persist'; import VuexPersistence from 'vuex-persist';
import root from './root.js'; import root from './root.js';
import uistate from './modules/uistate';
import config from './modules/config'; import config from './modules/config';
import reader from './modules/reader'; import reader from './modules/reader';
@@ -13,7 +12,6 @@ const vuexLocal = new VuexPersistence();
export default createStore(Object.assign({}, root, { export default createStore(Object.assign({}, root, {
modules: { modules: {
uistate,
config, config,
reader, reader,
}, },

View File

@@ -1,4 +1,3 @@
import miscApi from '../../api/misc';
// initial state // initial state
const state = { const state = {
name: null, name: null,

View File

@@ -1,6 +1,11 @@
import _ from 'lodash';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json'; import googleFonts from './fonts/fonts.json';
const minuteMs = 60*1000;//количество ms в минуте
const hourMs = 60*minuteMs;//количество ms в часе
const dayMs = 24*hourMs;//количество ms в сутках
const readerActions = { const readerActions = {
'loader': 'На страницу загрузки', 'loader': 'На страницу загрузки',
'loadFile': 'Загрузить файл с диска', 'loadFile': 'Загрузить файл с диска',
@@ -17,6 +22,7 @@ const readerActions = {
'copyText': 'Скопировать текст со страницы', 'copyText': 'Скопировать текст со страницы',
'convOptions': 'Настроить конвертирование', 'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу', 'refresh': 'Принудительно обновить книгу',
'nightMode': 'Ночной режим',
'clickControl': 'Управление кликом', 'clickControl': 'Управление кликом',
'offlineMode': 'Автономный режим (без интернета)', 'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки', 'contents': 'Оглавление/закладки',
@@ -44,17 +50,18 @@ const toolButtons = [
{name: 'undoAction', show: true}, {name: 'undoAction', show: true},
{name: 'redoAction', show: true}, {name: 'redoAction', show: true},
{name: 'fullScreen', show: true}, {name: 'fullScreen', show: true},
{name: 'scrolling', show: false}, {name: 'scrolling', show: true},
{name: 'setPosition', show: true}, {name: 'setPosition', show: true},
{name: 'search', show: true}, {name: 'search', show: true},
{name: 'copyText', show: false}, {name: 'copyText', show: true},
{name: 'convOptions', show: true}, {name: 'convOptions', show: true},
{name: 'refresh', show: true}, {name: 'refresh', show: true},
{name: 'contents', show: true}, {name: 'contents', show: true},
{name: 'libs', show: true}, {name: 'libs', show: true},
{name: 'recentBooks', show: true}, {name: 'recentBooks', show: true},
{name: 'clickControl', show: false}, {name: 'nightMode', show: true},
{name: 'offlineMode', show: false}, {name: 'clickControl', show: true},
{name: 'offlineMode', show: true},
]; ];
//readerActions[name] //readerActions[name]
@@ -76,6 +83,7 @@ const hotKeys = [
{name: 'contents', codes: ['C']}, {name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']}, {name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']}, {name: 'recentBooks', codes: ['X']},
{name: 'nightMode', codes: ['Equal']},
{name: 'clickControl', codes: ['Ctrl+B']}, {name: 'clickControl', codes: ['Ctrl+B']},
{name: 'offlineMode', codes: ['O']}, {name: 'offlineMode', codes: ['O']},
@@ -153,6 +161,10 @@ const settingDefaults = {
statusBarColorAlpha: 0.4, statusBarColorAlpha: 0.4,
statusBarClickOpen: true, statusBarClickOpen: true,
nightMode: false, //ночной режим
dayColorSets: {},
nightColorSets: {},
scrollingDelay: 3000,// замедление, ms scrollingDelay: 3000,// замедление, ms
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
@@ -160,7 +172,6 @@ const settingDefaults = {
pageChangeAnimationSpeed: 80, //0-100% pageChangeAnimationSpeed: 80, //0-100%
allowUrlParamBookPos: false, allowUrlParamBookPos: false,
lazyParseEnabled: false,
copyFullText: false, copyFullText: false,
showClickMapPage: true, showClickMapPage: true,
clickControl: true, clickControl: true,
@@ -186,6 +197,7 @@ const settingDefaults = {
fontShifts: {}, fontShifts: {},
showToolButton: {}, showToolButton: {},
toolBarHideOnScroll: false, toolBarHideOnScroll: false,
toolBarMultiLine: true,
userHotKeys: {}, userHotKeys: {},
userWallpapers: [], userWallpapers: [],
@@ -198,10 +210,6 @@ const settingDefaults = {
bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
//для SettingsPage
needUpdateSettingsView: 0,
}; };
for (const font of fonts) for (const font of fonts)
@@ -217,6 +225,8 @@ const diffExclude = [];
for (const hotKey of hotKeys) for (const hotKey of hotKeys)
diffExclude.push(`userHotKeys/${hotKey.name}`); diffExclude.push(`userHotKeys/${hotKey.name}`);
diffExclude.push('userWallpapers'); diffExclude.push('userWallpapers');
diffExclude.push('dayColorSets');
diffExclude.push('nightColorSets');
function addDefaultsToSettings(settings) { function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude}); const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
@@ -227,30 +237,79 @@ function addDefaultsToSettings(settings) {
return false; return false;
} }
const libsDefaults = { const colorSetsList = [
startLink: 'http://flibusta.is', 'textColor',
comment: 'Флибуста | Книжное братство', 'backgroundColor',
closeAfterSubmit: false, 'wallpaper',
openInFrameOnEnter: false, 'statusBarColorAsText',
openInFrameOnAdd: false, 'statusBarColor',
groups: [ 'statusBarColorAlpha',
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [ 'dualDivColorAsText',
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'}, 'dualDivColor',
]}, 'dualDivColorAlpha',
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [ ];
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]}, function saveColorSets(nightMode, settings) {
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [ const target = (nightMode ? settings.nightColorSets : settings.dayColorSets);
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'}, for (const prop of colorSetsList) {
]}, target[prop] = settings[prop];
{r: 'http://lib.ru', s: 'http://lib.ru', list: [ }
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'}, }
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [ function restoreColorSets(nightMode, settings) {
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'}, const source = (nightMode ? settings.nightColorSets : settings.dayColorSets);
]}, for (const prop of colorSetsList) {
] if (utils.hasProp(source, prop))
}; settings[prop] = source[prop];
}
}
function getLibsDefaults(mode = 'reader') {
const result = {
startLink: '',
comment: '',
closeAfterSubmit: false,
openInFrameOnEnter: false,
openInFrameOnAdd: false,
helpShowed: false,
mode,
groups: [
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
]},
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},
],
};
if (mode === 'liberama') {
result.groups.unshift(
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]}
);
result.groups.unshift(
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]}
);
} else if (mode === 'omnireader') {
result.groups.unshift(
{r: 'https://lib.omnireader.ru', s: 'https://lib.omnireader.ru', list: [
{l: 'https://lib.omnireader.ru', c: 'Общественное достояние'},
]}
);
}
result.startLink = result.groups[0].r;
result.comment = result.groups[0].c;
return result;
}
// initial state // initial state
const state = { const state = {
@@ -262,11 +321,11 @@ const state = {
profilesRev: 0, profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '', whatsNewContentHash: '',
donationRemindDate: '', donationNextPopup: Date.now() + dayMs*30,
currentProfile: '', currentProfile: '',
settings: Object.assign({}, settingDefaults), settings: _.cloneDeep(settingDefaults),
settingsRev: {}, settingsRev: {},
libs: Object.assign({}, libsDefaults), libs: {},
libsRev: 0, libsRev: 0,
}; };
@@ -302,20 +361,38 @@ const mutations = {
setWhatsNewContentHash(state, value) { setWhatsNewContentHash(state, value) {
state.whatsNewContentHash = value; state.whatsNewContentHash = value;
}, },
setDonationRemindDate(state, value) { setDonationNextPopup(state, value) {
state.donationRemindDate = value; state.donationNextPopup = value;
}, },
setCurrentProfile(state, value) { setCurrentProfile(state, value) {
state.currentProfile = value; state.currentProfile = value;
}, },
setSettings(state, value) { setSettings(state, value) {
const newSettings = Object.assign({}, state.settings, value); let newSettings = Object.assign({}, state.settings, value);
//при смене профиля подгружаются старые настройки, могут отсутствовать атрибуты
//поэтому:
const added = addDefaultsToSettings(newSettings); const added = addDefaultsToSettings(newSettings);
if (added) { if (added)
state.settings = added; newSettings = added;
} else {
state.settings = newSettings; state.settings = newSettings;
},
nightModeToggle(state) {
//переключение режима день-ночь
const newSettings = Object.assign({}, state.settings);
saveColorSets(newSettings.nightMode, newSettings);
newSettings.nightMode = !newSettings.nightMode;
if (newSettings.nightMode && !utils.hasProp(newSettings.nightColorSets, 'textColor')) {
// Ночной режим активирован впервые. Цвета заданы по умолчанию.
newSettings.nightColorSets = {textColor: '#778a9e', backgroundColor: '#363131'};
} }
restoreColorSets(newSettings.nightMode, newSettings);
state.settings = newSettings;
}, },
setSettingsRev(state, value) { setSettingsRev(state, value) {
state.settingsRev = Object.assign({}, state.settingsRev, value); state.settingsRev = Object.assign({}, state.settingsRev, value);
@@ -329,6 +406,10 @@ const mutations = {
}; };
export default { export default {
minuteMs,
hourMs,
dayMs,
readerActions, readerActions,
toolButtons, toolButtons,
hotKeys, hotKeys,
@@ -336,7 +417,7 @@ export default {
webFonts, webFonts,
settingDefaults, settingDefaults,
addDefaultsToSettings, addDefaultsToSettings,
libsDefaults, getLibsDefaults,
namespaced: true, namespaced: true,
state, state,

View File

@@ -1,25 +0,0 @@
// initial state
const state = {
asideBarCollapse: false,
};
// getters
const getters = {};
// actions
const actions = {};
// mutations
const mutations = {
setAsideBarCollapse(state, value) {
state.asideBarCollapse = value;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};

View File

@@ -87,18 +87,22 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /tmp {
root /home/beta.liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/beta.liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/beta.liberama/public; root /home/beta.liberama/.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;

View File

@@ -32,18 +32,22 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /tmp {
root /home/beta.liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/beta.liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/beta.liberama/public; root /home/beta.liberama/.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;
@@ -57,3 +61,55 @@ server {
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
server {
listen 80;
server_name b.beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass $liberama;
}
location /ws {
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location /tmp {
root /home/beta.liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/beta.liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / {
root /home/beta.liberama/.liberama/public;
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
}
}
}

View File

@@ -1,6 +1,6 @@
server { server {
listen 80; listen 80;
server_name beta.omnireader.ru; server_name beta.omnireader.ru b.beta.omnireader.ru;
set $liberama http://127.0.0.1:34081; set $liberama http://127.0.0.1:34081;
client_max_body_size 50m; client_max_body_size 50m;
@@ -27,18 +27,22 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /tmp {
root /home/beta.liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/beta.liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/beta.liberama/public; root /home/beta.liberama/.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;

View File

@@ -43,18 +43,22 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /tmp {
root /home/liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/liberama/public; root /home/liberama/.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;
@@ -98,18 +102,22 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /tmp {
root /home/liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/liberama/public; root /home/liberama/.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;

View File

@@ -3,7 +3,7 @@
### git, clone ### git, clone
``` ```
cd ~ cd ~
sudo apt install ssh git sudo apt install ssh git zip
git clone https://github.com/bookpauk/liberama git clone https://github.com/bookpauk/liberama
``` ```
@@ -18,6 +18,7 @@ sudo apt install -y nodejs
``` ```
cd liberama cd liberama
npm i npm i
cd docs/omnireader.ru
``` ```
### create public dir ### create public dir
@@ -30,8 +31,8 @@ sudo chown www-data.www-data /home/liberama
#### download from https://download.calibre-ebook.com/ #### download from https://download.calibre-ebook.com/
``` ```
wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz" wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz"
sudo -u www-data mkdir -p /home/liberama/data/calibre sudo -u www-data mkdir -p /home/liberama/.liberama/calibre
sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/data/calibre sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/.liberama/calibre
``` ```
### external converters ### external converters
@@ -44,7 +45,7 @@ sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graph
Сначала настроим для HTTP: Сначала настроим для HTTP:
``` ```
sudo apt install nginx sudo apt install nginx
sudo cp docs/omnireader.ru/omnireader_http /etc/nginx/sites-available/omnireader sudo cp ./omnireader_http /etc/nginx/sites-available/omnireader
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
sudo rm /etc/nginx/sites-enabled/default sudo rm /etc/nginx/sites-enabled/default
sudo service nginx reload sudo service nginx reload
@@ -55,7 +56,7 @@ sudo chown -R www-data.www-data /var/www
#### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20 #### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20
После установки сертификата, можно использовать конфиг для nginx c ssl: После установки сертификата, можно использовать конфиг для nginx c ssl:
``` ```
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader sudo cp ./omnireader /etc/nginx/sites-available/omnireader
sudo service nginx reload sudo service nginx reload
``` ```
@@ -68,7 +69,7 @@ sudo service php7.4-fpm restart
sudo mkdir /home/oldreader sudo mkdir /home/oldreader
sudo chown www-data.www-data /home/oldreader sudo chown www-data.www-data /home/oldreader
sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader sudo -u www-data cp -r ./old/* /home/oldreader
``` ```
## Запуск по крону ## Запуск по крону
@@ -78,7 +79,6 @@ sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
## Деплой и запуск ## Деплой и запуск
``` ```
cd docs/omnireader.ru
./stop_server.sh ./stop_server.sh
./deploy.sh ./deploy.sh
./start_server.sh ./start_server.sh

View File

@@ -32,18 +32,73 @@ server {
proxy_read_timeout 600s; proxy_read_timeout 600s;
} }
location /tmp {
root /home/liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/liberama/public; root /home/liberama/.liberama/public;
location /tmp { location ~* \.(?:manifest|appcache|html)$ {
types { } default_type "application/xml; charset=utf-8"; expires -1;
add_header Content-Encoding gzip;
try_files $uri @liberama;
} }
}
}
location /upload { server {
try_files $uri @liberama; listen 80;
} server_name b.omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 50m;
proxy_read_timeout 1h;
gzip on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass $liberama;
}
location /ws {
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /tmp {
root /home/liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / {
root /home/liberama/.liberama/public;
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;

View File

@@ -1,6 +1,6 @@
server { server {
listen 80; listen 80;
server_name omnireader.ru; server_name omnireader.ru b.omnireader.ru;
set $liberama http://127.0.0.1:44081; set $liberama http://127.0.0.1:44081;
client_max_body_size 50m; client_max_body_size 50m;
@@ -26,18 +26,22 @@ server {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
location /tmp {
root /home/liberama/.liberama/public-files;
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
root /home/liberama/.liberama/public-files;
try_files $uri @liberama;
}
location / { location / {
root /home/liberama/public; root /home/liberama/.liberama/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
try_files $uri @liberama;
}
location /upload {
try_files $uri @liberama;
}
location ~* \.(?:manifest|appcache|html)$ { location ~* \.(?:manifest|appcache|html)$ {
expires -1; expires -1;

10643
package-lock.json generated

File diff suppressed because it is too large Load Diff

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