Compare commits

...

290 Commits

Author SHA1 Message Date
Book Pauk
cafdb5b04b Merge branch 'release/1.2.0' 2024-03-25 12:54:05 +07:00
Book Pauk
697774978e Добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47) 2024-03-25 12:52:46 +07:00
Book Pauk
8c2c2fe2fc 1.2.0 2023-12-07 16:31:13 +07:00
Book Pauk
e3770463a1 В списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий 2023-12-07 16:26:30 +07:00
Book Pauk
d3ad23e9e4 Актуализация пакетов 2023-12-07 15:01:26 +07:00
Book Pauk
79d1e0b30d Merge tag '1.1.3' into develop
1.1.3
2023-02-06 19:48:02 +07:00
Book Pauk
1370bae4d6 Merge branch 'release/1.1.3' 2023-02-06 19:47:56 +07:00
Book Pauk
01fbdf38fa Версия 1.1.3 2023-02-06 19:47:28 +07:00
Book Pauk
be6b07a0cf Исправление бага при обнулении libs 2023-02-06 19:45:13 +07:00
Book Pauk
1b057029c8 Улучшено хранение ключа доступа 2023-02-05 16:04:52 +07:00
Book Pauk
b6b567f20b Улучшение парсинга невалидных fb2 2023-02-03 17:30:22 +07:00
Book Pauk
c4c109fe0e Мелкий рефакторинг 2023-02-03 16:28:24 +07:00
Book Pauk
4c8c921b03 Улучшения механизма запуска периодических задач 2023-02-03 16:23:13 +07:00
Book Pauk
69a2e5cda3 Merge tag '1.1.2-1' into develop
1.1.2-1
2023-01-25 17:06:39 +07:00
Book Pauk
c2adf8d5b8 Merge branch 'release/1.1.2-1' 2023-01-25 17:06:35 +07:00
Book Pauk
5c8d257923 Добавлены отладочные сообщения в журнал 2023-01-25 17:05:53 +07:00
Book Pauk
55dae33e60 "jembadb": "^5.1.7" 2023-01-25 15:46:09 +07:00
Book Pauk
57d8e9061f Merge tag '1.1.2' into develop
1.1.2
2023-01-22 20:56:12 +07:00
Book Pauk
4642679842 Merge branch 'release/1.1.2' 2023-01-22 20:56:08 +07:00
Book Pauk
ba18743fab Версия 1.1.2 2023-01-22 20:55:48 +07:00
Book Pauk
e739356733 Исправление бага - не открывалась ссылка по нажатию кнопки "Открыть" 2023-01-22 20:50:06 +07:00
Book Pauk
cae4aed8d2 Merge tag '1.1.1-1' into develop
1.1.1-1
2023-01-11 21:32:22 +07:00
Book Pauk
6c6a08d8e0 Merge branch 'release/1.1.1-1' 2023-01-11 21:32:13 +07:00
Book Pauk
deafbae945 Версия 1.1.1 2023-01-11 21:31:40 +07:00
Book Pauk
0b23c609f1 Merge tag '1.1.1' into develop
1.1.1
2023-01-11 21:30:53 +07:00
Book Pauk
0359061321 Merge branch 'release/1.1.1' 2023-01-11 21:30:45 +07:00
Book Pauk
bc7a5f6be4 Merge tag '1.1.0-1' into develop
1.1.0-1
2023-01-11 21:30:36 +07:00
Book Pauk
be36f8f6e8 Merge branch 'release/1.1.0-1' 2023-01-11 21:30:30 +07:00
Book Pauk
3b8d084c76 Доработки ночного режима 2023-01-11 21:29:35 +07:00
Book Pauk
ce1cdca6a0 Merge tag '1.1.0' into develop
1.1.0
2023-01-11 21:06:40 +07:00
Book Pauk
2f380dce1b Merge branch 'release/1.1.0' 2023-01-11 21:06:36 +07:00
Book Pauk
63b7bb24cf Версия 1.1.0 2023-01-11 21:06:08 +07:00
Book Pauk
2401ef8d16 Работа над ночным режимом 2023-01-11 20:53:19 +07:00
Book Pauk
62df3c0197 Работа над ночным режимом 2023-01-11 20:16:25 +07:00
Book Pauk
ba2dbca226 Работа над ночным режимом 2023-01-11 20:01:29 +07:00
Book Pauk
810b131b92 Работа над ночным режимом 2023-01-11 19:24:47 +07:00
Book Pauk
1d5bcde293 Работа над ночным режимом 2023-01-11 19:05:08 +07:00
Book Pauk
2fcf584e40 Вырезал ненужный код 2023-01-11 18:48:24 +07:00
Book Pauk
ecc6791892 Работа над ночным режимом 2023-01-11 18:44:54 +07:00
Book Pauk
8bf19c1e69 К предыдущему 2023-01-11 18:31:35 +07:00
Book Pauk
273ab4ae60 Работа над ночным режимом 2023-01-11 18:26:52 +07:00
Book Pauk
ec8fedc73d Работа над ночным режимом 2023-01-11 17:42:46 +07:00
Book Pauk
e6b1d4b032 Работа над ночным режимом 2023-01-11 14:46:27 +07:00
Book Pauk
a89572f85f Работа над ночным режимом 2023-01-11 14:33:16 +07:00
Book Pauk
bf4f5bc88b Работа над ночным режимом 2023-01-11 14:07:08 +07:00
Book Pauk
f4ce1f337e Работа над ночным режимом 2023-01-11 13:22:43 +07:00
Book Pauk
5e8afa15b2 Работа над ночным режимом 2023-01-11 12:57:40 +07:00
Book Pauk
7b1d0bb778 Работа над ночным режимом 2023-01-10 21:06:54 +07:00
Book Pauk
c0aec66f0f Работа над ночным режимом 2023-01-10 20:56:27 +07:00
Book Pauk
31481453f5 Работа над ночным режимом 2023-01-10 19:53:58 +07:00
Book Pauk
9724ec230c Работа над ночным режимом 2023-01-10 19:35:40 +07:00
Book Pauk
9e4be96522 Работа над ночным режимом 2023-01-08 20:08:03 +07:00
Book Pauk
91097515f2 Вырезал ненужный код 2023-01-08 18:08:17 +07:00
Book Pauk
230c3bb5b2 Начало работы над ночным режимом 2023-01-08 18:05:02 +07:00
Book Pauk
7a71db9de4 Поправил формирование заголовка 2023-01-08 15:22:36 +07:00
Book Pauk
7261afc428 Версия 1.0.1 2023-01-08 15:14:45 +07:00
Book Pauk
ddde7d038b Поправки редиректа при заданном url 2023-01-08 15:06:01 +07:00
Book Pauk
4d3d66fbe2 Merge tag '1.0.0' into develop
1.0.0
2022-12-18 15:17:20 +07:00
Book Pauk
b98a44def2 Merge branch 'release/1.0.0' 2022-12-18 15:17:09 +07:00
Book Pauk
c6e972b165 Поправки багов 2022-12-18 14:53:58 +07:00
Book Pauk
7b7146b502 Поправки дефолтных сетевых библиотек для режима omnireader 2022-12-18 14:35:40 +07:00
Book Pauk
f00700cb41 Поправка конфигов nginx 2022-12-18 13:30:47 +07:00
Book Pauk
c3e099f095 Поправка бага 2022-12-18 13:22:55 +07:00
Book Pauk
6393c24575 Поправка README 2022-12-18 13:21:25 +07:00
Book Pauk
17378f3686 Поправки конфигов nginx 2022-12-18 13:04:45 +07:00
Book Pauk
d7453302f7 versionHistory 2022-12-18 13:04:25 +07:00
Book Pauk
07f5146534 Небольшие улучшения UI 2022-12-17 20:49:39 +07:00
Book Pauk
d04851af72 versionHistory 2022-12-17 20:39:09 +07:00
Book Pauk
6aff0eb4e6 Улучшение формы доната 2022-12-17 20:28:16 +07:00
Book Pauk
2f5409b485 Актуализация пакетов 2022-12-16 21:22:47 +07:00
Book Pauk
3aa7dc32d3 Мелкая поправка 2022-12-16 21:03:22 +07:00
Book Pauk
f5cd6ebdbc Добавлена настройка "Многострочная панель" для размещения кнопок в
несколько рядов на тулбаре
2022-12-16 21:02:07 +07:00
Book Pauk
a7289cda74 Поправки процедуры скроллинга 2022-12-16 20:04:20 +07:00
Book Pauk
ada3a3b4fd Рефакторинг 2022-12-16 19:41:20 +07:00
Book Pauk
a21e216eb9 Поправка бага 2022-12-16 19:34:56 +07:00
Book Pauk
b85fe7f219 Поправки webkit-scrollbar 2022-12-16 19:26:30 +07:00
Book Pauk
4efb3031de Удаление более ненужного кода 2022-12-16 19:07:47 +07:00
Book Pauk
6b66acb2cf Поправил справку 2022-12-16 18:22:48 +07:00
Book Pauk
481e1e840e Добавлен переход в полноэкраннй режим по двойному тапу в середину экрана 2022-12-16 18:14:08 +07:00
Book Pauk
e296b49821 Поправки положения всплывающих сообщений 2022-12-16 15:38:38 +07:00
Book Pauk
254118f845 Мелкий рефакторинг 2022-12-16 14:44:16 +07:00
Book Pauk
88f5a98c55 Решение проблемы откатов страницы чтения при нестабильной связи во время синхронизации 2022-12-15 20:05:03 +07:00
Book Pauk
572a5dd200 Поправка положения кнопок панели 2022-12-15 18:19:04 +07:00
Book Pauk
8dce00db44 Поправки отображения кнопок панели 2022-12-15 18:00:05 +07:00
Book Pauk
0ab73deffd Исправление бага offlineModeActive 2022-12-15 17:53:22 +07:00
Book Pauk
9863dc6dd0 Поправка комментария 2022-12-15 17:52:40 +07:00
Book Pauk
797f93d467 Убрал дебаг 2022-12-15 17:36:24 +07:00
Book Pauk
c602f3d531 Переход на dayjs 2022-12-15 17:18:05 +07:00
Book Pauk
dfd45a58bd "dayjs": "^1.11.7" 2022-12-15 17:17:46 +07:00
Book Pauk
70a832530e versionHistory 2022-12-15 16:48:51 +07:00
Book Pauk
4fc32eafd7 Удалены неиспользуемые компоненты 2022-12-15 16:46:20 +07:00
Book Pauk
6579d34b90 Переименование режима "liberama.top" в "liberama" 2022-12-15 16:32:52 +07:00
Book Pauk
a5bf8f88cd Добавлен раздел "Сетевая библиотека" в режим "omnireader" 2022-12-15 16:31:42 +07:00
Book Pauk
55264314b8 Обработка ошибок 2022-12-14 20:07:45 +07:00
Book Pauk
23a9e9154b Доработки Logger 2022-12-14 20:06:56 +07:00
Book Pauk
0ee373c1f3 Убрал лишний код 2022-12-14 19:22:13 +07:00
Book Pauk
29b40bc91d Начата работа над 1.0.0 2022-12-14 19:07:36 +07:00
Book Pauk
10b7363b06 Поправки docs в соответствии с новыми изменениями 2022-12-14 18:56:48 +07:00
Book Pauk
e37f15975d Поправки README 2022-12-12 19:13:41 +07:00
Book Pauk
ce0f61c543 Поправки README 2022-12-12 19:11:19 +07:00
Book Pauk
ea62abfc9a Добавлен createWebApp 2022-12-12 18:10:18 +07:00
Book Pauk
15a2b6ba7e Обновление node_stream_zip_changed с моими изменениями 2022-12-12 18:06:20 +07:00
Book Pauk
10773526e4 Добавлена сборка релизов, ipfs удален 2022-12-12 16:24:19 +07:00
Book Pauk
facd7f1414 Поправка production-конфига 2022-12-12 16:04:57 +07:00
Book Pauk
29bf80108d Переход на WebSocket, поправки багов 2022-12-12 16:03:41 +07:00
Book Pauk
00bbb56ec6 README.md 2022-12-12 16:03:34 +07:00
Book Pauk
2e057f5c96 Поправки сборки webpack 2022-12-12 15:26:14 +07:00
Book Pauk
936fa6a172 gitignore 2022-12-11 18:34:16 +07:00
Book Pauk
5d5ad40f4e Выделение файлов приложения в рабочую директорию 2022-12-11 18:30:33 +07:00
Book Pauk
55ee303fc5 Убрал sharedDir 2022-12-11 16:32:54 +07:00
Book Pauk
f30f11ce2d Поправки FileDownloader 2022-12-11 15:38:51 +07:00
Book Pauk
f5e57b3319 Консольный лог включен всегда 2022-12-11 14:30:30 +07:00
Book Pauk
d5fe4f8eb4 Улучшение AsyncExit 2022-12-11 14:29:30 +07:00
Book Pauk
4f4f226d8c Поддержка наследования классов 2022-12-11 14:22:39 +07:00
Book Pauk
5b7712c274 Актуализация пакетов 2022-12-11 14:12:42 +07:00
Book Pauk
8da71a98da Небольшие поправки 2022-11-01 00:54:07 +07:00
Book Pauk
f9fc59718a Изменения для встраивания inpx-web в сетевую библиотеку 2022-10-14 20:12:36 +07:00
Book Pauk
9bc4c3201c Добавлен location.reload для фрейма 2022-10-14 17:20:48 +07:00
Book Pauk
eb4ea0cc9c Удалил лишние файлы 2022-10-14 15:49:13 +07:00
Book Pauk
4b2e63bb5b Улучшение NumInput 2022-10-13 21:19:18 +07:00
Book Pauk
817f018d4d Убрал лишнее 2022-10-13 21:13:37 +07:00
Book Pauk
9160b4ef90 Глобальный рефакторинг SettingsPage (закончено) 2022-10-13 21:12:56 +07:00
Book Pauk
e8d1817566 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-13 20:29:19 +07:00
Book Pauk
419b203fcf Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 21:45:40 +07:00
Book Pauk
528b32ccf7 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 20:24:08 +07:00
Book Pauk
bc0c9932c8 Поправлен баг 2022-10-12 19:21:48 +07:00
Book Pauk
5827d7a246 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 18:02:32 +07:00
Book Pauk
5dd08c43a6 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:51:48 +07:00
Book Pauk
13c5fc244a Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:37:58 +07:00
Book Pauk
b8b52fe662 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:13:34 +07:00
Book Pauk
f4c0a48868 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 17:13:21 +07:00
Book Pauk
78b98e77c6 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 16:57:30 +07:00
Book Pauk
8cbaf60755 К предыдущему 2022-10-12 16:30:27 +07:00
Book Pauk
62ac60887e Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 16:25:58 +07:00
Book Pauk
fe6243e889 Глобальный рефакторинг SettingsPage (в процессе) 2022-10-12 15:58:17 +07:00
Book Pauk
8abd8ecaab Глобальный рефакторинг SettingsPage (начало), избавление от includer 2022-10-12 15:18:23 +07:00
Book Pauk
c860422a5a Merge tag '0.12.2-3' into develop
0.12.2-3
2022-10-05 17:59:15 +07:00
Book Pauk
083151460a Merge branch 'release/0.12.2-3' 2022-10-05 17:59:10 +07:00
Book Pauk
c8f97ef386 Решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом 2022-10-05 17:58:39 +07:00
Book Pauk
c9a22a5eaf Merge tag '0.12.2-2' into develop
0.12.2-2
2022-10-05 15:16:29 +07:00
Book Pauk
f926732070 Merge branch 'release/0.12.2-2' 2022-10-05 15:16:24 +07:00
Book Pauk
3fbe6e9d9b Улучшение обработки ошибок 2022-10-05 15:15:26 +07:00
Book Pauk
225230381f Добавлена чистка output перед сборкой 2022-10-01 13:39:02 +07:00
Book Pauk
b58d3a1b8b Поправки параметров CopyWebpackPlugin 2022-09-20 20:21:41 +07:00
Book Pauk
ffedce4351 Поправки обработки ошибок сервера 2022-09-12 15:23:22 +07:00
Book Pauk
a4fdb67913 Merge tag '0.12.2-1' into develop
0.12.2-1
2022-09-04 21:44:06 +07:00
Book Pauk
6ba46421b9 Merge branch 'release/0.12.2-1' 2022-09-04 21:43:54 +07:00
Book Pauk
d201961046 Поправка положения notify-сообщений 2022-09-04 21:42:50 +07:00
Book Pauk
614a7f9da7 Merge tag '0.12.2' into develop
0.12.2
2022-09-04 21:22:39 +07:00
Book Pauk
113ab3e596 Merge branch 'release/0.12.2' 2022-09-04 21:22:28 +07:00
Book Pauk
c95870bfe5 Добавлено сохранение во vuex настройки offlineModeActive 2022-09-04 21:20:21 +07:00
Book Pauk
e69e9335f9 Исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц 2022-09-04 21:19:30 +07:00
Book Pauk
fd21cd77dd Node 16 2022-09-01 21:13:31 +07:00
Book Pauk
d1880acaf9 Merge tag '0.12.1' into develop
0.12.1
2022-09-01 21:10:57 +07:00
Book Pauk
428b507257 Merge branch 'release/0.12.1' 2022-09-01 21:10:52 +07:00
Book Pauk
043dab0731 Версия 0.12.1 2022-09-01 21:08:56 +07:00
Book Pauk
a7b4d9c0d8 Добавлена форма доната 2022-09-01 21:05:22 +07:00
Book Pauk
6f9c95e351 Переход на node 16, актуализация пакетов 2022-09-01 15:36:28 +07:00
Book Pauk
7a53063ea8 Исправление багов 2022-09-01 15:31:16 +07:00
Book Pauk
ec4d5cac4f Поправлен баг 2022-08-16 23:40:40 +07:00
Book Pauk
f8557cba88 Исправление багов 2022-08-05 02:25:45 +07:00
Book Pauk
5dead039f5 Дебаг 2022-08-05 01:09:47 +07:00
Book Pauk
ea38392df4 Дебаг 2022-08-05 00:57:18 +07:00
Book Pauk
0cc9d90a94 Поправлен мелкий баг 2022-08-05 00:31:56 +07:00
Book Pauk
8c7b86c458 Поправлен баг 2022-08-05 00:16:54 +07:00
Book Pauk
0e29546fc5 Добавлены таймауты 2022-08-04 23:53:46 +07:00
Book Pauk
c9fa90d07c Поправлен donate-адрес 2022-08-04 15:08:43 +07:00
Book Pauk
7d8e0525b1 Активировал DonateHelpPage 2022-08-04 15:03:48 +07:00
Book Pauk
ddf69876a6 Добавлено сообщение при изменении чекбокса проверки обновления 2022-08-04 13:23:32 +07:00
Book Pauk
1d78e75e38 Merge tag '0.12.0-2' into develop
0.12.0-2
2022-08-03 15:58:49 +07:00
Book Pauk
7ed58fe3c6 Merge branch 'release/0.12.0-2' 2022-08-03 15:58:42 +07:00
Book Pauk
058c79570b Поправки багов 2022-08-03 15:52:48 +07:00
Book Pauk
ec8fbcdf38 Исправление багов 2022-08-03 15:34:24 +07:00
Book Pauk
76673295bf Добавлена автоотмена проверки обновлений книг по истечении заданного количества дней 2022-08-03 14:57:01 +07:00
Book Pauk
084401b9c3 Мелкие поправки 2022-08-03 14:53:58 +07:00
Book Pauk
49038b10f7 Улучшение обработки ошибок 2022-07-29 17:45:33 +07:00
Book Pauk
45ea26810a Улучшение fillCheckQueue 2022-07-28 20:22:38 +07:00
Book Pauk
18c8b2d803 Мелкие поправки 2022-07-28 18:50:56 +07:00
Book Pauk
f4a7482b3b Улучшение парсинга head-запроса 2022-07-28 18:38:49 +07:00
Book Pauk
32dff128f4 Улучшение парсинга head-запроса 2022-07-28 18:04:47 +07:00
Book Pauk
a00b2d6574 Исправлен баг 2022-07-27 23:29:52 +07:00
Book Pauk
10c6e7d522 Merge tag '0.12.0-1' into develop
0.12.0-1
2022-07-27 21:33:56 +07:00
Book Pauk
df6a256d51 Merge branch 'release/0.12.0-1' 2022-07-27 21:33:49 +07:00
Book Pauk
fbdb74ee68 Поправка текста 2022-07-27 21:33:22 +07:00
Book Pauk
9ad7250da0 Merge tag '0.12.0' into develop
0.12.0
2022-07-27 21:10:04 +07:00
Book Pauk
8c86984ea1 Merge branch 'release/0.12.0' 2022-07-27 21:09:59 +07:00
Book Pauk
834b3f6210 Версия 0.12.0 2022-07-27 21:09:42 +07:00
Book Pauk
105b8d5042 Мелкие поправки 2022-07-27 21:02:26 +07:00
Book Pauk
7ca8fd9ca1 Доработки отправки bookUrls 2022-07-27 20:50:39 +07:00
Book Pauk
0067c2800a Дебаг 2022-07-27 20:37:56 +07:00
Book Pauk
688c8796f4 Поправлен баг 2022-07-27 19:00:25 +07:00
Book Pauk
56af65742b Улучшение настроек для BookUpdateChecker 2022-07-27 18:49:51 +07:00
Book Pauk
629ad26d40 Доработки BookUpdateChecker 2022-07-27 17:55:29 +07:00
Book Pauk
4b0e499c10 Работа над BookUpdateChecker 2022-07-27 17:28:02 +07:00
Book Pauk
4697b46cba Работа над BookUpdateChecker 2022-07-27 16:50:24 +07:00
Book Pauk
7f17e7daed Работа над BookUpdateChecker 2022-07-27 15:40:46 +07:00
Book Pauk
a1fcb7597b Работа над BookUpdateChecker 2022-07-27 14:08:59 +07:00
Book Pauk
35e46d0685 Работа над BookUpdateChecker 2022-07-27 12:44:10 +07:00
Book Pauk
e2c0f3658b Улучшения ServerStorage 2022-07-27 11:42:39 +07:00
Book Pauk
a3541ec16a Работа над BookUpdateChecker 2022-07-26 20:37:49 +07:00
Book Pauk
08d0d3e7f3 Работа над BookUpdateChecker 2022-07-26 20:12:44 +07:00
Book Pauk
2c47b2bee3 Работа над BookUpdateChecker 2022-07-26 18:43:42 +07:00
Book Pauk
e6008b5ec4 Работа над BookUpdateChecker 2022-07-26 17:30:34 +07:00
Book Pauk
e214ddf8d5 Работа над BookUpdateChecker 2022-07-26 00:41:07 +07:00
Book Pauk
52927c6188 Работа над BookUpdateChecker 2022-07-26 00:11:15 +07:00
Book Pauk
92ca9dd983 Работа над BookUpdateChecker 2022-07-25 23:27:38 +07:00
Book Pauk
ed8be34c12 Работа над BookUpdateChecker 2022-07-25 17:52:57 +07:00
Book Pauk
93bddfd05e Переход на vuex-persist вместо vuex-persistedstate 2022-07-25 17:03:29 +07:00
Book Pauk
8c99101bb3 Обновление пакетов 2022-07-25 16:41:07 +07:00
Book Pauk
d874f9ded4 Актуализация пакетов 2022-07-25 16:30:38 +07:00
Book Pauk
d7be4d3d94 Окончательное избавление от sqlite в пользу jembadb 2022-07-25 16:12:15 +07:00
Book Pauk
a2fa312839 Merge tag '0.11.8-7' into develop
0.11.8-7
2022-07-19 00:52:43 +07:00
Book Pauk
f7e1e09928 Merge branch 'release/0.11.8-7' 2022-07-19 00:52:36 +07:00
Book Pauk
f0832b07cb Исправление привнесенного бага 2022-07-19 00:50:44 +07:00
Book Pauk
7c253df291 Merge tag '0.11.8-6' into develop
0.11.8-6
2022-07-19 00:36:00 +07:00
Book Pauk
bb7cd9cbde Merge branch 'release/0.11.8-6' 2022-07-19 00:35:55 +07:00
Book Pauk
56c4182985 Небольшой тюнинг 2022-07-19 00:35:12 +07:00
Book Pauk
cb6c7536bf Небольшой тюнинг 2022-07-19 00:32:52 +07:00
Book Pauk
fbfe8cbda0 Решение проблемы невалидного tls-сертификата 2022-07-19 00:27:54 +07:00
Book Pauk
6129d2d7eb Небольшие поправки 2022-07-19 00:14:18 +07:00
Book Pauk
16b30c922a Улучшение работы с удаленным хранилищем 2022-07-18 23:54:25 +07:00
Book Pauk
c42ad66be6 Merge tag '0.11.8-5' into develop
0.11.8-5
2022-07-17 21:15:37 +07:00
Book Pauk
f36c13fea1 Merge branch 'release/0.11.8-5' 2022-07-17 21:15:31 +07:00
Book Pauk
4fd9d579e0 Небольшие доработки remoteSent, оптимизация отправки файлов 2022-07-17 21:10:52 +07:00
Book Pauk
e65a8a13ea Рефакторинг 2022-07-17 20:04:23 +07:00
Book Pauk
6ddb97d43e Тюнинг таймаутов 2022-07-17 17:11:34 +07:00
Book Pauk
89082603de Merge tag '0.11.8-4' into develop
0.11.8-4
2022-07-17 16:54:15 +07:00
Book Pauk
a9a3227433 Merge branch 'release/0.11.8-4' 2022-07-17 16:53:59 +07:00
Book Pauk
60cb3514b2 Тюнинг таймаутов 2022-07-17 16:53:12 +07:00
Book Pauk
4aeaa05f0b Merge tag '0.11.8-3' into develop
0.11.8-3
2022-07-17 15:58:34 +07:00
Book Pauk
9c06552278 Merge branch 'release/0.11.8-3' 2022-07-17 15:58:28 +07:00
Book Pauk
000f8dde82 Переход на RemoteStorage 2022-07-17 15:43:12 +07:00
Book Pauk
9ffc218002 Поправка 2022-07-16 21:36:50 +07:00
Book Pauk
68a188f099 Конфиг nginx 2022-07-16 21:10:33 +07:00
Book Pauk
8829bb3810 Конфиг nginx 2022-07-16 21:07:16 +07:00
Book Pauk
5164d2f536 Merge tag '0.11.8-2' into develop
0.11.8-2
2022-07-16 21:02:05 +07:00
Book Pauk
451538fcf7 Merge branch 'release/0.11.8-2' 2022-07-16 21:01:56 +07:00
Book Pauk
82a02ef339 Удаление более ненужной функциональности 2022-07-16 20:48:50 +07:00
Book Pauk
b834d4951f Обработка ошибок 2022-07-16 20:40:21 +07:00
Book Pauk
edc3b669be Добавлено восстановление файлов из webdav 2022-07-16 20:35:34 +07:00
Book Pauk
522826311d Переделка механизма чистки папок и отправки через RemoteWebDavStorage 2022-07-16 20:24:37 +07:00
Book Pauk
e69b9951d5 Отключил проверку валидности tls-сертификата 2022-07-16 18:43:09 +07:00
Book Pauk
c6300222ea Мелкий рефакторинг 2022-07-16 17:54:27 +07:00
Book Pauk
5aa6ee899c Изменение механизма работы с /tmp и /upload (начало) 2022-07-16 17:35:32 +07:00
Book Pauk
4b76f97d2b Поправки конфигов nginx 2022-07-16 15:45:52 +07:00
Book Pauk
5ccfe71c55 Начало работы над BookUpdateChecker 2022-07-16 13:16:57 +07:00
Book Pauk
97fc902cdb Поправлен баг 2022-07-15 23:53:54 +07:00
Book Pauk
7e935951d7 Поправка разметки 2022-07-15 23:17:30 +07:00
Book Pauk
810c6d68d2 Поправка разметки 2022-07-15 23:14:09 +07:00
Book Pauk
003dc70f4f Merge tag '0.11.8-1' into develop
0.11.8-1
2022-07-15 18:14:12 +07:00
Book Pauk
371ff64a95 Merge branch 'release/0.11.8-1' 2022-07-15 18:14:06 +07:00
Book Pauk
b0de5adbf3 Добавлена возможность скачивать обои 2022-07-15 18:11:24 +07:00
Book Pauk
d1d2b07c33 Поправки разметки 2022-07-15 17:42:19 +07:00
Book Pauk
d9b2444c1a Улучшен механизм загрузки обложек 2022-07-15 17:36:49 +07:00
Book Pauk
e7fae27031 Убрал отладку 2022-07-15 17:17:00 +07:00
Book Pauk
eb0c7b0a32 Отладка 2022-07-15 17:11:58 +07:00
Book Pauk
3d7ad0dd9a Небюольшие оптимизации загрузки обложек 2022-07-15 17:05:17 +07:00
Book Pauk
ae04feb311 Merge tag '0.11.8' into develop
0.11.8
2022-07-15 02:11:03 +07:00
Book Pauk
7b59f911ef Merge branch 'release/0.11.8' 2022-07-15 02:10:58 +07:00
Book Pauk
d3444da647 Поправки разметки 2022-07-15 01:58:42 +07:00
Book Pauk
66738d0c9c К предыдущему 2022-07-15 01:51:28 +07:00
Book Pauk
7e187acd68 Версия 0.11.8 2022-07-15 01:50:17 +07:00
Book Pauk
c751372a54 Добавлен resizeImage 2022-07-15 01:38:25 +07:00
Book Pauk
7fc98fc7da Добавление отображения обложки (coverpage) в окне загруженных файлов 2022-07-15 00:47:24 +07:00
Book Pauk
b56f45694e Добавлен coversStorage для хранения coverpage 2022-07-15 00:45:56 +07:00
Book Pauk
091ca521ef Новые upload-методы 2022-07-15 00:45:09 +07:00
Book Pauk
c7a17b0a76 Добавлена синхронизация файлов обоев 2022-07-14 20:14:40 +07:00
Book Pauk
26468b996a Мелкая поправка 2022-07-14 20:12:37 +07:00
Book Pauk
c4e240d87c Увеличил maxPayloadSize 2022-07-14 20:11:17 +07:00
Book Pauk
04713f47c8 Небольшие поправки 2022-07-14 16:14:25 +07:00
Book Pauk
37ab3493db Merge tag '0.11.7-6' into develop
0.11.7-6
2022-07-14 03:52:50 +07:00
Book Pauk
a4cb3c628e Merge branch 'release/0.11.7-6' 2022-07-14 03:52:44 +07:00
Book Pauk
8492da8a13 Небольшое улучшение 2022-07-14 03:51:59 +07:00
Book Pauk
98d7c64a56 Исправление багов 2022-07-14 03:34:55 +07:00
Book Pauk
25f121e5ed Merge tag '0.11.7-5' into develop
0.11.7-5
2022-07-14 01:57:36 +07:00
Book Pauk
4c8797c99c Merge branch 'release/0.11.7-5' 2022-07-14 01:57:30 +07:00
Book Pauk
1155aa285d Лишние пробелы 2022-07-14 01:57:03 +07:00
Book Pauk
239bbb8263 Добавлено восстановление из архива 2022-07-14 01:55:09 +07:00
Book Pauk
e6b9330108 Добавление работы с архивом 2022-07-14 01:17:09 +07:00
Book Pauk
935b767c2e Поправил поведение buttonActiveClass 2022-07-14 00:31:24 +07:00
Book Pauk
8acf3295b5 Поправил разметку 2022-07-14 00:31:09 +07:00
Book Pauk
48c3a12fa0 Улучшение парсинга плохих fb2 2022-07-14 00:30:27 +07:00
Book Pauk
a1dea514b7 Поправка разметки 2022-07-13 23:47:55 +07:00
Book Pauk
d4788439cb Merge tag '0.11.7-4' into develop
0.11.7-4
2022-07-13 16:38:10 +07:00
Book Pauk
0a60ad354c Merge branch 'release/0.11.7-4' 2022-07-13 16:38:04 +07:00
Book Pauk
c565a20344 Поправки разметки 2022-07-13 16:37:47 +07:00
Book Pauk
735ee88f0b Merge tag '0.11.7-3' into develop
0.11.7-3
2022-07-13 16:34:22 +07:00
Book Pauk
9405ce2cc0 Merge branch 'release/0.11.7-3' 2022-07-13 16:34:16 +07:00
Book Pauk
115277d88a Поправки разметки 2022-07-13 16:34:00 +07:00
Book Pauk
6925c11dbd Merge tag '0.11.7-2' into develop
0.11.7-2
2022-07-13 16:25:11 +07:00
141 changed files with 15275 additions and 11515 deletions

10
.gitignore vendored
View File

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

203
README.md
View File

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

View File

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

View File

@@ -1,69 +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);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
//ipfs
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs`;
if (!await fs.pathExists(ipfsDecompressedFilename)) {
// Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
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

@@ -14,6 +14,7 @@ module.exports = {
entry: [`${clientDir}/main.js`],
output: {
publicPath: '/app/',
clean: true
},
module: {
@@ -29,10 +30,6 @@ module.exports = {
}
}*/
},
{
resourceQuery: /^\?vue/,
use: path.resolve(__dirname, 'includer.js')
},
{
test: /\.js$/,
loader: 'babel-loader',

View File

@@ -1,5 +1,6 @@
const path = require('path');
const webpack = require('webpack');
const pckg = require('../package.json');
const { merge } = require('webpack-merge');
const baseWpConfig = require('./webpack.base.config');
@@ -8,15 +9,15 @@ baseWpConfig.entry.unshift('webpack-hot-middleware/client');
const HtmlWebpackPlugin = require('html-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');
module.exports = merge(baseWpConfig, {
mode: 'development',
devtool: 'inline-source-map',
output: {
path: `${publicDir}/app`,
filename: 'bundle.js'
path: `${publicDir}${baseWpConfig.output.publicPath}`,
filename: 'bundle.js',
},
module: {
@@ -38,6 +39,6 @@ module.exports = merge(baseWpConfig, {
template: `${clientDir}/index.html.template`,
filename: `${publicDir}/index.html`
}),
new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
new CopyWebpackPlugin({patterns: [{context: `${clientDir}/assets`, from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
]
});

View File

@@ -17,8 +17,8 @@ const clientDir = path.resolve(__dirname, '../client');
module.exports = merge(baseWpConfig, {
mode: 'production',
output: {
path: `${publicDir}/app_new`,
filename: 'bundle.[contenthash].js'
path: `${publicDir}${baseWpConfig.output.publicPath}`,
filename: 'bundle.[contenthash].js',
},
module: {
rules: [
@@ -54,7 +54,7 @@ module.exports = merge(baseWpConfig, {
filename: `${publicDir}/index.html`
}),
new CopyWebpackPlugin({patterns:
[{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
[{context: `${clientDir}/assets`, from: `${clientDir}/assets/*`, to: `${publicDir}/` }]
}),
new GenerateSW({
cacheId: 'liberama',

View File

@@ -1,63 +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);
//sqlite3
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
if (!await fs.pathExists(sqliteDecompressedFilename)) {
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
console.log(`done downloading ${sqliteRemoteUrl}`);
//распаковываем
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
console.log('files decompressed');
}
// копируем в дистрибутив
await fs.copy(sqliteDecompressedFilename, `${outDir}/node_sqlite3.node`);
console.log(`copied ${sqliteDecompressedFilename} to ${outDir}/node_sqlite3.node`);
//ipfs
const ipfsDecompressedFilename = `${tempDownloadDir}/go-ipfs/ipfs.exe`;
if (!await fs.pathExists(ipfsDecompressedFilename)) {
// Скачиваем ipfs
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
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,17 @@
import axios from 'axios';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api'
});
class Misc {
async loadConfig() {
const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch', 'networkLibraryLink',
]};
try {
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error)
throw new Error(config.error);
return config;
} catch (e) {
console.error(e);
}
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error)
throw new Error(config.error);
//если с WebSocket проблема, работаем по http
const response = await api.post('/config', query);
return response.data;
return config;
}
}

View File

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

View File

@@ -20,7 +20,6 @@ import StdDialog from './share/StdDialog.vue';
import sanitizeHtml from 'sanitize-html';
import miscApi from '../api/misc';
import * as utils from '../share/utils';
const componentOptions = {
components: {
@@ -31,7 +30,10 @@ const componentOptions = {
mode: function() {
this.setAppTitle();
this.redirectIfNeeded();
}
},
nightMode() {
this.setNightMode();
},
},
};
@@ -39,22 +41,40 @@ class App {
_options = componentOptions;
showPage = false;
itemRuText = {
'/cardindex': 'Картотека',
'/reader': 'Читалка',
'/forum': 'Форум-чат',
'/income': 'Поступления',
'/sources': 'Источники',
'/settings': 'Параметры',
'/help': 'Справка',
};
created() {
this.commit = this.$store.commit;
this.state = this.$store.state;
this.uistate = this.$store.state.uistate;
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
let cachedRoute = '';
let cachedPath = '';
@@ -66,7 +86,7 @@ class App {
}
return cachedRoute;
}
};
this.$router.beforeEach((to, from, next) => {
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
@@ -122,6 +142,8 @@ class App {
window.addEventListener('resize', (event) => {
this.$root.eventHook('resize', event);
});
this.setNightMode();
}
mounted() {
@@ -130,7 +152,7 @@ class App {
this.setAppTitle();
(async() => {
//загрузим конфиг сревера
//загрузим конфиг сервера
try {
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
@@ -155,38 +177,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() {
return this.state.apiError;
}
@@ -195,14 +185,23 @@ class App {
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) {
if (!title) {
if (this.mode == 'liberama.top') {
if (this.mode == 'liberama') {
document.title = `Liberama Reader - всегда с вами`;
} else if (this.mode == 'omnireader') {
document.title = `Omni Reader - всегда с вами`;
} else if (this.config && this.mode !== null) {
document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
document.title = `Универсальная читалка книг и ресурсов интернета`;
}
} else {
document.title = title;
@@ -217,33 +216,15 @@ class App {
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() {
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>" и редирект при необходимости
if (!this.isReaderActive) {
const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) {
q.url = decodeURIComponent(url);
}
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
}
//распознавание параметра url вида "?url=<link>" и редирект при необходимости
const s = search.split('url=');
const url = s[1] || '';
if (url) {
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: {url} });
}
}
}
@@ -253,22 +234,155 @@ export default vueComponent(App);
</script>
<style scoped>
.app-name {
margin-left: 10px;
margin-top: 10px;
text-align: center;
line-height: 140%;
font-weight: bold;
}
</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 {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font: normal 12pt ReaderDefault;
background-color: #333;
}
.q-notifications__list--top {
top: 55px !important;
}
.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>
<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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть выбранную закладку
</q-tooltip>
</q-btn>
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
<q-input ref="search" v-model="search" bg-color="input" class="col" outlined dense placeholder="Найти">
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
</template>
@@ -19,7 +19,7 @@
</div>
<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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Добавить закладку
@@ -62,6 +62,7 @@
v-model:ticked="ticked"
v-model:expanded="expanded"
class="q-my-xs"
color="input"
:nodes="nodes"
node-key="key"
tick-strategy="leaf"
@@ -347,6 +348,7 @@ export default vueComponent(BookmarkSettings);
padding: 0px 10px 10px 10px;
overflow-x: auto;
overflow-y: auto;
max-width: 520px;
}
.selected {

View File

@@ -29,6 +29,7 @@
ref="rootLink"
v-model="rootLink"
class="q-mr-sm"
bg-color="input"
:options="rootLinkOptions"
style="width: 230px"
dropdown-icon="la la-angle-down la-sm"
@@ -58,6 +59,7 @@
ref="selectedLink"
v-model="selectedLink"
class="q-mr-sm"
bg-color="input"
:options="selectedLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
@@ -73,8 +75,8 @@
ref="input"
v-model="bookUrl"
class="col q-mr-sm"
bg-color="input"
outlined dense
bg-color="white"
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
>
@@ -99,7 +101,7 @@
</template>
</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%">
Открыть в читалке
@@ -108,9 +110,9 @@
</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">
<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 v-show="transparentLayoutVisible" ref="transparentLayout" class="fit transparent-layout" @click="transparentLayoutClick"></div>
</div>
@@ -133,8 +135,8 @@
ref="bookmarkLink"
v-model="bookmarkLink"
class="col q-mr-sm"
bg-color="input"
outlined dense
bg-color="white"
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
>
</q-input>
@@ -143,6 +145,7 @@
ref="defaultRootLink"
v-model="defaultRootLink"
class="q-mr-sm"
bg-color="input"
:options="defaultRootLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
@@ -159,8 +162,8 @@
ref="bookmarkDesc"
v-model="bookmarkDesc"
class="col q-mr-sm"
bg-color="input"
outlined dense
bg-color="white"
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
>
</q-input>
@@ -304,7 +307,12 @@ class ExternalLibs {
openInFrameOnAdd = false;
frameScale = 1;
inpxReady = false;
inpxTitle = '';
inpxUrl = '';
created() {
this.commit = this.$store.commit;
this.oldStartLink = '';
this.justOpened = true;
this.$root.addEventHook('key', this.keyHook);
@@ -321,8 +329,6 @@ class ExternalLibs {
this.debouncedGoToLink = _.debounce((link) => {
this.goToLink(link);
}, 100, {'maxWait':200});
//this.commit = this.$store.commit;
//this.commit('reader/setLibs', rstore.libsDefaults);
}
mounted() {
@@ -334,10 +340,7 @@ class ExternalLibs {
i++;
}
if (this.mode != 'liberama.top') {
this.$router.replace('/404');
return;
}
this.libsDefaults = rstore.getLibsDefaults(this.mode);
this.$refs.window.init();
@@ -348,17 +351,28 @@ class ExternalLibs {
const openerOrigin2 = `https://${openerHost}`;
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)
return;
if (!_.isObject(event.data) || event.data.from != 'LibsPage')
return;
if (event.origin == openerOrigin1)
this.opener = window.opener;
else
this.opener = event.source;
this.openerOrigin = event.origin;
//console.log(event);
this.openerOrigin = event.origin;
this.recvMessage(event.data);
});
@@ -389,7 +403,10 @@ class ExternalLibs {
}
} else if (d.type == 'libs') {
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') {
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() {
if (this.opener.closed) {
await this.$root.stdDialog.alert('Потеряна связь с читалкой. Окно будет закрыто', 'Ошибка');
@@ -410,6 +451,11 @@ class ExternalLibs {
}
}
updateSets(sets) {
if (sets.nightMode !== this.nightMode)
this.commit('reader/nightModeToggle');
}
commitLibs(libs) {
this.sendMessage({type: 'libs', data: libs});
}
@@ -458,11 +504,24 @@ class ExternalLibs {
return this.$store.state.config.mode;
}
get nightMode() {
return this.$store.state.reader.settings.nightMode;
}
get header() {
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
let result = [this.ready ? 'Сетевая библиотека' : 'Загрузка...'];
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);
return result;
}
@@ -532,7 +591,7 @@ class ExternalLibs {
get defaultRootLinkOptions() {
let result = [];
rstore.libsDefaults.groups.forEach(group => {
this.libsDefaults.groups.forEach(group => {
result.push({label: lu.removeProtocol(group.r), value: group.r});
});
@@ -561,6 +620,11 @@ class ExternalLibs {
}
goToLink(link) {
this.inpxReady = false;
this.inpxTitle = '';
this.inpxUrl = '';
this.inpxOrigin = false;
if (!this.ready || !link)
return;
@@ -576,6 +640,7 @@ class ExternalLibs {
this.frameVisible = true;
this.$nextTick(() => {
if (this.$refs.frame) {
this.$refs.frame.contentWindow.location.reload(true);
this.$refs.frame.contentWindow.focus();
this.frameResize();
}
@@ -648,13 +713,17 @@ class ExternalLibs {
this.updateStartLink(true);
}
submitUrl() {
if (this.bookUrl) {
submitUrl(url) {
if (!url) {
url = this.bookUrl;
this.bookUrl = '';
}
if (url) {
this.sendMessage({type: 'submitUrl', data: {
url: this.bookUrl,
url,
force: true
}});
this.bookUrl = '';
if (this.closeAfterSubmit)
this.close();
}
@@ -668,6 +737,12 @@ class ExternalLibs {
} else {
this.bookmarkLink = this.bookUrl;
this.bookmarkDesc = '';
if (!this.bookmarkLink && this.inpxReady && this.inpxUrl) {
this.bookmarkLink = this.inpxUrl;
if (this.inpxTitle)
this.bookmarkDesc = this.inpxTitle;
}
}
this.addBookmarkMode = mode;
@@ -679,10 +754,10 @@ class ExternalLibs {
}
updateBookmarkLink() {
const index = lu.getSafeRootIndexByUrl(rstore.libsDefaults.groups, this.defaultRootLink);
const index = lu.getSafeRootIndexByUrl(this.libsDefaults.groups, this.defaultRootLink);
if (index >= 0) {
this.bookmarkLink = rstore.libsDefaults.groups[index].s;
this.bookmarkDesc = this.getCommentByLink(rstore.libsDefaults.groups[index].list, this.bookmarkLink);
this.bookmarkLink = this.libsDefaults.groups[index].s;
this.bookmarkDesc = this.getCommentByLink(this.libsDefaults.groups[index].list, this.bookmarkLink);
} else {
this.bookmarkLink = '';
this.bookmarkDesc = '';
@@ -837,20 +912,22 @@ class ExternalLibs {
<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>
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
к третьим лицам.
</p>
`
: '') +
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
`<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
На мобильных устройствах для этого служит системная клавиша 'Назад (стрелка влево)' и опция 'Вперед (стрелка вправо)' в меню браузера.
</p>
<p>Приятного пользования ;-)
</p>
`, 'Справка', {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>
<div class="bg-grey-3 row">
<div class="bg-menu-1 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
active-color="app"
active-bg-color="app"
indicator-color="bg-app"
dense
no-caps
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="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>
</div>
@@ -80,13 +80,13 @@
<div class="image-num">
{{ item.num }}
</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
</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
</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
</div>
</div>
@@ -250,7 +250,7 @@ class ContentsPage {
const bin = parsed.binary[image.id];
const type = (bin ? bin.type : '');
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: var(--bg-menu-color2)"><i>Без названия</i></span>');
const indentStyle = getIndentStyle(1);
const labelStyle = getLabelStyle(1);
@@ -466,27 +466,31 @@ export default vueComponent(ContentsPage);
}
.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 {
background-color: #f0f0f0;
background-color: var(--bg-menu-color2);
}
.item-book-pos {
background-color: #b0f0b0;
opacity: 1;
background-color: var(--bg-selected-item-color1);
}
.subitem-book-pos {
background-color: #d0f5d0;
opacity: 1;
background-color: var(--bg-selected-item-color2);
}
.item-book-pos:hover {
background-color: #b0e0b0;
opacity: 0.8;
transition: opacity 0.2s linear;
}
.subitem-book-pos:hover {
background-color: #d0f0d0;
opacity: 0.8;
transition: opacity 0.2s linear;
}
.expand-button, .no-expand-button {
@@ -535,6 +539,7 @@ export default vueComponent(ContentsPage);
.image-thumb {
height: 50px;
background-color: white;
}
.loading-img-icon {

View File

@@ -24,7 +24,7 @@
</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>
Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
<br><strong>{{ bookmarkText }}</strong>
@@ -59,7 +59,7 @@ class CommonHelpPage {
}
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) {
@@ -88,6 +88,6 @@ export default vueComponent(CommonHelpPage);
margin-left: 10px;
cursor: pointer;
font-size: 120%;
color: blue;
color: var(--text-anchor-color);
}
</style>

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -1,20 +1,20 @@
<template>
<Window @close="close">
<Window style="z-index: 200" @close="close">
<template #header>
Справка
</template>
<div class="col column" style="min-width: 600px">
<div class="bg-grey-3 row">
<div class="bg-menu-1 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
active-color="app"
active-bg-color="app"
indicator-color="bg-app"
dense
no-caps
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-tabs>
@@ -36,14 +36,14 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
const pages = {
'CommonHelpPage': CommonHelpPage,
'HotkeysHelpPage': HotkeysHelpPage,
'MouseHelpPage': MouseHelpPage,
'VersionHistoryPage': VersionHistoryPage,
//'DonateHelpPage': DonateHelpPage,
'DonateHelpPage': DonateHelpPage,
};
const tabs = [
@@ -51,7 +51,7 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
//['DonateHelpPage', 'Помочь проекту'],
['DonateHelpPage', 'Помочь проекту'],
];
const componentOptions = {
@@ -80,7 +80,7 @@ class HelpPage {
}
activateDonateHelpPage() {
//this.selectedTab = 'DonateHelpPage';
this.selectedTab = 'DonateHelpPage';
}
activateVersionHistoryHelpPage() {

View File

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

View File

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

View File

@@ -72,7 +72,7 @@ p {
}
.clickable {
color: blue;
color: var(--text-anchor-color);
text-decoration: underline;
cursor: pointer;
}

View File

@@ -8,7 +8,7 @@ import vueComponent from '../../vueComponent.js';
import Window from '../../share/Window.vue';
import * as utils from '../../../share/utils';
//import rstore from '../../../store/modules/reader';
import rstore from '../../../store/modules/reader';
import _ from 'lodash';
const componentOptions = {
@@ -28,13 +28,18 @@ class LibsPage {
this.popupWindow = null;
this.commit = this.$store.commit;
this.messageListener = null;
//this.commit('reader/setLibs', rstore.libsDefaults);
}
init() {
if (this.mode != 'liberama.top')
async init() {
if (!this.mode)
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;
const subdomain = (window.location.protocol != 'http:' ? 'b.' : '');
this.origin = `http://${subdomain}${window.location.host}`;
@@ -114,8 +119,12 @@ class LibsPage {
return this.$store.state.reader.libs;
}
get nightMode() {
return this.$store.state.reader.settings.nightMode;
}
sendLibs() {
this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs)});
this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs), sets: {nightMode: this.nightMode}});
}
close() {

View File

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

View File

@@ -8,9 +8,11 @@
</span>
</template>
<q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
<hr />
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
<div class="fit column main">
<q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
<hr />
<textarea ref="textArea" class="main text" @paste="calcTitle"></textarea>
</div>
</Window>
</template>
@@ -39,6 +41,10 @@ class PasteTextPage {
this.$refs.textArea.focus();
}
get dark() {
return this.$store.state.reader.settings.nightMode;
}
getNonEmptyLine3words(text, count) {
let result = '';
const lines = text.split("\n");
@@ -60,7 +66,7 @@ class PasteTextPage {
calcTitle(event) {
if (this.bookTitle == '') {
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`;
this.bookTitle = `Из буфера обмена ${utils.dateFormat(new Date())}`;
if (event) {
let text = event.clipboardData.getData('text');
this.bookTitle += ': ' + _.compact([
@@ -115,6 +121,11 @@ export default vueComponent(PasteTextPage);
outline: none;
}
.main {
color: var(--text-app-color);
background-color: var(--bg-app-color);
}
hr {
margin: 0;
padding: 0;

View File

@@ -1,137 +1,148 @@
<template>
<div class="column no-wrap">
<div v-show="toolBarActive" ref="header" class="header">
<div ref="buttons" class="row justify-between no-wrap">
<div class="row no-wrap">
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
<q-icon name="la la-arrow-left" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loader'] }}
</q-tooltip>
</button>
<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-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadFile'] }}
</q-tooltip>
</button>
<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-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadBuffer'] }}
</q-tooltip>
</button>
<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-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['help'] }}
</q-tooltip>
</button>
</div>
<div ref="buttons" class="row" :class="{'no-wrap': !toolBarMultiLine}">
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
<q-icon name="la la-arrow-left" size="32px" />
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loader'] }}
</q-tooltip>
</button>
<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-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadFile'] }}
</q-tooltip>
</button>
<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-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['loadBuffer'] }}
</q-tooltip>
</button>
<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-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
{{ rstore.readerActions['help'] }}
</q-tooltip>
</button>
<div class="row no-wrap">
<div class="space"></div>
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
<q-icon name="la la-angle-left" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['undoAction'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
<q-icon name="la la-angle-right" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['redoAction'] }}
</q-tooltip>
</button>
<div class="space"></div>
<button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['fullScreen'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
<q-icon name="la la-film" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['scrolling'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
<q-icon name="la la-angle-double-right" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['setPosition'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
<q-icon name="la la-search" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['search'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
<q-icon name="la la-copy" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['copyText'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
<q-icon name="la la-magic" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['convOptions'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['refresh'] }}
</q-tooltip>
</button>
<div class="space"></div>
<button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
<q-icon name="la la-list" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['contents'] }}
</q-tooltip>
</button>
<button v-show="mode == 'liberama.top' && showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
<q-icon name="la la-sitemap" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['libs'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
<q-icon name="la la-book-open" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['recentBooks'] }}
</q-tooltip>
</button>
<div class="space"></div>
</div>
<div class="col"></div>
<div class="row no-wrap">
<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" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['clickControl'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
<q-icon name="la la-unlink" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['offlineMode'] }}
</q-tooltip>
</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 class="space"></div>
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
<q-icon name="la la-angle-left" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['undoAction'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
<q-icon name="la la-angle-right" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['redoAction'] }}
</q-tooltip>
</button>
<div class="space"></div>
<button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['fullScreen'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
<q-icon name="la la-film" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['scrolling'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
<q-icon name="la la-angle-double-right" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['setPosition'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
<q-icon name="la la-search" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['search'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
<q-icon name="la la-copy" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['copyText'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
<q-icon name="la la-magic" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['convOptions'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['refresh'] }}
</q-tooltip>
</button>
<div v-show="showToolButton['libs']" class="space"></div>
<button v-show="showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
<q-icon name="la la-sitemap" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['libs'] }}
</q-tooltip>
</button>
<div class="space"></div>
<button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
<q-icon name="la la-list" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['contents'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
<div v-show="bothBucEnabled && needBookUpdateCount > 0" style="position: absolute">
<div class="need-book-update-count">
{{ needBookUpdateCount }}
</div>
</div>
<q-icon name="la la-book-open" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['recentBooks'] }}
</q-tooltip>
</button>
<div class="space"></div>
<div class="col"></div>
<button v-show="showToolButton['nightMode']" ref="nightMode" v-ripple class="tool-button" :class="buttonActiveClass('nightMode')" @click="buttonClick('nightMode')">
<q-icon name="la la-moon" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['nightMode'] }}
</q-tooltip>
</button>
<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" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['clickControl'] }}
</q-tooltip>
</button>
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
<q-icon name="la la-unlink" size="32px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ rstore.readerActions['offlineMode'] }}
</q-tooltip>
</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 class="main col row relative-position">
<div class="col row relative-position main">
<keep-alive>
<component
:is="activePage"
@@ -156,7 +167,7 @@
></SearchPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose" @update-count-changed="updateCountChanged"></RecentBooksPage>
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
@@ -194,6 +205,7 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
import bookManager from './share/bookManager';
import wallpaperStorage from './share/wallpaperStorage';
import coversStorage from './share/coversStorage';
import dynamicCss from '../../share/dynamicCss';
import rstore from '../../store/modules/reader';
@@ -284,8 +296,9 @@ class Reader {
contentsActive = false;
libsActive = false;
recentBooksActive = false;
nightModeActive = false;
clickControlActive = false;
offlineModeActive = false;
settingsActive = false;
clickMapActive = false;
@@ -298,6 +311,8 @@ class Reader {
showRefreshIcon = true;
mostRecentBookReactive = null;
showToolButton = {};
toolBarHideOnScroll = false;
toolBarMultiLine = false;
actionList = [];
actionCur = -1;
@@ -308,6 +323,10 @@ class Reader {
donationVisible = false;
dualPageMode = false;
bucEnabled = false;
bucSetOnNew = false;
needBookUpdateCount = 0;
created() {
this.rstore = rstore;
this.loading = true;
@@ -356,6 +375,35 @@ class Reader {
}
}, 200);
this.debouncedRecentBooksPageUpdate = _.debounce(async() => {
if (this.recentBooksActive) {
await this.$refs.recentBooksPage.updateTableData();
}
}, 100);
this.recentItemKeys = [];
this.debouncedSaveRecent = _.debounce(async() => {
let timer = setTimeout(() => {
if (!this.offlineModeActive)
this.$root.notify.error('Таймаут соединения');
}, 10000);
try {
const itemKeys = this.recentItemKeys;
this.recentItemKeys = [];
//сохранение в удаленном хранилище
await this.$refs.serverStorage.saveRecent(itemKeys);
//periodicTasks
this.periodicTasks();//no await
} catch (e) {
if (!this.offlineModeActive)
this.$root.notify.error(e.message);
} finally {
clearTimeout(timer);
}
}, 500, {maxWait: 1000});
document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null);
});
@@ -366,6 +414,8 @@ class Reader {
mounted() {
(async() => {
await wallpaperStorage.init();
await coversStorage.init();
await bookManager.init(this.settings);
bookManager.addEventListener(this.bookManagerEvent);
@@ -391,16 +441,19 @@ class Reader {
this.updateRoute();
await this.$refs.dialogs.init();
this.$refs.recentBooksPage.init();
})();
//единственный запуск periodicTasks при инициализации
//дальнейшие запуски periodicTasks выполняются из debouncedSaveRecent
//т.е. только по действию пользователя
(async() => {
await utils.sleep(15*1000);
this.isFirstNeedUpdateNotify = true;
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
while (true) {// eslint-disable-line no-constant-condition
await this.checkNewVersionAvailable();
await utils.sleep(3600*1000); //каждый час
}
//дальше кода нет
this.allowPeriodicTasks = true;
this.periodicTasks();//no await
})();
}
@@ -409,11 +462,12 @@ class Reader {
this.allowUrlParamBookPos = settings.allowUrlParamBookPos;
this.copyFullText = settings.copyFullText;
this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl;
this.clickControlActive = this.clickControl;
this.nightModeActive = settings.nightMode;
this.clickControlActive = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.toolBarMultiLine = settings.toolBarMultiLine;
this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara;
@@ -422,6 +476,11 @@ class Reader {
this.pdfQuality = settings.pdfQuality;
this.dualPageMode = settings.dualPageMode;
this.userWallpapers = settings.userWallpapers;
this.bucEnabled = settings.bucEnabled;
this.bucSizeDiff = settings.bucSizeDiff;
this.bucSetOnNew = settings.bucSetOnNew;
this.bucCancelEnabled = settings.bucCancelEnabled;
this.bucCancelDays = settings.bucCancelDays;
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
this.$root.readerActionByKeyEvent = (event) => {
@@ -450,50 +509,185 @@ class Reader {
//wallpaper css
async loadWallpapers() {
const wallpaperDataLength = await wallpaperStorage.getLength();
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
this.wallpaperDataLength = wallpaperDataLength;
if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
let newCss = '';
let updated = false;
const wallpaperExists = new Set();
for (const wp of this.userWallpapers) {
const data = await wallpaperStorage.getData(wp.cssClass);
wallpaperExists.add(wp.cssClass);
let data = await wallpaperStorage.getData(wp.cssClass);
if (!data) {
//здесь будем восстанавливать данные с сервера
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
try {
data = await readerApi.getUploadedFileBuf(url);
await wallpaperStorage.setData(wp.cssClass, data);
updated = true;
} catch (e) {
console.error(e);
}
}
if (data) {
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
}
}
//почистим wallpaperStorage
for (const key of await wallpaperStorage.getKeys()) {
if (!wallpaperExists.has(key)) {
await wallpaperStorage.removeData(key);
}
}
//обновим settings, если загружали обои из /upload/
if (updated) {
this.commit('reader/setSettings', {});
}
dynamicCss.replace('wallpapers', newCss);
}
}
async checkNewVersionAvailable() {
if (!this.checkingNewVersion && this.showNeedUpdateNotify) {
this.checkingNewVersion = true;
try {
await utils.sleep(15*1000); //подождем 15 секунд, чтобы прогрузился ServiceWorker при выходе новой версии
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
async periodicTasks() {
if (!this.allowPeriodicTasks || this.doingPeriodicTasks)
return;
let againMes = '';
if (this.isFirstNeedUpdateNotify) {
againMes = ' еще один раз';
this.doingPeriodicTasks = true;
try {
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;
}
}
async checkBuc() {
if (!this.bothBucEnabled)
return;
const sorted = bookManager.getSortedRecent();
//выберем все кандидиаты на обновление
const updateUrls = new Set();
for (const book of sorted) {
if (!book.deleted && book.checkBuc && book.url && book.url.indexOf('disk://') !== 0)
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 checkSetTime = {};
//проставим новые размеры у книг
for (const book of sorted) {
if (book.deleted)
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
//от этой даты будем потом отсчитывать bucCancelDays
if (updateUrls.has(book.url)) {
let rec = checkSetTime[book.url] || {time: 0, loadTime: 0};
const time = (book.checkBucTime ? book.checkBucTime : (rec.loadTime || 0));
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
//снимем флаг checkBuc у необновлявшихся bucCancelDays
if (this.bucCancelEnabled) {
for (const rec of Object.values(checkSetTime)) {
if (rec.time && Date.now() - rec.time > this.bucCancelDays*24*3600*1000) {
const book = await bookManager.getRecentBook({key: rec.key});
const needBookUpdate =
book.checkBuc
&& book.bucSize
&& utils.hasProp(book, 'downloadSize')
&& book.bucSize !== book.downloadSize
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff)
;
if (book && !needBookUpdate) {
await bookManager.setCheckBuc(book, undefined);//!!!
}
}
}
}
await this.$refs.recentBooksPage.updateTableData();
}
updateCountChanged(event) {
this.needBookUpdateCount = event.needBookUpdateCount;
}
checkSetStorageAccessKey() {
const q = this.$route.query;
@@ -552,7 +746,7 @@ class Reader {
return;
const recent = this.mostRecentBook();
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
const url = (recent ? `url=${recent.url}` : '');
const url = (recent ? `url=${encodeURIComponent(recent.url)}` : '');
if (isNewRoute)
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
else
@@ -572,6 +766,10 @@ class Reader {
return versionHistory[0].version;
}
get bothBucEnabled() {
return this.$store.state.config.bucEnabled && this.bucEnabled;
}
get routeParamUrl() {
let result = '';
const path = this.$route.fullPath;
@@ -620,27 +818,12 @@ class Reader {
}
if (eventName == 'recent-changed') {
if (this.recentBooksActive) {
await this.$refs.recentBooksPage.updateTableData();
}
this.debouncedRecentBooksPageUpdate();
//сохранение в serverStorage
if (value) {
await utils.sleep(500);
let timer = setTimeout(() => {
if (!this.offlineModeActive)
this.$root.notify.error('Таймаут соединения');
}, 10000);
try {
await this.$refs.serverStorage.saveRecent(value);
} catch (e) {
if (!this.offlineModeActive)
this.$root.notify.error(e.message);
} finally {
clearTimeout(timer);
}
if (value && this.recentItemKeys.indexOf(value) < 0) {
this.recentItemKeys.push(value);
this.debouncedSaveRecent();
}
}
}
@@ -649,6 +832,10 @@ class Reader {
return this.reader.toolBarActive;
}
get offlineModeActive() {
return this.reader.offlineModeActive;
}
mostRecentBook() {
const result = bookManager.mostRecentBook();
this.mostRecentBookReactive = result;
@@ -679,8 +866,7 @@ class Reader {
}
fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) {
if (!this.$q.fullscreen.isActive) {
this.$q.fullscreen.request();
} else {
this.$q.fullscreen.exit();
@@ -846,23 +1032,33 @@ class Reader {
}
libsToogle() {
if (this.config.networkLibraryLink) {
window.open(this.config.networkLibraryLink, '_blank');
return;
}
this.libsActive = !this.libsActive;
if (this.libsActive) {
this.$refs.libsPage.init();
this.$refs.libsPage.init();//no await
} else {
this.$refs.libsPage.done();
}
}
nightModeToggle() {
if (!this.nightModeActive && !utils.hasProp(this.settings.nightColorSets, 'textColor')) {
this.$root.notify.warning(`Ночной режим активирован впервые. Цвета заданы по умолчанию.`);
}
this.commit('reader/nightModeToggle');
}
clickControlToggle() {
const newSettings = _.cloneDeep(this.settings);
newSettings.clickControl = !this.clickControl;
this.commit('reader/setSettings', newSettings);
this.commit('reader/setSettings', {clickControl: !this.clickControlActive});
}
offlineModeToggle() {
this.offlineModeActive = !this.offlineModeActive;
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
this.commit('reader/setOfflineModeActive', !this.offlineModeActive);
}
settingsToggle() {
@@ -960,6 +1156,7 @@ class Reader {
case 'contents':
case 'libs':
case 'recentBooks':
case 'nightMode':
case 'clickControl':
case 'offlineMode':
case 'settings':
@@ -998,7 +1195,6 @@ class Reader {
classResult = classDisabled;
break;
case 'refresh':
case 'recentBooks':
if (!this.mostRecentBookReactive)
classResult = classDisabled;
break;
@@ -1009,7 +1205,7 @@ class Reader {
}
async activateClickMapPage() {
if (this.clickControl && this.showClickMapPage && !this.clickMapActive) {
if (this.clickControlActive && this.showClickMapPage && !this.clickMapActive) {
this.clickMapActive = true;
await this.$refs.clickMapPage.slowDisappear();
this.clickMapActive = false;
@@ -1108,6 +1304,7 @@ class Reader {
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
wasOpened = Object.assign(wasOpened, {
url: (opts.url !== undefined ? opts.url : wasOpened.url),
path: (opts.path !== undefined ? opts.path : wasOpened.path),
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
@@ -1133,6 +1330,7 @@ class Reader {
this.checkBookPosPercent();
this.activateClickMapPage();//no await
this.$refs.recentBooksPage.updateTableData();//no await
return;
}
@@ -1186,6 +1384,7 @@ class Reader {
found = (found ? _.cloneDeep(found) : found);
if (found) {
//если такой файл уже не загружен (path не совпадают)
if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
@@ -1209,9 +1408,13 @@ class Reader {
delete wasOpened.loadTime;
// добавляем в историю
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
const recentBook = await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
if (this.bucSetOnNew) {
await bookManager.setCheckBuc(recentBook, true);
}
this.mostRecentBook();
this.addAction(wasOpened.bookPos);
this.addAction(recentBook.bookPos);
this.updateRoute(true);
this.loaderActive = false;
@@ -1223,14 +1426,13 @@ class Reader {
this.checkBookPosPercent();
this.activateClickMapPage();//no await
this.$refs.recentBooksPage.updateTableData();//no await
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
} finally {
this.checkNewVersionAvailable();
}
}
@@ -1360,6 +1562,9 @@ class Reader {
case 'recentBooks':
this.recentBooksToggle();
break;
case 'nightMode':
this.nightModeToggle();
break;
case 'clickControl':
this.clickControlToggle();
break;
@@ -1485,46 +1690,41 @@ export default vueComponent(Reader);
<style scoped>
.header {
height: 50px;
padding-left: 5px;
padding-right: 5px;
padding: 5px 5px 0px 5px;
background-color: #1B695F;
color: #000;
overflow-x: auto;
overflow-y: hidden;
scrollbar-color: #c49a60 #e4e4e4;
scrollbar-color: #c4aa60 #e4e4e4;
}
.header::-webkit-scrollbar {
height: 10px;
height: 5px;
}
.header::-webkit-scrollbar-track {
background-color: #e4e4e4;
border-radius: 4px;
background-color: #1B695F;
border-radius: 1px;
}
.header::-webkit-scrollbar-thumb {
background-color: #c49a60;
border-radius: 4px;
border: 2px solid #e4e4e4;
}
.header::-webkit-scrollbar-thumb:hover {
background-color: #b48a50;
background-color: #c4aa60;
border-radius: 1px;
border: 1px solid #1B695F;
}
.main {
background-color: #EBE2C9;
color: #000;
background-color: var(--bg-loader-color);
color: var(--text-app-color);
}
.tool-button {
margin: 0px 2px 0 2px;
margin: 0px 2px 7px 2px;
padding: 0;
color: #3E843E;
background-color: #E6EDF4;
margin-top: 5px;
color: var(--text-tb-normal);
background-color: var(--bg-tb-normal);
min-height: 38px;
min-width: 38px;
height: 38px;
width: 38px;
border: 0;
@@ -1534,34 +1734,33 @@ export default vueComponent(Reader);
}
.tool-button:hover {
background-color: white;
background-color: var(--bg-tb-hover);
cursor: pointer;
}
.tool-button-active {
box-shadow: 0 0 0;
color: white;
background-color: #8AB45F;
color: var(--text-tb-active);
background-color: var(--bg-tb-active);
position: relative;
top: 1px;
left: 1px;
}
.tool-button-active:hover {
color: white;
background-color: #81C581;
background-color: var(--bg-tb-active-hover);
cursor: pointer;
}
.tool-button-disabled {
color: lightgray;
background-color: gray;
color: var(--text-tb-disabled);
background-color: var(--bg-tb-disabled);
cursor: default;
}
.tool-button-disabled:hover {
color: lightgray;
background-color: gray;
color: var(--text-tb-disabled);
background-color: var(--bg-tb-disabled);
cursor: default;
}
@@ -1573,4 +1772,16 @@ export default vueComponent(Reader);
.clear {
color: rgba(0,0,0,0);
}
.need-book-update-count {
position: relative;
padding: 2px 6px 2px 6px;
left: 27px;
top: 22px;
background-color: blue;
border-radius: 10px;
color: white;
z-index: 10;
font-size: 80%;
}
</style>

View File

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

View File

@@ -7,32 +7,58 @@
</span>
</template>
<template #buttons>
<div
v-show="needBookUpdateCount > 0"
class="row justify-center items-center"
:class="{'header-button-update': !showNeedBookUpdateOnly, 'header-button-update-pressed': showNeedBookUpdateOnly}"
@mousedown.stop @click="showNeedBookUpdateOnlyToggle"
>
<span style="font-size: 90%">{{ needBookUpdateCount }} обновлен{{ wordEnding(needBookUpdateCount, 3) }}</span>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ (needBookUpdateCount ? 'Скрыть обновления' : 'Показать обновления') }}
</q-tooltip>
</div>
<div
class="row justify-center items-center"
:class="{'header-button': !showArchive, 'header-button-pressed': showArchive}"
@mousedown.stop @click="showArchiveToggle"
>
<q-icon class="q-mr-xs" name="la la-archive" size="20px" />
<span style="font-size: 90%">Архив</span>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ (showArchive ? 'Скрыть архивные' : 'Показать архивные') }}
</q-tooltip>
</div>
</template>
<a ref="download" style="display: none;" target="_blank"></a>
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
<div ref="header" class="scroll-header row bg-blue-2">
<q-btn class="tool-button" round @click="showSameBookClick">
<div ref="header" class="scroll-header row bg-header-3">
<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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Показать/скрыть версии книг
</q-tooltip>
</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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В начало списка
</q-tooltip>
</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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В конец списка
</q-tooltip>
</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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
На текущую книгу
@@ -45,7 +71,7 @@
class="q-ml-sm q-mt-xs"
outlined dense
style="width: 185px"
bg-color="white"
bg-color="input"
placeholder="Найти"
@click.stop
>
@@ -60,7 +86,7 @@
class="q-ml-sm q-mt-xs"
:options="sortMethodOptions"
style="width: 180px"
bg-color="white"
bg-color="input"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options display-value-sanitize options-sanitize
options-html display-value-html
@@ -86,13 +112,22 @@
@virtual-scroll="onScroll"
>
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px; border-right: 1px solid #cccccc">
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px">
<q-icon name="la la-code-branch" size="24px" style="color: green" />
</div>
<div class="row-part column justify-center items-stretch" style="width: 80px">
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
<div class="col row justify-center items-center clickable" style="padding: 0 2px 0 2px" @click="loadBook(item, bothBucEnabled && item.needBookUpdate)">
<div v-show="isLoadedCover(item.coverPageUrl)" style="height: 80px" v-html="getCoverHtml(item.coverPageUrl)" />
<q-icon v-show="!isLoadedCover(item.coverPageUrl)" name="la la-book" size="40px" style="color: #dddddd" />
<div
v-show="bothBucEnabled && item.needBookUpdate"
class="column justify-center"
style="position: absolute; background-color: rgba(255, 255, 255, 0.5); border-radius: 40px;"
>
<q-icon name="la la-sync" size="60px" style="color: blue" />
</div>
</div>
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
@@ -100,12 +135,21 @@
</div>
</div>
<div class="row-part column items-stretch clickable break-word" :style="{ 'width': (350 - 40*(+item.inGroup)) + 'px' }" style="font-size: 75%" @click="loadBook(item)">
<div class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
<div class="text-green-10" style="font-size: 105%">
<div class="row-part column items-stretch clickable break-word" @click="loadBook(item)">
<div
class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
:style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
>
<div :class="dark ? 'text-lime-4' : 'text-green-10'" style="font-size: 80%">
{{ item.desc.author }}
</div>
<div>{{ item.desc.title }}</div>
<div style="font-size: 75%">
{{ item.desc.title }}
</div>
<div v-show="bothBucEnabled && item.needBookUpdate" style="font-size: 75%; color: blue;">
Размер: {{ item.bucSize - item.downloadSize > 0 ? '+' : '' }}{{ item.bucSize - item.downloadSize }}
({{ item.downloadSize }} &rarr; {{ item.bucSize }})
</div>
</div>
<div class="row" style="font-size: 10px">
@@ -113,7 +157,7 @@
{{ item.desc.textLen }}
</div>
<div class="row items-center row-info-top" :style="`width: ${(220 - 40*(+item.inGroup))}px; padding: 1px`">
<div class="row items-center row-info-top" :style="`width: ${(260 - 40*(+item.inGroup))}px; padding: 1px`">
<div class="read-bar" :style="`width: ${100*item.readPart}%`"></div>
</div>
@@ -124,7 +168,7 @@
</div>
</div>
<div class="row" style="font-size: 10px" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
<div class="row" style="font-size: 10px" :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }">
<div class="row justify-center items-center row-info-bottom" style="width: 30px">
{{ item.num }}
</div>
@@ -141,21 +185,61 @@
</div>
</div>
<div class="row-part column justify-center" style="width: 80px; font-size: 75%">
<div>
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
</div>
</div>
<div class="row-part column justify-center">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(item.key)"
<div
class="row-part column"
style="width: 90px;"
>
<div
class="col column justify-center"
style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
>
<q-icon class="la la-times" size="14px" />
</q-btn>
<div style="margin: 25px 0 0 5px">
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
</div>
</div>
<div
class="del-button self-end row justify-center items-center clickable"
@click="handleDel(item)"
>
<q-icon class="la la-times" size="12px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
{{ (showArchive ? 'Удалить окончательно' : 'Перенести в архив') }}
</q-tooltip>
</div>
<div
v-show="showArchive"
class="restore-button self-start row justify-center items-center clickable"
@click="handleRestore(item)"
>
<q-icon class="la la-arrow-left" size="14px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Восстановить из архива
</q-tooltip>
</div>
<div
v-show="bothBucEnabled && item.showCheckBuc"
class="buc-checkbox self-start"
>
<q-checkbox
v-model="item.checkBuc"
size="xs"
style="position: relative; top: -3px; left: -3px;"
@update:model-value="checkBucChange(item)"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
<div v-if="item.checkBuc === undefined">
Проверка обновлений отключена автоматически<br>т.к. книга не обновлялась {{ bucCancelDays }} дней
</div>
<div v-else>
{{ (item.checkBuc ? 'Проверка обновлений книги включена' : 'Проверка обновлений книги отключена') }}
</div>
</q-tooltip>
</q-checkbox>
</div>
</div>
</div>
</q-virtual-scroll>
@@ -175,6 +259,7 @@ import LockQueue from '../../../share/LockQueue';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
import coversStorage from '../share/coversStorage';
const componentOptions = {
components: {
@@ -190,6 +275,12 @@ const componentOptions = {
settings() {
this.loadSettings();
},
needBookUpdateCount() {
if (this.needBookUpdateCount == 0)
this.showNeedBookUpdateOnly = false;
this.$emit('update-count-changed', {needBookUpdateCount: this.needBookUpdateCount});
}
},
};
class RecentBooksPage {
@@ -200,6 +291,17 @@ class RecentBooksPage {
tableData = [];
sortMethod = '';
showSameBook = false;
bucEnabled = false;
bucSizeDiff = 0;
bucSetOnNew = false;
bucCancelDays = 0;
needBookUpdateCount = 0;
showArchive = false;
showNeedBookUpdateOnly = false;
covers = {};
coversLoadFunc = {};
created() {
this.commit = this.$store.commit;
@@ -225,6 +327,7 @@ class RecentBooksPage {
this.showBar();
await this.updateTableData();
await this.scrollToActiveBook();
//await this.scrollRefresh();
})();
}
@@ -232,12 +335,24 @@ class RecentBooksPage {
const settings = this.settings;
this.showSameBook = settings.recentShowSameBook;
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
this.bucEnabled = settings.bucEnabled;
this.bucSizeDiff = settings.bucSizeDiff;
this.bucSetOnNew = settings.bucSetOnNew;
this.bucCancelDays = settings.bucCancelDays;
}
get settings() {
return this.$store.state.reader.settings;
}
get bothBucEnabled() {
return this.$store.state.config.bucEnabled && this.bucEnabled;
}
get dark() {
return this.$store.state.reader.settings.nightMode;
}
async updateTableData() {
if (!this.inited)
return;
@@ -251,15 +366,15 @@ class RecentBooksPage {
//подготовка полей
for (const book of sorted) {
if (book.deleted)
if ((!this.showArchive && book.deleted) || (this.showArchive && book.deleted != 1))
continue;
let d = new Date();
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);
d.setTime(loadTimeRaw);
const loadTime = utils.formatDate(d);
const loadTime = utils.dateFormat(d, 'DD.MM.YYYY HH:mm');
let readPart = 0;
let perc = '';
@@ -275,9 +390,14 @@ class RecentBooksPage {
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url))) || '';
result.push({
key: book.key,
url: book.url,
path: book.path,
deleted: book.deleted,
touchTime,
loadTime,
desc: {
@@ -287,14 +407,25 @@ class RecentBooksPage {
textLen,
},
readPart,
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
sameBookKey: book.sameBookKey,
active: (activeBook.key == book.key),
activeParent: false,
inGroup: false,
coverPageUrl: book.coverPageUrl,
showCheckBuc: !this.showArchive && utils.hasProp(book, 'downloadSize') && book.url.indexOf('disk://') !== 0,
checkBuc: book.checkBuc,
needBookUpdate: (
!this.showArchive
&& book.checkBuc
&& book.bucSize
&& utils.hasProp(book, 'downloadSize')
&& book.bucSize !== book.downloadSize
&& (book.bucSize - book.downloadSize >= this.bucSizeDiff)
),
bucSize: book.bucSize,
downloadSize: book.downloadSize,
//для сортировки
loadTimeRaw,
@@ -313,12 +444,15 @@ class RecentBooksPage {
//фильтрация
const search = this.search;
if (search) {
const lowerSearch = search.toLowerCase();
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.loadTime.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
return !search
|| item.touchTime.includes(search)
|| item.loadTime.includes(search)
|| item.desc.title.toLowerCase().includes(lowerSearch)
|| item.desc.author.toLowerCase().includes(lowerSearch)
;
});
}
@@ -351,6 +485,7 @@ class RecentBooksPage {
}
//группировка
let nbuCount = 0;
const groups = {};
const parents = {};
let newResult = [];
@@ -367,13 +502,20 @@ class RecentBooksPage {
if (book.active)
parents[book.sameBookKey].activeParent = true;
book.showCheckBuc = false;
book.needBookUpdate = false;
groups[book.sameBookKey].push(book);
}
} else {
newResult.push(book);
}
if (book.needBookUpdate)
nbuCount++;
}
result = newResult;
this.needBookUpdateCount = nbuCount;
//showSameBook
if (this.showSameBook) {
@@ -390,6 +532,11 @@ class RecentBooksPage {
result = newResult;
}
//showNeedBookUpdateOnly
if (this.showNeedBookUpdateOnly) {
result = result.filter(item => item.needBookUpdate);
}
//другие стадии
//.....
@@ -407,7 +554,9 @@ class RecentBooksPage {
wordEnding(num, type = 0) {
const endings = [
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й'],
['о', '', 'о', 'о', 'о', 'о', 'о', 'о', 'о', 'о'],
['ий', 'ие', 'ия', 'ия', 'ия', 'ий', 'ий', 'ий', 'ий', 'ий']
];
const deci = num % 100;
if (deci > 10 && deci < 20) {
@@ -419,7 +568,7 @@ class RecentBooksPage {
get header() {
const len = (this.tableData ? this.tableData.length : 0);
return `${(this.search ? 'Найдено' : 'Всего')} ${len} файл${this.wordEnding(len)}`;
return `${(this.search || this.showNeedBookUpdateOnly ? `Найден${this.wordEnding(len, 2)}` : 'Всего')} ${len} файл${this.wordEnding(len)}${this.showArchive ? ' в архиве' : ''}`;
}
async downloadBook(fb2path, fullTitle) {
@@ -444,16 +593,53 @@ class RecentBooksPage {
}
}
async handleDel(key) {
await bookManager.delRecentBook({key});
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
async handleDel(item) {
if (item.group) {
const keys = [{key: item.key}];
for (const book of item.group)
keys.push({key: book.key});
if (!bookManager.mostRecentBook())
this.close();
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 {
if (!this.showArchive) {
await bookManager.delRecentBooks([{key: item.key}]);
this.$root.notify.info('Книга перенесена в архив');
} else {
if (await this.$root.stdDialog.confirm('Подтвердите удаление книги из архива:', ' ')) {
await bookManager.delRecentBooks([{key: item.key}], 2);
this.$root.notify.info('Книга удалено безвозвратно');
}
}
}
}
loadBook(row) {
this.$emit('load-book', {url: row.url, path: row.path});
async handleRestore(item) {
if (item.group) {
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) {
if (item.deleted)
await this.handleRestore(item.key);
this.$emit('load-book', {url: item.url, path: item.path, force});
this.close();
}
@@ -510,6 +696,8 @@ class RecentBooksPage {
}
async scrollToActiveBook() {
await this.$nextTick();
this.lockScroll = true;
try {
let activeIndex = -1;
@@ -555,6 +743,16 @@ class RecentBooksPage {
}
}
async scrollRefresh() {
this.lockScroll = true;
await utils.sleep(100);
try {
this.$refs.virtualScroll.refresh();
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
get sortMethodOptions() {
return [
@@ -569,6 +767,13 @@ class RecentBooksPage {
];
}
showArchiveToggle() {
this.showArchive = !this.showArchive;
this.showNeedBookUpdateOnly = false;
this.updateTableData();
}
close() {
this.$emit('recent-books-close');
}
@@ -579,6 +784,80 @@ class RecentBooksPage {
}
return true;
}
makeCoverHtml(data) {
return `<img src="${data}" style="height: 100%; width: 100%; object-fit: contain" />`;
}
isLoadedCover(coverPageUrl) {
if (!coverPageUrl)
return false;
let loadedCover = this.covers[coverPageUrl];
if (loadedCover == 'error')
return false;
if (!loadedCover) {
(async() => {
if (this.coversLoadFunc[coverPageUrl])
return;
this.coversLoadFunc[coverPageUrl] = (async() => {
//сначала заглянем в storage
let data = await coversStorage.getData(coverPageUrl);
if (data) {
this.covers[coverPageUrl] = this.makeCoverHtml(data);
} else {//иначе идем на сервер
try {
data = await readerApi.getUploadedFileBuf(coverPageUrl);
await coversStorage.setData(coverPageUrl, data);
this.covers[coverPageUrl] = this.makeCoverHtml(data);
} catch (e) {
console.error(e);
this.covers[coverPageUrl] = 'error';
}
}
});
try {
await this.coversLoadFunc[coverPageUrl]();
} finally {
this.coversLoadFunc[coverPageUrl] = null;
}
})();
}
return (loadedCover != undefined);
}
getCoverHtml(coverPageUrl) {
if (coverPageUrl && this.covers[coverPageUrl])
return this.covers[coverPageUrl];
else
return '';
}
async checkBucChange(item) {
const book = await bookManager.getRecentBook(item);
if (book) {
await bookManager.setCheckBuc(book, item.checkBuc);
this.$root.notify.info(item.checkBuc
? 'Проверка обновлений книги включена'
: 'Проверка обновлений книги отключена'
);
}
}
showNeedBookUpdateOnlyToggle() {
this.showNeedBookUpdateOnly = !this.showNeedBookUpdateOnly;
this.showArchive = false;
this.updateTableData();
}
}
export default vueComponent(RecentBooksPage);
@@ -597,17 +876,16 @@ export default vueComponent(RecentBooksPage);
position: sticky;
z-index: 1;
top: 0;
border-bottom: 2px solid #aaaaaa;
border-bottom: 2px solid var(--bg-menu-color2);
padding-left: 5px;
}
.table-row {
min-height: 80px;
border-bottom: 1px solid #cccccc;
}
.row-part {
padding: 4px 4px 4px 4px;
padding: 4px 0px 4px 0px;
}
.clickable {
@@ -615,22 +893,21 @@ export default vueComponent(RecentBooksPage);
}
.break-word {
line-height: 180%;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: normal;
}
.even {
background-color: #f2f2f2;
background-color: var(--bg-menu-color1);
}
.active-book {
background-color: #b0f0b0 !important;
background-color: var(--bg-selected-item-color1) !important;
}
.active-parent-book {
background-color: #ffbbbb !important;
background-color: var(--bg-selected-item-color2) !important;
}
.icon {
@@ -647,21 +924,20 @@ export default vueComponent(RecentBooksPage);
min-height: 30px;
height: 30px;
margin: 10px 6px 0px 3px;
background-color: white;
}
.row-info-bottom {
line-height: 110%;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
height: 12px;
height: 14px;
}
.row-info-top {
line-height: 110%;
border: 1px solid #cccccc;
border-right: 0;
height: 12px;
height: 14px;
}
.time-info, .row-info-top {
@@ -670,7 +946,64 @@ export default vueComponent(RecentBooksPage);
.read-bar {
height: 6px;
background-color: #bbbbbb;
margin-bottom: 2px;
background-color: #b8b8b8;
}
.del-button {
width: 25px;
height: 20px;
position: absolute;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
border-radius: 0 0 0 10px;
margin: 1px;
}
.del-button:hover {
color: white;
background-color: #FF3030;
}
.restore-button {
width: 25px;
height: 20px;
position: absolute;
border-right: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
border-radius: 0 0 10px 0;
margin: 1px;
}
.restore-button:hover {
color: white;
background-color: #00bb00;
}
.header-button, .header-button-pressed {
width: 80px;
height: 30px;
cursor: pointer;
color: #555555;
}
.header-button-update, .header-button-update-pressed {
width: 120px;
height: 30px;
cursor: pointer;
color: white;
}
.header-button:hover, .header-button-update:hover {
color: white;
background-color: #39902F;
}
.header-button-pressed, .header-button-update-pressed {
color: black;
background-color: yellow;
}
.buc-checkbox {
position: absolute;
}
</style>

View File

@@ -11,6 +11,7 @@
<q-input
ref="input" v-model="needle"
class="col" outlined dense
bg-color="input"
placeholder="Найти"
@keydown="inputKeyDown"
/>
@@ -20,10 +21,10 @@
</div>
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
<q-btn class="button" dense stretch @click="showNext">
<q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
<q-icon style="top: -2px" name="la la-angle-down" dense size="22px" />
</q-btn>
<q-btn class="button" dense stretch @click="showPrev">
<q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
<q-icon name="la la-angle-up" dense size="22px" />
</q-btn>
</q-btn-group>
</div>
@@ -108,10 +109,15 @@ class SearchPage {
this.header = 'Поиск в тексте';
await this.$nextTick();
this.$refs.input.focus();
this.focusInput();
this.$refs.input.select();
}
focusInput() {
if (!this.$root.isMobileDevice)
this.$refs.input.focus();
}
get foundText() {
if (this.foundList.length && this.foundCur >= 0)
return `${this.foundCur + 1}/${this.foundList.length}`;
@@ -149,7 +155,8 @@ class SearchPage {
} else {
this.$emit('stop-text-search');
}
this.$refs.input.focus();
this.focusInput();
}
showPrev() {
@@ -165,7 +172,8 @@ class SearchPage {
} else {
this.$emit('stop-text-search');
}
this.$refs.input.focus();
this.focusInput();
}
close() {

View File

@@ -12,6 +12,7 @@ import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
import LockQueue from '../../../share/LockQueue';
import localForage from 'localforage';
const ssCacheStore = localForage.createInstance({
@@ -21,10 +22,12 @@ const ssCacheStore = localForage.createInstance({
const componentOptions = {
watch: {
serverSyncEnabled: function() {
this.serverSyncEnabledChanged();
if (this.inited)
this.serverSyncEnabledChanged();
},
serverStorageKey: function() {
this.serverStorageKeyChanged(true);
if (this.inited)
this.serverStorageKeyChanged(true);
},
settings: function() {
this.debouncedSaveSettings();
@@ -48,6 +51,9 @@ class ServerStorage {
this.keyInited = false;
this.commit = this.$store.commit;
this.prevServerStorageKey = null;
this.identity = utils.randomHexString(20);
this.lock = new LockQueue(100);
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
this.debouncedSaveSettings = _.debounce(() => {
@@ -81,6 +87,13 @@ class ServerStorage {
if (!this.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) {
//генерируем новый ключ
await this.generateNewServerStorageKey();
@@ -119,6 +132,7 @@ class ServerStorage {
async generateNewServerStorageKey() {
const key = utils.toBase58(utils.randomArray(32));
this.commit('reader/setServerStorageKey', key);
//дождемся serverStorageKeyChanged, событие по watch не работает при this.inited == false
await this.serverStorageKeyChanged(true);
}
@@ -137,6 +151,10 @@ class ServerStorage {
async serverStorageKeyChanged(force) {
if (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.keyInited = true;
@@ -201,6 +219,10 @@ class ServerStorage {
return this.$store.state.reader.libsRev;
}
get offlineModeActive() {
return this.$store.state.reader.offlineModeActive;
}
checkCurrentProfile() {
if (!this.profiles[this.currentProfile]) {
this.commit('reader/setCurrentProfile', '');
@@ -542,14 +564,16 @@ class ServerStorage {
return true;
}
async saveRecent(itemKey, recurse) {
while (!this.inited || this.savingRecent)
async saveRecent(itemKeys, recurse) {
while (!this.inited)
await utils.sleep(100);
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
if (!this.keyInited || !this.serverSyncEnabled)
return;
this.savingRecent = true;
let needRecurseCall = false;
await this.lock.get();
try {
const bm = bookManager;
@@ -559,22 +583,29 @@ class ServerStorage {
//newRecentMod
let newRecentMod = {};
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
let oneItemKey = null;
if (itemKeys && itemKeys.length == 1)
oneItemKey = itemKeys[0];
if (oneItemKey && this.cachedRecentPatch.data[oneItemKey] && this.prevItemKey == oneItemKey) {
newRecentMod = _.cloneDeep(this.cachedRecentMod);
newRecentMod.rev++;
newRecentMod.data.key = itemKey;
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
newRecentMod.data.key = oneItemKey;
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[oneItemKey], bm.recent[oneItemKey]);
needSaveRecentMod = true;
}
this.prevItemKey = itemKey;
this.prevItemKey = oneItemKey;
//newRecentPatch
let newRecentPatch = {};
if (itemKey && !needSaveRecentMod) {
if (itemKeys && !needSaveRecentMod) {
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
newRecentPatch.rev++;
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
for (const key of itemKeys) {
newRecentPatch.data[key] = _.cloneDeep(bm.recent[key]);
}
const applyMod = this.cachedRecentMod.data;
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
@@ -587,11 +618,7 @@ class ServerStorage {
//newRecent
let newRecent = {};
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
//ждем весь bm.recent
/*while (!bookManager.loaded)
await utils.sleep(100);*/
if (!itemKeys || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
@@ -625,10 +652,8 @@ class ServerStorage {
if (res)
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse && itemKey) {
this.savingRecent = false;
await this.saveRecent(itemKey, true);
return;
if (!recurse && itemKeys) {
needRecurseCall = true;
}
} else if (result.state == 'success') {
if (needSaveRecent && newRecent.rev)
@@ -637,10 +662,15 @@ class ServerStorage {
await this.setCachedRecentPatch(newRecentPatch);
if (needSaveRecentMod && newRecentMod.rev)
await this.setCachedRecentMod(newRecentMod);
} else {
this.prevItemKey = null;
}
} finally {
this.savingRecent = false;
this.lock.ret();
}
if (needRecurseCall)
await this.saveRecent(itemKeys, true);
}
async storageCheck(items) {
@@ -656,7 +686,7 @@ class ServerStorage {
}
async storageApi(action, items, force) {
const request = {action, items};
const request = {action, identity: this.identity, items};
if (force)
request.force = true;
const encodedRequest = await this.encodeStorageItems(request);

View File

@@ -1,19 +1,21 @@
<template>
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<template #header>
Установить позицию
</template>
<div id="set-position-slider" class="slider q-px-md">
<q-slider
v-model="sliderValue"
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
:max="sliderMax"
label
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
<div class="col column justify-center">
<div id="set-position-slider" class="slider q-px-md column justify-center">
<q-slider
v-model="sliderValue"
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
:max="sliderMax"
label
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
color="primary"
/>
</div>
</div>
</Window>
</template>
@@ -76,8 +78,9 @@ export default vueComponent(SetPositionPage);
<style scoped>
.slider {
margin: 20px;
background-color: #efefef;
margin: 0 20px 0 20px;
height: 35px;
background-color: var(--bg-input-color);
border-radius: 15px;
}
</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">
<!-- header -->
<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 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>
@@ -14,7 +14,7 @@
v-model="search"
class="q-ml-sm col"
outlined dense
bg-color="grey-4"
bg-color="input"
placeholder="Найти"
@click.stop
/>
@@ -73,10 +73,9 @@
<script>
//-----------------------------------------------------------------------------
import vueComponent from '../../../vueComponent.js';
import vueComponent from '../../../../vueComponent.js';
import rstore from '../../../../store/modules/reader';
//import * as utils from '../../share/utils';
import rstore from '../../../../../store/modules/reader';
const componentOptions = {
watch: {
@@ -116,7 +115,7 @@ class UserHotKeys {
}
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 codesIncludeSearch = (action) => {
@@ -235,11 +234,11 @@ export default vueComponent(UserHotKeys);
}
.table-row:nth-child(even) {
background-color: #f7f7f7;
background-color: var(--bg-menu-color1);
}
.table-row:hover {
background-color: #f0f0f0;
background-color: var(--bg-menu-color2);
}
.desc {

View File

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

View File

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

@@ -9,102 +9,47 @@
<q-tabs
ref="tabs"
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"
right-icon="la la-caret-down"
active-color="white"
active-bg-color="primary"
indicator-color="black"
indicator-color="bg-app"
vertical
no-caps
stretch
inline-label
>
<div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
<div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab v-for="item in tabs" :key="item.name" class="tab row items-center" :name="item.name">
<q-icon :name="item.icon" :color="selectedTab == item.name ? 'yellow' : 'teal-7'" size="24px" />
<div class="q-ml-xs" style="font-size: 90%">
{{ item.label }}
</div>
</q-tab>
</q-tabs>
</div>
<div class="col fit">
<!-- Профили --------------------------------------------------------------------->
<div v-if="selectedTab == 'profiles'" class="fit tab-panel">
@@include('./ProfilesTab.inc');
</div>
<ProfilesTab v-if="selectedTab == 'profiles'" :form="form" />
<!-- Вид ------------------------------------------------------------------------->
<div v-if="selectedTab == 'view'" class="fit column">
<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>
<ViewTab v-if="selectedTab == 'view'" :form="form" @tab-event="tabEvent" />
<!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ToolBarTab.inc');
</div>
<ToolBarTab v-if="selectedTab == 'toolbar'" :form="form" />
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column">
@@include('./KeysTab.inc');
</div>
<KeysTab v-if="selectedTab == 'keys'" :form="form" />
<!-- Листание -------------------------------------------------------------------->
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
@@include('./PageMoveTab.inc');
</div>
<PageMoveTab v-if="selectedTab == 'pagemove'" :form="form" />
<!-- Конвертирование ------------------------------------------------------------->
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
@@include('./ConvertTab.inc');
</div>
<ConvertTab v-if="selectedTab == 'convert'" :form="form" />
<!-- Обновление ------------------------------------------------------------------>
<UpdateTab v-if="selectedTab == 'update'" :form="form" />
<!-- Прочее ---------------------------------------------------------------------->
<div v-if="selectedTab == 'others'" class="fit tab-panel">
@@include('./OthersTab.inc');
</div>
<!-- Сброс ----------------------------------------------------------------------->
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
@@include('./ResetTab.inc');
</div>
<OthersTab v-if="selectedTab == 'others'" :form="form" />
<!-- Сброс ----------------------------------------------------------------------->
<ResetTab v-if="selectedTab == 'reset'" :form="form" @tab-event="tabEvent" />
</div>
</div>
</Window>
@@ -112,151 +57,86 @@
<script>
//-----------------------------------------------------------------------------
import { ref, watch } from 'vue';
import vueComponent from '../../vueComponent.js';
import { reactive } from 'vue';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import * as cryptoUtils from '../../../share/cryptoUtils';
//stuff
import Window from '../../share/Window.vue';
import NumInput from '../../share/NumInput.vue';
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
import wallpaperStorage from '../share/wallpaperStorage';
import rstore from '../../../store/modules/reader';
import defPalette from './defPalette';
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
//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 = {
components: {
Window,
NumInput,
UserHotKeys,
},
data: function() {
return Object.assign({}, rstore.settingDefaults);
//pages
ProfilesTab,
ViewTab,
ToolBarTab,
KeysTab,
PageMoveTab,
ConvertTab,
UpdateTab,
OthersTab,
ResetTab,
},
watch: {
settings: function() {
this.settingsChanged();
this.settingsChanged();//no await
},
form: function(newValue) {
if (this.inited) {
this.commit('reader/setSettings', _.cloneDeep(newValue));
}
},
fontBold: function(newValue) {
this.fontWeight = (newValue ? 'bold' : '');
},
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;
form: {
handler() {
if (this.inited && !this.isSetsChanged) {
this.debouncedCommitSettings();
}
},
deep: true,
},
},
};
class SettingsPage {
_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';
selectedViewTab = 'mode';
selectedKeysTab = 'mouse';
fontBold = false;
fontItalic = false;
vertShift = 0;
tabsScrollable = false;
textColorFiltered = '';
bgColorFiltered = '';
dualDivColorFiltered = '';
webFonts = [];
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;
}
isSetsChanged = false;
created() {
this.commit = this.$store.commit;
this.reader = this.$store.state.reader;
this.form = {};
this.rstore = rstore;
this.toolButtons = rstore.toolButtons;
this.settingsChanged();
this.debouncedCommitSettings = _.debounce(() => {
this.commit('reader/setSettings', _.cloneDeep(this.form));
}, 50);
this.settingsChanged();//no await
}
mounted() {
this.$watch(
'$refs.tabs.scrollable',
(newValue) => {
this.tabsScrollable = newValue && !this.$root.isMobileDevice;
}
);
}
init() {
@@ -264,190 +144,20 @@ class SettingsPage {
this.inited = true;
}
settingsChanged() {
if (_.isEqual(this.form, this.settings))
return;
this.form = Object.assign({}, this.settings);
for (const prop in rstore.settingDefaults) {
this[prop] = _.cloneDeep(this.form[prop]);
async settingsChanged() {
this.isSetsChanged = true;
try {
this.form = reactive(_.cloneDeep(this.settings));
} finally {
await this.$nextTick();
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() {
return this.$store.state.reader.settings;
}
get serverSyncEnabled() {
return this.$store.state.reader.serverSyncEnabled;
}
set serverSyncEnabled(newValue) {
this.commit('reader/setServerSyncEnabled', newValue);
}
get profiles() {
return this.$store.state.reader.profiles;
}
get currentProfileOptions() {
const profNames = Object.keys(this.profiles)
profNames.sort();
let result = [{label: 'Нет', value: ''}];
profNames.forEach(name => {
result.push({label: name, value: name});
});
return result;
}
get wallpaperOptions() {
let result = [{label: 'Нет', value: ''}];
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() {
this.$emit('do-action', {action: 'settings'});
}
@@ -455,212 +165,20 @@ class SettingsPage {
async setDefaults() {
try {
if (await this.$root.stdDialog.confirm('Подтвердите установку настроек по умолчанию:', ' ')) {
this.form = Object.assign({}, rstore.settingDefaults);
for (let prop in rstore.settingDefaults) {
this[prop] = this.form[prop];
}
this.form = _.cloneDeep(rstore.settingDefaults);
}
} catch (e) {
//
}
}
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)
tabEvent(event) {
if (!event || !event.action)
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 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);
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 = '';
switch (event.action) {
case 'set-defaults': this.setDefaults(); break;
case 'night-mode': this.$emit('do-action', {action: 'nightMode'}); break;
}
}
@@ -680,15 +198,17 @@ export default vueComponent(SettingsPage);
.tab {
justify-content: initial;
}
</style>
.tab-panel {
<style>
.sets-tab-panel {
overflow-x: hidden;
overflow-y: auto;
font-size: 90%;
padding: 0 10px 15px 10px;
}
.part-header {
.sets-part-header {
border-top: 2px solid #bbbbbb;
font-weight: bold;
font-size: 110%;
@@ -696,25 +216,7 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px;
}
.item {
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 {
.sets-label {
display: flex;
flex-direction: column;
justify-content: center;
@@ -723,33 +225,14 @@ export default vueComponent(SettingsPage);
overflow: hidden;
}
.text {
font-size: 90%;
line-height: 130%;
.sets-item {
width: 100%;
margin-top: 5px;
margin-bottom: 5px;
}
.button {
.sets-button {
margin: 3px 15px 3px 0;
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>

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

@@ -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,116 +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>
</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)'
];
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

@@ -81,9 +81,6 @@ const componentOptions = {
settings: function() {
this.debouncedLoadSettings();
},
toggleLayout: function() {
this.updateLayout();
},
inAnimation: function() {
this.updateLayout();
},
@@ -92,7 +89,6 @@ const componentOptions = {
class TextPage {
_options = componentOptions;
toggleLayout = false;
showStatusBar = false;
clickControl = true;
@@ -130,10 +126,6 @@ class TextPage {
this.startClickRepeat(x, y);
}, 800);
this.debouncedPrepareNextPage = _.debounce(() => {
this.prepareNextPage();
}, 100);
this.debouncedDrawStatusBar = _.throttle(() => {
this.drawStatusBar();
}, 60);
@@ -147,17 +139,11 @@ class TextPage {
}, 50);
this.debouncedUpdatePage = _.debounce(async(lines) => {
if (!this.pageChangeAnimation)
this.toggleLayout = !this.toggleLayout;
else {
if (this.pageChangeAnimation) {
this.page2 = this.page1;
this.toggleLayout = true;
}
if (this.toggleLayout)
this.page1 = this.drawHelper.drawPage(lines);
else
this.page2 = this.drawHelper.drawPage(lines);
this.page1 = this.drawHelper.drawPage(lines);
await this.doPageAnimation();
}, 10);
@@ -174,7 +160,12 @@ class TextPage {
}
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})`;
}
@@ -425,7 +416,6 @@ class TextPage {
showBook() {
this.$refs.main.focus();
this.toggleLayout = false;
this.updateLayout();
this.book = null;
this.meta = null;
@@ -443,10 +433,6 @@ class TextPage {
if (this.lastBook) {
(async() => {
try {
//подождем ленивый парсинг
this.stopLazyParse = true;
while (this.doingLazyParse) await utils.sleep(10);
const isParsed = await bookManager.hasBookParsed(this.lastBook);
if (!isParsed) {
return;
@@ -470,8 +456,6 @@ class TextPage {
await this.calcPropsAndLoadFonts();
this.refreshTime();
if (this.lazyParseEnabled)
this.lazyParsePara();
} catch (e) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
@@ -483,12 +467,9 @@ class TextPage {
if (this.inAnimation) {
this.$refs.scrollBox1.style.visibility = 'visible';
this.$refs.scrollBox2.style.visibility = 'visible';
} else if (this.toggleLayout) {
} else {
this.$refs.scrollBox1.style.visibility = 'visible';
this.$refs.scrollBox2.style.visibility = 'hidden';
} else {
this.$refs.scrollBox1.style.visibility = 'hidden';
this.$refs.scrollBox2.style.visibility = 'visible';
}
}
@@ -589,28 +570,25 @@ class TextPage {
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.draw();
const page = this.$refs.scrollingPage1;
let i = 0;
while (!this.stopScrolling) {
page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
page.style.transform = `translateY(-${this.lineHeight}px)`;
if (i > 0) {
this.doDown();
await utils.sleep(1);
await this.$nextTick();
if (this.linesDown.length <= this.pageLineCount + 1) {
this.stopScrolling = true;
}
}
page.style.transition = `${this.scrollingDelay}ms ${this.scrollingType}`;
page.style.transform = `translateY(-${this.lineHeight}px)`;
await transitionFinish(this.scrollingDelay);
page.style.transition = '';
page.style.transform = 'none';
page.offsetHeight;
@@ -678,21 +656,11 @@ class TextPage {
return;
}
//fast draw prepared
if (!this.pageChangeAnimation && this.pageChangeDirectionDown && this.pagePrepared && this.bookPos == this.bookPosPrepared) {
this.toggleLayout = !this.toggleLayout;
this.linesDown = this.linesDownNext;
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);
}
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.debouncedDrawPageDividerAndOrnament();
@@ -864,36 +832,6 @@ class TextPage {
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() {
if (!this.timeRefreshing) {
this.timeRefreshing = true;
@@ -907,30 +845,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() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true;
@@ -1117,6 +1031,7 @@ class TextPage {
if (this.startTouch) {
const dy = this.startTouch.y - y;
const dx = this.startTouch.x - x;
this.startTouch = null;
const moveDelta = 30;
const touchDelta = 15;
if (dy > 0 && Math.abs(dy) >= moveDelta && Math.abs(dy) > Math.abs(dx)) {
@@ -1132,10 +1047,23 @@ class TextPage {
//движение вправо
this.doScrollingSpeedUp();
} 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;
})();
}
}
}
}

View File

@@ -3,6 +3,7 @@ import sax from '../../../../server/core/sax';
import * as utils from '../../../share/utils';
const maxImageLineCount = 100;
const maxParaLength = 10000;
const maxParaTextLength = 10000;
// defaults
@@ -84,6 +85,7 @@ export default class BookParser {
let binaryId = '';
let binaryType = '';
let dimPromises = [];
this.coverPageId = '';
//оглавление
this.contents = [];
@@ -228,6 +230,7 @@ export default class BookParser {
};
const growParagraph = (text, len, textRaw) => {
//начальный параграф
if (paraIndex < 0) {
newParagraph();
growParagraph(text, len);
@@ -253,6 +256,14 @@ export default class BookParser {
}
const p = para[paraIndex];
//ограничение на размер параграфа
if (p.length > maxParaLength) {
newParagraph();
growParagraph(text, len);
return;
}
p.length += len;
p.text += text;
paraOffset += len;
@@ -279,7 +290,7 @@ export default class BookParser {
const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
if (href[0] == '#') {//local
if (local) {//local
imageNum++;
if (inPara && !this.sets.showInlineImagesInCenter && !center)
@@ -291,6 +302,11 @@ export default class BookParser {
if (inPara && this.sets.showInlineImagesInCenter)
newParagraph();
//coverpage
if (path == '/fictionbook/description/title-info/coverpage/image') {
this.coverPageId = id;
}
} else {//external
imageNum++;
@@ -422,61 +438,63 @@ export default class BookParser {
};
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
if (tag == elemName) {
if (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
tag = elemName;
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0);
}
if (tag == 'p') {
inPara = false;
}
if (tag == 'subtitle') {
isFirstTitlePara = false;
bold = false;
center = false;
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 (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
path = path.substr(0, path.length - tag.length - 1);
let i = path.lastIndexOf('/');
if (i >= 0) {
tag = path.substr(i + 1);
} else {
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0);
}
if (tag == 'p') {
inPara = false;
}
if (tag == 'subtitle') {
isFirstTitlePara = false;
bold = false;
center = false;
inSubtitle = false;
}
if (tag == 'epigraph' || tag == 'annotation') {
italic = false;
space -= 1;
newParagraph();
}
if (tag == 'stanza') {
newParagraph();
}
if (tag == 'text-author') {
bold = false;
space -= 1;
}
}
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;
}
}
};

View File

@@ -2,8 +2,10 @@ import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
import readerApi from '../../../api/reader';
import coversStorage from './coversStorage';
import * as utils from '../../../share/utils';
const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
@@ -232,6 +234,10 @@ class BookManager {
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path};
if (newBook.downloadSize !== undefined && newBook.downloadSize >= 0)
meta.downloadSize = newBook.downloadSize;
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
@@ -345,9 +351,38 @@ class BookManager {
const parsed = new BookParser(this.settings);
const parsedMeta = await parsed.parse(data, callback);
//cover page
let coverPageUrl = '';
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
const bin = parsed.binary[parsed.coverPageId];
let dataUrl = `data:${bin.type};base64,${bin.data}`;
try {
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
} catch (e) {
console.error(e);
}
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
//далее асинхронно
(async() => {
//отправим dataUrl на сервер в /upload
try {
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
} catch (e) {
console.error(e);
}
//сохраним в storage
await coversStorage.setData(coverPageUrl, dataUrl);
})();
}
const result = Object.assign({}, meta, parsedMeta, {
length: data.length,
textLength: parsed.textLength,
coverPageUrl,
parsed
});
@@ -432,10 +467,10 @@ class BookManager {
async getRecentBook(value) {
return this.recent[value.key];
}
async delRecentBook(value) {
/*
async delRecentBook(value, delFlag = 1) {
const item = this.recent[value.key];
item.deleted = 1;
item.deleted = delFlag;
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
@@ -444,6 +479,62 @@ class BookManager {
await this.recentSetItem(item);
this.emit('recent-deleted', value.key);
}
*/
async delRecentBooks(values, delFlag = 1) {
for (const value of values) {
const item = this.recent[value.key];
item.deleted = delFlag;
if (this.recentLastKey == value.key) {
await this.recentSetLastKey(null);
}
await this.recentSetItem(item);
}
this.emit('recent-deleted');
}
/*
async restoreRecentBook(value) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
*/
async restoreRecentBooks(values) {
for (const value of values) {
const item = this.recent[value.key];
item.deleted = 0;
await this.recentSetItem(item);
}
}
async setCheckBuc(value, checkBuc) {
const item = this.recent[value.key];
const updateItems = [];
if (item) {
if (item.sameBookKey !== undefined) {
const sorted = this.getSortedRecent();
for (const book of sorted) {
if (!book.deleted && book.sameBookKey === item.sameBookKey)
updateItems.push(book);
}
} else {
updateItems.push(item);
}
}
const now = Date.now();
for (const book of updateItems) {
book.checkBuc = checkBuc;
if (checkBuc)
book.checkBucTime = now;
await this.recentSetItem(book);
}
}
async cleanRecentBooks() {
const sorted = this.getSortedRecent();

View File

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

View File

@@ -32,6 +32,10 @@ class WallpaperStorage {
this.cachedKeys = await wpStore.keys();
}
async getKeys() {
return await wpStore.keys();
}
keyExists(key) {//не асинхронная
return this.cachedKeys.includes(key);
}

View File

@@ -1,4 +1,134 @@
export const versionHistory = [
{
version: '1.2.0',
releaseDate: '2024-03-25',
showUntil: '2024-03-24',
content:
`
<ul>
<li>в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий</li>
<li>добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параматр networkLibraryLink (#47)</li>
</ul>
`
},
{
version: '1.1.3',
releaseDate: '2023-02-06',
showUntil: '2023-02-05',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.1.2',
releaseDate: '2023-01-22',
showUntil: '2023-01-21',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.1.1',
releaseDate: '2023-01-11',
showUntil: '2023-01-15',
content:
`
<ul>
<li>добавлена опция "Ночной режим" и кнопка на панель</li>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.0.0',
releaseDate: '2022-12-18',
showUntil: '2022-12-25',
content:
`
<ul>
<li>на мобильных устройствах переход в полноэкранный режим теперь возможен через двойной тап по центру</li>
<li>добавлено окно "Сетевая библиотека" для omnireader.ru</li>
<li>улучшена работа синхронизации с сервером при плохом качестве связи</li>
<li>добавлена сборка релизов читалки: <a href="https://github.com/bookpauk/liberama/releases" target="_blank">https://github.com/bookpauk/liberama/releases</a></li>
</ul>
`
},
{
version: '0.12.2',
releaseDate: '2022-09-04',
showUntil: '2022-09-11',
content:
`
<ul>
<li>исправлен баг с формой для доната, показывалась каждый день, а не каждый месяц</li>
<li>автор приносит извинения за доставленные неудобства</li>
</ul>
`
},
{
version: '0.12.1',
releaseDate: '2022-09-01',
showUntil: '2022-08-30',
content:
`
<ul>
<li>добавлена форма для доната</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.12.0',
releaseDate: '2022-07-27',
showUntil: '2022-08-03',
content:
`
<ul>
<li>запущен сервер проверки обновлений книг:</li>
<ul>
<li>проверка обновления той или иной книги настраивается в списке загруженных (чекбокс)</li>
<li>для того, чтобы чекбокс появился у ранее загруженной, необходимо принудительно обновить книгу</li>
<li>в настройках можно указать разницу размеров, при которой требуется делать уведомление</li>
</ul>
</ul>
`
},
{
version: '0.11.8',
releaseDate: '2022-07-14',
showUntil: '2022-07-13',
content:
`
<ul>
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
<li>добавлена синхронизация обоев</li>
</ul>
`
},
{
version: '0.11.7',
releaseDate: '2022-07-12',

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>
<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="caption col row items-center q-ml-md">
<slot name="header"></slot>

View File

@@ -29,7 +29,7 @@ class Notify {
html: true,
message:
`<div style="max-width: 350px;">
`<div style="max-width: 350px">
${caption}
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
</div>`

View File

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

View File

@@ -3,7 +3,7 @@
<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="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>
@@ -28,7 +28,7 @@
</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="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>
@@ -56,7 +56,7 @@
</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="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>
@@ -84,7 +84,7 @@
</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="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>
@@ -116,7 +116,7 @@
</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="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>

View File

@@ -10,7 +10,9 @@
@touchend.stop="onTouchEnd"
@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>
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
</div>
@@ -146,14 +148,14 @@ export default vueComponent(Window);
.window {
margin: 10px;
background-color: #ffffff;
border: 3px double black;
background-color: var(--bg-app-color);
border: 3px double var(--text-app-color);
border-radius: 4px;
box-shadow: 3px 3px 5px black;
}
.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;
height: 30px;
}

View File

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

View File

@@ -1,41 +1,16 @@
import { createRouter, createWebHashHistory } from 'vue-router';
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';
const Reader = () => import('./components/Reader/Reader.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 = [
['/', null, null, '/cardindex'],
['/cardindex', CardIndex],
['/cardindex~search', Search],
['/cardindex~card', Card],
['/cardindex~card/:authorId', Card],
['/cardindex~book', Book],
['/cardindex~book/:bookId', Book],
['/cardindex~history', History],
['/', null, null, '/reader'],
['/reader', Reader],
['/external-libs', ExternalLibs],
['/income', Income],
['/sources', Sources],
['/settings', Settings],
['/help', Help],
['/404', NotFound404],
['/:pathMatch(.*)*', null, null, '/cardindex'],
['/:pathMatch(.*)*', null, null, '/reader'],
];
let routes = {};

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import dayjs from 'dayjs';
import baseX from 'base-x';
import PAKO from 'pako';
import {Buffer} from 'safe-buffer';
@@ -35,22 +36,6 @@ export function randomHexString(len) {
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 'noDate':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
}
}
export function fallbackCopyTextToClipboard(text) {
let textArea = document.createElement('textarea');
textArea.value = text;
@@ -363,4 +348,58 @@ export function getBookTitle(fb2) {
]).join(' - ');
return result;
}
}
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
return new Promise ((resolve, reject) => { (async() => {
const img = new Image();
let resolved = false;
img.onload = () => {
try {
let width = img.width;
let height = img.height;
if (width > height) {
if (width > toWidth) {
height = height * (toWidth / width);
width = toWidth;
}
} else {
if (height > toHeight) {
width = width * (toHeight / height);
height = toHeight;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
const result = canvas.toDataURL('image/jpeg', quality);
resolved = true;
resolve(result);
} catch (e) {
reject(e);
return;
}
};
img.onerror = reject;
img.src = dataUrl;
await sleep(1000);
if (!resolved)
reject('Не удалось изменить размер');
})().catch(reject); });
}
export function makeDonation() {
window.open('https://donatty.com/liberama', '_blank');
}
export function dateFormat(date, format = 'DD.MM.YYYY') {
return dayjs(date).format(format);
}

View File

@@ -1,19 +1,20 @@
import { createStore } from 'vuex';
import createPersistedState from 'vuex-persistedstate';
//import createPersistedState from 'vuex-persistedstate';
import VuexPersistence from 'vuex-persist';
import root from './root.js';
import uistate from './modules/uistate';
import config from './modules/config';
import reader from './modules/reader';
const debug = process.env.NODE_ENV !== 'production';
const vuexLocal = new VuexPersistence();
export default createStore(Object.assign({}, root, {
modules: {
uistate,
config,
reader,
},
strict: debug,
plugins: [createPersistedState()]
plugins: [vuexLocal.plugin]
}));

View File

@@ -1,6 +1,11 @@
import _ from 'lodash';
import * as utils from '../../share/utils';
import googleFonts from './fonts/fonts.json';
const minuteMs = 60*1000;//количество ms в минуте
const hourMs = 60*minuteMs;//количество ms в часе
const dayMs = 24*hourMs;//количество ms в сутках
const readerActions = {
'loader': 'На страницу загрузки',
'loadFile': 'Загрузить файл с диска',
@@ -17,6 +22,7 @@ const readerActions = {
'copyText': 'Скопировать текст со страницы',
'convOptions': 'Настроить конвертирование',
'refresh': 'Принудительно обновить книгу',
'nightMode': 'Ночной режим',
'clickControl': 'Управление кликом',
'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки',
@@ -44,17 +50,18 @@ const toolButtons = [
{name: 'undoAction', show: true},
{name: 'redoAction', show: true},
{name: 'fullScreen', show: true},
{name: 'scrolling', show: false},
{name: 'scrolling', show: true},
{name: 'setPosition', show: true},
{name: 'search', show: true},
{name: 'copyText', show: false},
{name: 'copyText', show: true},
{name: 'convOptions', show: true},
{name: 'refresh', show: true},
{name: 'contents', show: true},
{name: 'libs', show: true},
{name: 'recentBooks', show: true},
{name: 'clickControl', show: false},
{name: 'offlineMode', show: false},
{name: 'nightMode', show: true},
{name: 'clickControl', show: true},
{name: 'offlineMode', show: true},
];
//readerActions[name]
@@ -76,6 +83,7 @@ const hotKeys = [
{name: 'contents', codes: ['C']},
{name: 'libs', codes: ['L']},
{name: 'recentBooks', codes: ['X']},
{name: 'nightMode', codes: ['Equal']},
{name: 'clickControl', codes: ['Ctrl+B']},
{name: 'offlineMode', codes: ['O']},
@@ -153,6 +161,10 @@ const settingDefaults = {
statusBarColorAlpha: 0.4,
statusBarClickOpen: true,
nightMode: false, //ночной режим
dayColorSets: {},
nightColorSets: {},
scrollingDelay: 3000,// замедление, ms
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
@@ -160,7 +172,6 @@ const settingDefaults = {
pageChangeAnimationSpeed: 80, //0-100%
allowUrlParamBookPos: false,
lazyParseEnabled: false,
copyFullText: false,
showClickMapPage: true,
clickControl: true,
@@ -180,17 +191,25 @@ const settingDefaults = {
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
showDonationDialog: true,
showNeedUpdateNotify: true,
fontShifts: {},
showToolButton: {},
toolBarHideOnScroll: true,
toolBarHideOnScroll: false,
toolBarMultiLine: true,
userHotKeys: {},
userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
//Book Update Checker
bucEnabled: true, // общее включение/выключение проверки обновлений
bucSizeDiff: 1, // разница в размерах файла, при которой показывать наличие обновления
bucSetOnNew: true, // автоматически включать проверку обновлений для вновь загружаемых файлов
bucCancelEnabled: true, // вкл/выкл отмену проверки книг через bucCancelDays
bucCancelDays: 90, // количество дней, через которое отменяется проверка книги, при условии отсутствия обновлений за это время
};
for (const font of fonts)
@@ -206,6 +225,8 @@ const diffExclude = [];
for (const hotKey of hotKeys)
diffExclude.push(`userHotKeys/${hotKey.name}`);
diffExclude.push('userWallpapers');
diffExclude.push('dayColorSets');
diffExclude.push('nightColorSets');
function addDefaultsToSettings(settings) {
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
@@ -216,45 +237,95 @@ function addDefaultsToSettings(settings) {
return false;
}
const libsDefaults = {
startLink: 'http://flibusta.is',
comment: 'Флибуста | Книжное братство',
closeAfterSubmit: false,
openInFrameOnEnter: false,
openInFrameOnAdd: false,
groups: [
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
]},
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
]},
{r: 'http://samlib.ru', s: 'http://samlib.ru', list: [
{l: 'http://samlib.ru', c: 'Журнал "Самиздат"'},
]},
{r: 'http://lib.ru', s: 'http://lib.ru', list: [
{l: 'http://lib.ru', c: 'Библиотека Максима Мошкова'},
]},
{r: 'https://aldebaran.ru', s: 'https://aldebaran.ru', list: [
{l: 'https://aldebaran.ru', c: 'АЛЬДЕБАРАН | Электронная библиотека книг'},
]},
]
};
const colorSetsList = [
'textColor',
'backgroundColor',
'wallpaper',
'statusBarColorAsText',
'statusBarColor',
'statusBarColorAlpha',
'dualDivColorAsText',
'dualDivColor',
'dualDivColorAlpha',
];
function saveColorSets(nightMode, settings) {
const target = (nightMode ? settings.nightColorSets : settings.dayColorSets);
for (const prop of colorSetsList) {
target[prop] = settings[prop];
}
}
function restoreColorSets(nightMode, settings) {
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
const state = {
toolBarActive: true,
offlineModeActive: false,
serverSyncEnabled: false,
serverStorageKey: '',
profiles: {},
profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '',
donationRemindDate: '',
donationNextPopup: Date.now() + dayMs*30,
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settings: _.cloneDeep(settingDefaults),
settingsRev: {},
libs: Object.assign({}, libsDefaults),
libs: {},
libsRev: 0,
};
@@ -269,6 +340,9 @@ const mutations = {
setToolBarActive(state, value) {
state.toolBarActive = value;
},
setOfflineModeActive(state, value) {
state.offlineModeActive = value;
},
setServerSyncEnabled(state, value) {
state.serverSyncEnabled = value;
},
@@ -287,20 +361,38 @@ const mutations = {
setWhatsNewContentHash(state, value) {
state.whatsNewContentHash = value;
},
setDonationRemindDate(state, value) {
state.donationRemindDate = value;
setDonationNextPopup(state, value) {
state.donationNextPopup = value;
},
setCurrentProfile(state, value) {
state.currentProfile = value;
},
setSettings(state, value) {
const newSettings = Object.assign({}, state.settings, value);
let newSettings = Object.assign({}, state.settings, value);
//при смене профиля подгружаются старые настройки, могут отсутствовать атрибуты
//поэтому:
const added = addDefaultsToSettings(newSettings);
if (added) {
state.settings = added;
} else {
state.settings = newSettings;
if (added)
newSettings = added;
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) {
state.settingsRev = Object.assign({}, state.settingsRev, value);
@@ -314,6 +406,10 @@ const mutations = {
};
export default {
minuteMs,
hourMs,
dayMs,
readerActions,
toolButtons,
hotKeys,
@@ -321,7 +417,7 @@ export default {
webFonts,
settingDefaults,
addDefaultsToSettings,
libsDefaults,
getLibsDefaults,
namespaced: true,
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

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.liberama.top;
set $liberama http://127.0.0.1:34082;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -15,15 +16,20 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
@@ -32,6 +38,11 @@ server {
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)$ {
@@ -50,6 +61,7 @@ server {
server {
listen 80;
server_name b.beta.liberama.top;
set $liberama http://127.0.0.1:34082;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -59,24 +71,38 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34082;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34082;
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/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
root /home/beta.liberama/.liberama/public;
location ~* \.(?:manifest|appcache|html)$ {
expires -1;

View File

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -15,24 +16,38 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34081;
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/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
root /home/beta.liberama/.liberama/public;
location ~* \.(?:manifest|appcache|html)$ {
expires -1;
@@ -46,3 +61,55 @@ server {
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,7 @@
server {
listen 80;
server_name beta.omnireader.ru;
server_name beta.omnireader.ru b.beta.omnireader.ru;
set $liberama http://127.0.0.1:34081;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -10,24 +11,38 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:34081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:34081;
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/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
root /home/beta.liberama/.liberama/public;
location ~* \.(?:manifest|appcache|html)$ {
expires -1;

View File

@@ -17,6 +17,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name liberama.top;
set $liberama http://127.0.0.1:55081;
client_max_body_size 100m;
proxy_read_timeout 1h;
@@ -26,25 +27,38 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
root /home/liberama/public;
location /tmp {
root /home/liberama/.liberama/public-files;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
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)$ {
expires -1;
@@ -62,6 +76,7 @@ server {
server {
listen 80;
server_name b.liberama.top;
set $liberama http://127.0.0.1:55081;
client_max_body_size 100m;
proxy_read_timeout 1h;
@@ -71,24 +86,38 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:55081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:55081;
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/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/public;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
root /home/liberama/.liberama/public;
location ~* \.(?:manifest|appcache|html)$ {
expires -1;

View File

@@ -3,14 +3,14 @@
### git, clone
```
cd ~
sudo apt install ssh git
sudo apt install ssh git zip
git clone https://github.com/bookpauk/liberama
```
### node.js
```
sudo apt install -y curl
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs
```
@@ -18,6 +18,7 @@ sudo apt install -y nodejs
```
cd liberama
npm i
cd docs/omnireader.ru
```
### create public dir
@@ -30,8 +31,8 @@ sudo chown www-data.www-data /home/liberama
#### download from https://download.calibre-ebook.com/
```
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 tar xvf calibre-5.29.0-x86_64.txz -C /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/.liberama/calibre
```
### external converters
@@ -44,7 +45,7 @@ sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graph
Сначала настроим для HTTP:
```
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 rm /etc/nginx/sites-enabled/default
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
После установки сертификата, можно использовать конфиг для 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
```
@@ -68,7 +69,7 @@ sudo service php7.4-fpm restart
sudo mkdir /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
./deploy.sh
./start_server.sh

View File

@@ -6,6 +6,7 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 100m;
proxy_read_timeout 1h;
@@ -15,25 +16,89 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
location / {
root /home/liberama/public;
location /tmp {
root /home/liberama/.liberama/public-files;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
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)$ {
expires -1;
}
}
}
server {
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)$ {
expires -1;

View File

@@ -1,6 +1,7 @@
server {
listen 80;
server_name omnireader.ru;
server_name omnireader.ru b.omnireader.ru;
set $liberama http://127.0.0.1:44081;
client_max_body_size 50m;
proxy_read_timeout 1h;
@@ -10,24 +11,37 @@ server {
gzip_proxied expired no-cache no-store private auth;
gzip_types *;
location @liberama {
proxy_pass $liberama;
}
location /api {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
}
location /ws {
proxy_pass http://127.0.0.1:44081;
proxy_pass $liberama;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
root /home/liberama/public;
location /tmp {
root /home/liberama/.liberama/public-files;
location /tmp {
types { } default_type "application/xml; charset=utf-8";
add_header Content-Encoding gzip;
}
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)$ {
expires -1;

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