Compare commits

...

79 Commits

Author SHA1 Message Date
Book Pauk
8c9fd7678d Merge branch 'release/1.2.8' 2025-06-04 09:28:35 +07:00
Book Pauk
01313d66b2 Версия 1.2.8 2025-06-04 09:28:15 +07:00
Book Pauk
eaeacbfb1b Улучшено форматирование текста при копировании из окна 2025-06-04 09:23:13 +07:00
Book Pauk
5328998c21 Merge tag '1.2.7' into develop
1.2.7
2025-02-22 14:44:22 +07:00
Book Pauk
ee066c7c4b Merge branch 'release/1.2.7' 2025-02-22 14:44:18 +07:00
Book Pauk
130aebb514 Версия 1.2.7 2025-02-22 14:43:18 +07:00
Book Pauk
dbec1e630e Отключена форма для сбора донатов 2025-02-22 14:39:29 +07:00
Book Pauk
583b966616 Мелкая оптимизация, чтобы не отдавал большой конфиг каждый раз при обновлении страницы 2025-02-22 14:31:19 +07:00
Book Pauk
9e509ac845 Обновление caniuse-lite 2025-02-22 13:49:57 +07:00
Book Pauk
4ea2d8918e Merge tag '1.2.6' into develop
1.2.6
2024-10-03 15:43:48 +07:00
Book Pauk
6667688193 Merge branch 'release/1.2.6' 2024-10-03 15:43:44 +07:00
Book Pauk
30a1629f23 Исправления из-за нарушения авторских прав 2024-10-03 15:38:16 +07:00
Book Pauk
ba50faeebb Merge tag '1.2.5' into develop
1.2.5
2024-10-03 11:51:40 +07:00
Book Pauk
3c0d784e3d Merge branch 'release/1.2.5' 2024-10-03 11:51:36 +07:00
Book Pauk
3e75310e1f Исправления из-за нарушения авторских прав 2024-10-03 11:51:09 +07:00
Book Pauk
2b01d6d8d7 Merge tag '1.2.4' into develop
1.2.4
2024-08-27 12:59:44 +07:00
Book Pauk
be6d60d7a9 Merge branch 'release/1.2.4' 2024-08-27 12:59:41 +07:00
Book Pauk
3c0815d55b 1.2.4 2024-08-27 12:59:28 +07:00
Book Pauk
abd8584cb8 1.2.4 2024-08-27 12:59:20 +07:00
Book Pauk
5a910f80b3 Поправлена реакция на клик в строке статуса в режиме clickControl 2024-08-27 12:58:07 +07:00
Book Pauk
67bdfd853e Merge tag '1.2.3' into develop
1.2.3
2024-08-02 15:22:27 +07:00
Book Pauk
fc8e986acb Merge branch 'release/1.2.3' 2024-08-02 15:22:24 +07:00
Book Pauk
64539785c2 1.2.3 2024-08-02 15:22:07 +07:00
Book Pauk
f530455146 Версия 1.2.3 2024-08-02 15:21:43 +07:00
Book Pauk
70dc66e1ae Исправление мелких багов при прокрутке 2024-08-02 15:15:54 +07:00
Book Pauk
3e5894d9e0 Исправление багов 2024-07-31 11:44:07 +07:00
Book Pauk
d7ac9d1bfc Улучшение отображения примечаний 2024-07-31 11:30:31 +07:00
Book Pauk
5160c5fb75 Мелкая поправка текста 2024-07-30 21:29:02 +07:00
Book Pauk
d9c7964410 Поправки багов 2024-07-30 21:28:27 +07:00
Book Pauk
110952b4c4 К предыдущему 2024-07-30 18:41:21 +07:00
Book Pauk
ece17dc0dd Улучшение отображения сносок 2024-07-30 18:23:52 +07:00
Book Pauk
35e1087531 Merge tag '1.2.2' into develop
1.2.2
2024-07-28 20:22:59 +07:00
Book Pauk
59c4b62770 Merge branch 'release/1.2.2' 2024-07-28 20:22:54 +07:00
Book Pauk
4be9ce5ff3 Версия 1.2.2 2024-07-28 20:22:33 +07:00
Book Pauk
92a811cabd Поправки парсинга примечаний 2024-07-28 20:20:45 +07:00
Book Pauk
897cdc8ac7 Исправление парсинга примечаний 2024-07-28 20:13:35 +07:00
Book Pauk
418ff482ae Merge tag '1.2.1' into develop
1.2.1
2024-07-28 17:55:11 +07:00
Book Pauk
8858d6d1f2 Merge branch 'release/1.2.1' 2024-07-28 17:55:05 +07:00
Book Pauk
41f8a28631 Версия 1.2.1 2024-07-28 17:52:16 +07:00
Book Pauk
da0771d5e5 Мелкая поправка разметки 2024-07-28 17:47:15 +07:00
Book Pauk
c03995367a Поправки багов 2024-07-28 17:45:18 +07:00
Book Pauk
0430105061 Добавлено отображение примечаний на месте, по клику на примечании (#50) 2024-07-28 17:23:16 +07:00
Book Pauk
afd4d02dad Улучшение BUCServer 2024-07-26 17:19:45 +07:00
Book Pauk
d634ebf14c Улучшение BUCServer 2024-07-26 15:54:41 +07:00
Book Pauk
613230256a Небольшой тюнинг BUCServer 2024-07-26 00:49:39 +07:00
Book Pauk
2da1736c99 Поправка для игнорирования невалидных сертификатов 2024-07-25 18:10:13 +07:00
Book Pauk
1914092520 npx update-browserslist-db@latest 2024-07-25 16:51:44 +07:00
Book Pauk
4a6f93a14f edit 2024-03-25 13:02:13 +07:00
Book Pauk
9da8142078 Merge tag '1.2.0' into develop
1.2.0
2024-03-25 12:54:14 +07:00
Book Pauk
cafdb5b04b Merge branch 'release/1.2.0' 2024-03-25 12:54:05 +07:00
Book Pauk
697774978e Добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47) 2024-03-25 12:52:46 +07:00
Book Pauk
8c2c2fe2fc 1.2.0 2023-12-07 16:31:13 +07:00
Book Pauk
e3770463a1 В списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий 2023-12-07 16:26:30 +07:00
Book Pauk
d3ad23e9e4 Актуализация пакетов 2023-12-07 15:01:26 +07:00
Book Pauk
79d1e0b30d Merge tag '1.1.3' into develop
1.1.3
2023-02-06 19:48:02 +07:00
Book Pauk
1370bae4d6 Merge branch 'release/1.1.3' 2023-02-06 19:47:56 +07:00
Book Pauk
01fbdf38fa Версия 1.1.3 2023-02-06 19:47:28 +07:00
Book Pauk
be6b07a0cf Исправление бага при обнулении libs 2023-02-06 19:45:13 +07:00
Book Pauk
1b057029c8 Улучшено хранение ключа доступа 2023-02-05 16:04:52 +07:00
Book Pauk
b6b567f20b Улучшение парсинга невалидных fb2 2023-02-03 17:30:22 +07:00
Book Pauk
c4c109fe0e Мелкий рефакторинг 2023-02-03 16:28:24 +07:00
Book Pauk
4c8c921b03 Улучшения механизма запуска периодических задач 2023-02-03 16:23:13 +07:00
Book Pauk
69a2e5cda3 Merge tag '1.1.2-1' into develop
1.1.2-1
2023-01-25 17:06:39 +07:00
Book Pauk
c2adf8d5b8 Merge branch 'release/1.1.2-1' 2023-01-25 17:06:35 +07:00
Book Pauk
5c8d257923 Добавлены отладочные сообщения в журнал 2023-01-25 17:05:53 +07:00
Book Pauk
55dae33e60 "jembadb": "^5.1.7" 2023-01-25 15:46:09 +07:00
Book Pauk
57d8e9061f Merge tag '1.1.2' into develop
1.1.2
2023-01-22 20:56:12 +07:00
Book Pauk
4642679842 Merge branch 'release/1.1.2' 2023-01-22 20:56:08 +07:00
Book Pauk
ba18743fab Версия 1.1.2 2023-01-22 20:55:48 +07:00
Book Pauk
e739356733 Исправление бага - не открывалась ссылка по нажатию кнопки "Открыть" 2023-01-22 20:50:06 +07:00
Book Pauk
cae4aed8d2 Merge tag '1.1.1-1' into develop
1.1.1-1
2023-01-11 21:32:22 +07:00
Book Pauk
6c6a08d8e0 Merge branch 'release/1.1.1-1' 2023-01-11 21:32:13 +07:00
Book Pauk
deafbae945 Версия 1.1.1 2023-01-11 21:31:40 +07:00
Book Pauk
0b23c609f1 Merge tag '1.1.1' into develop
1.1.1
2023-01-11 21:30:53 +07:00
Book Pauk
0359061321 Merge branch 'release/1.1.1' 2023-01-11 21:30:45 +07:00
Book Pauk
bc7a5f6be4 Merge tag '1.1.0-1' into develop
1.1.0-1
2023-01-11 21:30:36 +07:00
Book Pauk
be36f8f6e8 Merge branch 'release/1.1.0-1' 2023-01-11 21:30:30 +07:00
Book Pauk
3b8d084c76 Доработки ночного режима 2023-01-11 21:29:35 +07:00
Book Pauk
ce1cdca6a0 Merge tag '1.1.0' into develop
1.1.0
2023-01-11 21:06:40 +07:00
28 changed files with 5478 additions and 4200 deletions

View File

@@ -115,6 +115,10 @@ Options:
// Подключение себя, как клиента, к серверу обновлений
"bucServer": false
// Сcылка для открытия в новом окне брауpера по клику на кнопку "Сетевая библиотека"
// Если не задано, открывается внутренний менеджер библиотек с использванием фрейма
"networkLibraryLink": "http://samlib.ru/"
}
```

View File

@@ -1,11 +1,15 @@
import wsc from './webSocketConnection';
class Misc {
async loadConfig() {
async loadConfig(_configHash) {
const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch',
]};
const query = {
params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter',
'acceptFileExt', 'bucEnabled', 'branch', 'networkLibraryLink', 'restricted'
],
_configHash,
};
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
if (config.error)

View File

@@ -154,8 +154,11 @@ class App {
(async() => {
//загрузим конфиг сервера
try {
const config = await miscApi.loadConfig();
this.commit('config/setConfig', config);
const config = await miscApi.loadConfig(this.config._configHash);
if (!config._useCached)
this.commit('config/setConfig', config);
this.showPage = true;
} catch(e) {
//проверим, не получен ли конфиг ранее

View File

@@ -101,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%">
Открыть в читалке
@@ -312,6 +312,7 @@ class ExternalLibs {
inpxUrl = '';
created() {
this.commit = this.$store.commit;
this.oldStartLink = '';
this.justOpened = true;
this.$root.addEventHook('key', this.keyHook);
@@ -404,6 +405,8 @@ class ExternalLibs {
this.ready = true;
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'});
}
@@ -448,6 +451,11 @@ class ExternalLibs {
}
}
updateSets(sets) {
if (sets.nightMode !== this.nightMode)
this.commit('reader/nightModeToggle');
}
commitLibs(libs) {
this.sendMessage({type: 'libs', data: libs});
}
@@ -496,6 +504,10 @@ class ExternalLibs {
return this.$store.state.config.mode;
}
get nightMode() {
return this.$store.state.reader.settings.nightMode;
}
get header() {
let result = [this.ready ? 'Сетевая библиотека' : 'Загрузка...'];
if (this.ready && this.selectedLink) {

View File

@@ -52,18 +52,21 @@ class CopyTextPage {
from = (from < 0 ? 0 : from);
to = paraIndex + 100;
to = (to > parsed.para.length ? parsed.para.length : to);
cut = '<p>..... Текст вырезан. Если хотите скопировать больше, поставьте в настройках галочку "Загружать весь текст"';
cut = '<dd>..... Текст вырезан. Если хотите скопировать больше, поставьте в настройках галочку "Загружать весь текст"';
}
if (from > 0)
text += cut;
for (let i = from; i < to; i++) {
const p = parsed.para[i];
if (p.addIndex > 0)
continue;
const parts = parsed.splitToStyle(p.text);
if (this.stopInit)
return;
text += `<p id="p${i}" class="copyPara">`;
text += `<dd id="p${i}" class="copyPara">&nbsp;&nbsp;`;
for (const part of parts)
text += part.text;

View File

@@ -51,7 +51,7 @@ const tabs = [
['MouseHelpPage', 'Мышь/тачскрин'],
['HotkeysHelpPage', 'Клавиатура'],
['VersionHistoryPage', 'История версий'],
['DonateHelpPage', 'Помочь проекту'],
//['DonateHelpPage', 'Помочь проекту'],
];
const componentOptions = {

View File

@@ -34,8 +34,8 @@ class LibsPage {
if (!this.mode)
return;
//TODO: убрать второе условие в 24г
if (!this.libs || (this.mode === 'omnireader' && this.libs.mode !== this.mode)) {
//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);
}
@@ -119,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

@@ -393,6 +393,9 @@ class Reader {
this.recentItemKeys = [];
//сохранение в удаленном хранилище
await this.$refs.serverStorage.saveRecent(itemKeys);
//periodicTasks
this.periodicTasks();//no await
} catch (e) {
if (!this.offlineModeActive)
this.$root.notify.error(e.message);
@@ -442,26 +445,15 @@ class Reader {
this.$refs.recentBooksPage.init();
})();
//проверки обновлений читалки
//единственный запуск periodicTasks при инициализации
//дальнейшие запуски periodicTasks выполняются из debouncedSaveRecent
//т.е. только по действию пользователя
(async() => {
await utils.sleep(15*1000);
this.isFirstNeedUpdateNotify = true;
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
while (1) {// eslint-disable-line no-constant-condition
await this.checkNewVersionAvailable();
await utils.sleep(60*60*1000); //каждый час
}
//дальше хода нет
})();
//проверки обновлений книг
(async() => {
await utils.sleep(15*1000); //подождем неск. секунд перед первым запросом
//вечный цикл, запрашиваем периодически обновления
while (1) {// eslint-disable-line no-constant-condition
await this.checkBuc();
await utils.sleep(70*60*1000); //каждые 70 минут
}
//дальше хода нет
this.allowPeriodicTasks = true;
this.periodicTasks();//no await
})();
}
@@ -560,26 +552,56 @@ class Reader {
}
}
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});
}
if (this.version != this.clientVersion)
this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
} catch(e) {
console.error(e);
} finally {
this.checkingNewVersion = false;
}
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}.`, 'Обновление');
this.isFirstNeedUpdateNotify = false;
}
}
@@ -588,82 +610,78 @@ class Reader {
if (!this.bothBucEnabled)
return;
try {
const sorted = bookManager.getSortedRecent();
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 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;
}
//теперь по кусочкам запросим сервер
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);
await utils.sleep(1000);//чтобы не ддосить сервер
}
const data = await readerApi.checkBuc(chunk);
const checkSetTime = {};
//проставим новые размеры у книг
for (const book of sorted) {
if (book.deleted)
continue;
for (const item of data) {
bucSize[item.id] = item.size;
}
await utils.sleep(1000);//чтобы не ддосить сервер
//размер 0 считаем отсутствующим
if (book.url && bucSize[book.url] && bucSize[book.url] !== book.bucSize) {
book.bucSize = bucSize[book.url];
await bookManager.recentSetItem(book);
}
const checkSetTime = {};
//проставим новые размеры у книг
for (const book of sorted) {
if (book.deleted)
continue;
//подготовка к следующему шагу, ищем книгу по url с максимальной датой установки checkBucTime/loadTime
//от этой даты будем потом отсчитывать bucCancelDays
if (updateUrls.has(book.url)) {
let rec = checkSetTime[book.url] || {time: 0, loadTime: 0};
//размер 0 считаем отсутствующим
if (book.url && bucSize[book.url] && bucSize[book.url] !== book.bucSize) {
book.bucSize = bucSize[book.url];
await bookManager.recentSetItem(book);
}
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};
//подготовка к следующему шагу, ищем книгу по 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;
}
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)
;
//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);//!!!
}
if (book && !needBookUpdate) {
await bookManager.setCheckBuc(book, undefined);//!!!
}
}
}
await this.$refs.recentBooksPage.updateTableData();
} catch (e) {
console.error(e);
}
await this.$refs.recentBooksPage.updateTableData();
}
updateCountChanged(event) {
@@ -752,6 +770,10 @@ class Reader {
return this.$store.state.config.bucEnabled && this.bucEnabled;
}
get restricted() {
return this.$store.state.config.restricted;
}
get routeParamUrl() {
let result = '';
const path = this.$route.fullPath;
@@ -1014,6 +1036,11 @@ 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();//no await
@@ -1240,6 +1267,19 @@ class Reader {
return result;
}
isUrlAllowed(url) {
const restrictedSites = this.restricted?.sites;
if (restrictedSites) {
url = url.toLowerCase();
for (const site of restrictedSites) {
if (url.indexOf(site) === 0)
return false;
}
}
return true;
}
async _loadBook(opts) {
if (!opts || !opts.url) {
this.mostRecentBook();
@@ -1250,6 +1290,11 @@ class Reader {
let url = encodeURI(decodeURI(opts.url));
if (!this.isUrlAllowed(url)) {
this.$root.stdDialog.alert('Книга не загружена, причина: нарушение авторских прав.<br>Приносим извинения за неудобство.', '', {color: 'negative'});
return;
}
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
(url.indexOf('disk://') != 0))
url = 'http://' + url;
@@ -1361,6 +1406,7 @@ class Reader {
found = (found ? _.cloneDeep(found) : found);
if (found) {
//если такой файл уже не загружен (path не совпадают)
if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
@@ -1409,8 +1455,6 @@ class Reader {
if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
} finally {
this.checkNewVersionAvailable();
}
}

View File

@@ -131,7 +131,7 @@ class ReaderDialogs {
async init() {
await this.showWhatsNew();
await this.showDonation();
//await this.showDonation();
}
loadSettings() {

View File

@@ -201,7 +201,7 @@
<div
class="del-button self-end row justify-center items-center clickable"
@click="handleDel(item.key)"
@click="handleDel(item)"
>
<q-icon class="la la-times" size="12px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -212,7 +212,7 @@
<div
v-show="showArchive"
class="restore-button self-start row justify-center items-center clickable"
@click="handleRestore(item.key)"
@click="handleRestore(item)"
>
<q-icon class="la la-arrow-left" size="14px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -593,26 +593,51 @@ class RecentBooksPage {
}
}
async handleDel(key) {
if (!this.showArchive) {
await bookManager.delRecentBook({key});
this.$root.notify.info('Перенесено в архив');
async handleDel(item) {
if (item.group?.length) {
const keys = [{key: item.key}];
for (const book of item.group)
keys.push({key: book.key});
if (!this.showArchive) {
await bookManager.delRecentBooks(keys);
this.$root.notify.info(`Группа книг (всего ${keys.length}) перенесена в архив`);
} else {
if (await this.$root.stdDialog.confirm(`Подтвердите удаление группы книг (всего ${keys.length}) из архива:`, ' ')) {
await bookManager.delRecentBooks(keys, 2);
this.$root.notify.info('Группа книг удалена безвозвратно');
}
}
} else {
if (await this.$root.stdDialog.confirm('Подтвердите удаление из архива:', ' ')) {
await bookManager.delRecentBook({key}, 2);
this.$root.notify.info('Удалено безвозвратно');
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('Книга удалена безвозвратно');
}
}
}
}
async handleRestore(key) {
await bookManager.restoreRecentBook({key});
this.$root.notify.info('Восстановлено из архива');
async handleRestore(item) {
if (item.group?.length) {
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);
await this.handleRestore(item);
this.$emit('load-book', {url: item.url, path: item.path, force});
this.close();

View File

@@ -22,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();
@@ -85,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();
@@ -123,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);
}
@@ -141,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;

View File

@@ -53,7 +53,7 @@
</q-checkbox>
</div>
<div class="sets-item row">
<!--div class="sets-item row">
<div class="sets-label label">
Уведомление
</div>
@@ -63,7 +63,7 @@
Показывать диалог для сбора пожертвований
</q-tooltip>
</q-checkbox>
</div>
</div-->
<!---------------------------------------------->
<div class="sets-part-header">

View File

@@ -14,6 +14,11 @@ export default class DrawHelper {
return this.context.measureText(text).width;
}
measureTextMetrics(text, style) {// eslint-disable-line no-unused-vars
this.context.font = this.fontByStyle(style);
return this.context.measureText(text);
}
measureTextFont(text, font) {// eslint-disable-line no-unused-vars
this.context.font = font;
return this.context.measureText(text).width;
@@ -46,7 +51,22 @@ export default class DrawHelper {
tOpen += (part.style.italic ? '<i>' : '');
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
if (part.style.note) {
const t = part.text;
const m = this.measureTextMetrics(t, part.style);
const d = this.fontSize - 1.1*m.fontBoundingBoxAscent;
const w = m.width;
const size = (this.fontSize > 18 ? this.fontSize : 18);
const pad = size/2;
const btnW = (w >= size ? w : size) + pad*2;
tOpen += `<span style="position: relative;">` +
`<span style="position: absolute; background-color: ${this.textColor}; opacity: 0.1; cursor: pointer; pointer-events: auto; ` +
`height: ${this.fontSize + pad*2}px; padding: ${pad}px; left: -${(btnW - w)/2 - pad*0.05}px; top: -${pad + d}px; width: ${btnW}px; border-radius: ${size}px;" ` +
`onclick="onNoteClickLiberama('${part.style.note.id}', ${part.style.note.orig ? 1 : 0})"><span style="visibility: hidden;" class="dborder">${t}</span></span>`;
}
let tClose = '';
tClose += (part.style.note ? '</span>' : '');
tClose += (part.style.sub ? '</span>' : '');
tClose += (part.style.sup ? '</span>' : '');
tClose += (part.style.italic ? '</i>' : '');

View File

@@ -4,34 +4,30 @@
<div class="absolute" v-html="background"></div>
<div class="absolute" v-html="pageDivider"></div>
</div>
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollBox1" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
<div @copy.prevent="copyText" v-html="page1"></div>
</div>
</div>
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollBox2" class="scroll-box layout over-hidden" @wheel.prevent.stop="onMouseWheel">
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
<div @copy.prevent="copyText" v-html="page2"></div>
</div>
</div>
<div v-show="showStatusBar" ref="statusBar" class="layout">
<div v-show="showStatusBar" ref="statusBar" class="layout" :class="{'no-events': clickControl}">
<div v-html="statusBar"></div>
</div>
<div
v-show="clickControl" ref="layoutEvents" class="layout events"
oncontextmenu="return false;"
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
@mouseover.prevent.stop="onMouseEvent" @mouseout.prevent.stop="onMouseEvent" @mousemove.prevent.stop="onMouseEvent"
@wheel.prevent.stop="onMouseWheel"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
>
<div
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
></div>
</div>
<div
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
v-show="showStatusBar && statusBarClickOpen" class="layout"
@mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"
v-html="statusBarClickable"
@@ -40,6 +36,29 @@
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
<!-- Примечание -->
<Dialog ref="dialog1" v-model="noteDialogVisible">
<template #header>
{{ noteTitle }}
</template>
<div class="column col" style="line-height: 20px; max-width: 400px; max-height: 200px; overflow-x: hidden; overflow-y: auto">
<div v-html="noteHtml"></div>
</div>
<template #footer>
<div class="row col">
<q-btn class="q-px-md q-mr-md" color="btn2" text-color="app" dense no-caps @click="goToNotes">
В примечания
</q-btn>
</div>
<q-btn class="q-px-md" color="btn2" text-color="app" dense no-caps @click="noteDialogVisible = false">
OK
</q-btn>
</template>
</Dialog>
</div>
</template>
@@ -51,6 +70,7 @@ import {loadCSS} from 'fg-loadcss';
import _ from 'lodash';
import he from 'he';
import Dialog from '../../share/Dialog.vue';
import './TextPage.css';
import * as utils from '../../../share/utils';
@@ -62,7 +82,19 @@ import {clickMap} from '../share/clickMap';
const minLayoutWidth = 100;
//обработчик кликов по примечаниям, см. DrawHelper
//коряво, но иначе придется сильно усложнять рендеринг страниц (через Vue)
window.onNoteClickLiberama = (noteId, orig) => {
const textPage = window.textPageLiberama;
if (textPage) {
textPage.showNote(noteId, orig);
}
}
const componentOptions = {
components: {
Dialog
},
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -90,6 +122,7 @@ class TextPage {
_options = componentOptions;
showStatusBar = false;
statusBarClickOpen = false;
clickControl = true;
background = null;
@@ -114,6 +147,11 @@ class TextPage {
meta = null;
noteDialogVisible = false;
noteId = '';
noteTitle = '';
noteHtml = '';
created() {
this.drawHelper = new DrawHelper();
@@ -153,6 +191,8 @@ class TextPage {
await utils.sleep(200);
this.$nextTick(this.onResize);
});
window.textPageLiberama = this;
}
mounted() {
@@ -298,6 +338,8 @@ class TextPage {
let page1 = this.$refs.scrollBox1.style;
let page2 = this.$refs.scrollBox2.style;
page1.pointerEvents = page2.pointerEvents = (this.clickControl ? 'none' : 'auto');
page1.perspective = page2.perspective = '3072px';
page1.width = page2.width = this.boxW + this.indentLR + 'px';
@@ -913,6 +955,22 @@ class TextPage {
}
}
doPara(paraIndex) {
const para = this.parsed.para[paraIndex];
if (para && this.pageLineCount > 0) {
const lines = this.parsed.getLines(para.offset, this.pageLineCount);
if (lines.length >= this.pageLineCount) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = lines[0].begin;
} else
this.doEnd();
}
}
doToolBarToggle(event) {
this.$emit('do-action', {action: 'switchToolbar', event});
}
@@ -1016,6 +1074,7 @@ class TextPage {
if (this.startTouch) {
event.preventDefault();
}
this.endClickRepeat();
}
onTouchEnd(event) {
@@ -1100,6 +1159,9 @@ class TextPage {
onMouseWheel(event) {
if (this.$root.isMobileDevice)
return;
this.endClickRepeat();
if (event.deltaY > 0) {
this.doDown();
} else if (event.deltaY < 0) {
@@ -1107,6 +1169,12 @@ class TextPage {
}
}
onMouseEvent() {
if (this.$root.isMobileDevice)
return;
this.endClickRepeat();
}
onStatusBarClick() {
const url = this.meta.url;
if (url && url.indexOf('disk://') != 0) {
@@ -1209,6 +1277,43 @@ class TextPage {
event.clipboardData.setData('text/plain', filtered);
}
showNote(noteId, orig) {
const note = this.parsed.notes[noteId];
if (note) {
if (orig) {//show dialog
this.noteId = noteId;
this.noteTitle = `[${note.title?.trim()}]`;
this.noteHtml = note.xml
.replace(/<p>/g, '<p class="note-para">')
.replace(/<stanza>/g, '<br>').replace(/<\/stanza>/g, '')
.replace(/<v>/g, '<p style="margin: 0">').replace(/<\/v>/g, '</p>')
.replace(/<emphasis>/g, '<em>').replace(/<\/emphasis>/g, '</em>')
.replace(/<text-author>/g, '<br>').replace(/<\/text-author>/g, '')
;
this.noteDialogVisible = true;
} else {//go to orig
this.goToOrigNote(noteId);
}
}
}
goToNotes() {
const note = this.parsed.notes[this.noteId];
if (note && note.noteParaIndex >= 0) {
this.doPara(note.noteParaIndex);
this.noteDialogVisible = false;
}
}
goToOrigNote(noteId) {
const note = this.parsed.notes[noteId];
if (note && note.linkParaIndex >= 0) {
this.doPara(note.linkParaIndex);
this.noteDialogVisible = false;
}
}
}
export default vueComponent(TextPage);
@@ -1244,8 +1349,18 @@ export default vueComponent(TextPage);
}
.events {
z-index: 20;
z-index: 9;
background-color: rgba(0,0,0,0);
}
.no-events {
pointer-events: none;
}
</style>
<style>
.note-para {
margin: 0;
padding: 0;
margin-bottom: 10px;
}
</style>

View File

@@ -86,17 +86,24 @@ export default class BookParser {
let binaryType = '';
let dimPromises = [];
this.coverPageId = '';
this.images = [];
let imageNum = 0;
//примечания
this.notes = {};
let inNote = false;
let noteId = '';
let inNotesBody = false;
const noteTags = new Set(['p', 'poem', 'stanza', 'v', 'text-author', 'emphasis']);
//оглавление
this.contents = [];
this.images = [];
let curTitle = {paraIndex: -1, title: '', subtitles: []};
let curSubtitle = {paraIndex: -1, title: ''};
let inTitle = false;
let inSubtitle = false;
let sectionLevel = 0;
let bodyIndex = 0;
let imageNum = 0;
let paraIndex = -1;
let paraOffset = 0;
@@ -289,7 +296,7 @@ export default class BookParser {
if (attrs.href && attrs.href.value) {
const href = attrs.href.value;
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
const {id, local} = this.imageHrefToId(href);
const {id, local} = this.hrefToId(href);
if (local) {//local
imageNum++;
@@ -322,6 +329,23 @@ export default class BookParser {
}
}
if (tag == 'a') {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value && attrs.type && attrs.type.value === 'note') {//note
const href = attrs.href.value;
const {id, local} = this.hrefToId(href);
if (local) {
inNote = true;
growParagraph(`<note href="${id}" orig="1">`, 0);
if (!this.notes[id]) {
this.notes[id] = {id, linkParaIndex: paraIndex};
}
}
}
}
if (path == '/fictionbook/description/title-info/author') {
if (!fb2.author)
fb2.author = [];
@@ -350,6 +374,11 @@ export default class BookParser {
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
let attrs = sax.getAttrsSync(tail);
if (attrs.name && attrs.name.value === 'notes') {//notes
inNotesBody = true;
}
if (isFirstBody && fb2.annotation) {
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
ann.forEach(a => {
@@ -373,6 +402,31 @@ export default class BookParser {
bodyIndex++;
}
if (tag == 'section') {
if (!isFirstSection)
newParagraph();
isFirstSection = false;
sectionLevel++;
if (inNotesBody) {
let attrs = sax.getAttrsSync(tail);
if (attrs.id && attrs.id.value) {//notes
const id = attrs.id.value;
let note = this.notes[id];
if (!note) {
note = {id};
this.notes[id] = note;
}
note.noteParaIndex = paraIndex;
note.xml = '';
note.title = '';
noteId = id;
}
}
}
if (tag == 'title') {
newParagraph();
isFirstTitlePara = true;
@@ -384,13 +438,6 @@ export default class BookParser {
this.contents.push(curTitle);
}
if (tag == 'section') {
if (!isFirstSection)
newParagraph();
isFirstSection = false;
sectionLevel++;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`<${tag}>`, 0);
}
@@ -401,6 +448,10 @@ export default class BookParser {
if (tag == 'p') {
inPara = true;
isFirstTitlePara = false;
if (inTitle && inNotesBody && noteId) {
growParagraph(`<note href="${noteId}">`, 0);
}
}
}
@@ -434,65 +485,88 @@ export default class BookParser {
bold = true;
space += 1;
}
if (!inTitle && inNotesBody && noteId && noteTags.has(tag)) {
this.notes[noteId].xml += `<${tag}>`;
}
}
};
const onEndNode = (elemName) => {// eslint-disable-line no-unused-vars
if (tag == elemName) {
if (tag == 'binary') {
binaryId = '';
tag = elemName;
if (tag == 'a' && inNote) {
growParagraph('</note>', 0);
inNote = false;
}
if (tag == 'binary') {
binaryId = '';
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'body') {
inNotesBody = false;
}
if (path.indexOf('/fictionbook/body') == 0) {
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
if (tag == 'title') {
isFirstTitlePara = false;
bold = false;
center = false;
inTitle = false;
}
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'section') {
sectionLevel--;
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0);
}
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
growParagraph(`</${tag}>`, 0);
}
if (tag == 'p') {
inPara = false;
}
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 (inTitle && inNotesBody && noteId) {
growParagraph('</note>', 0);
}
}
path = path.substr(0, path.length - tag.length - 1);
let i = path.lastIndexOf('/');
if (i >= 0) {
tag = path.substr(i + 1);
} else {
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 (!inTitle && inNotesBody && noteId && noteTags.has(tag)) {
this.notes[noteId].xml += `</${tag}>`;
}
}
let i = path.lastIndexOf(tag);
if (i >= 0) {
path = path.substring(0, i - 1);
i = path.lastIndexOf('/');
if (i >= 0)
tag = path.substring(i + 1);
else
tag = path;
}
}
};
@@ -568,6 +642,14 @@ export default class BookParser {
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
else
growParagraph(' ', 1);
if (inNotesBody && noteId) {
if (inTitle) {
this.notes[noteId].title += text;
} else {
this.notes[noteId].xml += text;
}
}
}
};
@@ -600,7 +682,7 @@ export default class BookParser {
return {fb2};
}
imageHrefToId(id) {
hrefToId(id) {
let local = false;
if (id[0] == '#') {
id = id.substr(1);
@@ -633,7 +715,7 @@ export default class BookParser {
splitToStyle(s) {
let result = [];/*array of {
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number, note: Object},
image: {local: Boolean, inline: Boolean, id: String},
text: String,
}*/
@@ -684,7 +766,7 @@ export default class BookParser {
case 'image': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
image = this.imageHrefToId(attrs.href.value);
image = this.hrefToId(attrs.href.value);
image.inline = false;
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
}
@@ -693,7 +775,7 @@ export default class BookParser {
case 'image-inline': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
const img = this.imageHrefToId(attrs.href.value);
const img = this.hrefToId(attrs.href.value);
img.inline = true;
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
result.push({
@@ -704,6 +786,13 @@ export default class BookParser {
}
break;
}
case 'note': {
let attrs = sax.getAttrsSync(tail);
if (attrs.href && attrs.href.value) {
style.note = {id: attrs.href.value, orig: attrs.orig?.value};
}
break;
}
}
};
@@ -732,6 +821,9 @@ export default class BookParser {
break;
case 'image-inline':
break;
case 'note':
style.note = false;
break;
}
};

View File

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

View File

@@ -1,6 +1,126 @@
export const versionHistory = [
{
version: '1.1.0',
version: '1.2.8',
releaseDate: '2025-06-04',
showUntil: '2025-06-03',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.7',
releaseDate: '2025-02-22',
showUntil: '2025-02-21',
content:
`
<ul>
<li>отключена форма для сбора донатов</li>
<li>мелкие оптимизации</li>
</ul>
`
},
{
version: '1.2.6',
releaseDate: '2024-10-03',
showUntil: '2024-10-02',
content:
`
<ul>
<li>исправления из-за нарушения авторских прав</li>
</ul>
`
},
{
version: '1.2.4',
releaseDate: '2024-08-27',
showUntil: '2024-08-26',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.3',
releaseDate: '2024-08-02',
showUntil: '2024-08-01',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.2',
releaseDate: '2024-07-28',
showUntil: '2024-07-27',
content:
`
<ul>
<li>добавлено отображение примечаний на месте, по клику на сноске (#50)</li>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.2.0',
releaseDate: '2024-03-25',
showUntil: '2024-03-24',
content:
`
<ul>
<li>в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий</li>
<li>добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47)</li>
</ul>
`
},
{
version: '1.1.3',
releaseDate: '2023-02-06',
showUntil: '2023-02-05',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.1.2',
releaseDate: '2023-01-22',
showUntil: '2023-01-21',
content:
`
<ul>
<li>исправление багов</li>
</ul>
`
},
{
version: '1.1.1',
releaseDate: '2023-01-11',
showUntil: '2023-01-15',
content:

View File

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

View File

@@ -325,7 +325,7 @@ const state = {
currentProfile: '',
settings: _.cloneDeep(settingDefaults),
settingsRev: {},
libs: false,
libs: {},
libsRev: 0,
};

8653
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "liberama",
"version": "1.1.0",
"version": "1.2.8",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",
@@ -25,66 +25,66 @@
"scripts": "server/config/*.js"
},
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/eslint-parser": "^7.19.1",
"@babel/eslint-plugin": "^7.19.1",
"@babel/plugin-proposal-decorators": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/core": "^7.23.5",
"@babel/eslint-parser": "^7.23.3",
"@babel/eslint-plugin": "^7.23.5",
"@babel/plugin-proposal-decorators": "^7.23.5",
"@babel/preset-env": "^7.23.5",
"@vue/compiler-sfc": "^3.2.22",
"babel-loader": "^9.1.0",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^4.2.2",
"eslint": "^8.29.0",
"eslint-plugin-vue": "^9.8.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.7.2",
"pkg": "^5.8.0",
"eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2",
"html-webpack-plugin": "^5.5.4",
"mini-css-extract-plugin": "^2.7.6",
"pkg": "^5.8.1",
"showdown": "^2.1.0",
"terser-webpack-plugin": "^5.3.6",
"vue-eslint-parser": "^9.1.0",
"vue-loader": "^17.0.1",
"terser-webpack-plugin": "^5.3.9",
"vue-eslint-parser": "^9.3.2",
"vue-loader": "^17.3.1",
"vue-style-loader": "^4.1.3",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-middleware": "^6.0.1",
"webpack-hot-middleware": "^2.25.3",
"webpack-merge": "^5.8.0",
"workbox-webpack-plugin": "^6.5.4"
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-middleware": "^6.1.1",
"webpack-hot-middleware": "^2.25.4",
"webpack-merge": "^5.10.0",
"workbox-webpack-plugin": "^6.6.0"
},
"dependencies": {
"@quasar/extras": "^1.15.8",
"@vue/compat": "^3.2.45",
"@quasar/extras": "^1.16.9",
"@vue/compat": "^3.3.10",
"axios": "^0.27.2",
"base-x": "^4.0.0",
"chardet": "^1.5.0",
"chardet": "^1.6.0",
"compression": "^1.7.4",
"dayjs": "^1.11.7",
"dayjs": "^1.11.10",
"express": "^4.18.2",
"fg-loadcss": "^3.1.0",
"fs-extra": "^10.1.0",
"he": "^1.2.0",
"iconv-lite": "^0.6.3",
"jembadb": "^5.1.5",
"jembadb": "^5.1.7",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"minimist": "^1.2.7",
"minimist": "^1.2.8",
"multer": "^1.4.5-lts.1",
"pako": "^2.1.0",
"path-browserify": "^1.0.1",
"pidusage": "^3.0.2",
"quasar": "^2.10.2",
"quasar": "^2.14.1",
"safe-buffer": "^5.2.1",
"sanitize-html": "^2.8.0",
"sanitize-html": "^2.11.0",
"sjcl": "^1.0.8",
"tar-fs": "^2.1.1",
"unbzip2-stream": "^1.4.3",
"vue": "^3.2.37",
"vue-router": "^4.1.6",
"vue-router": "^4.2.5",
"vuex": "^4.1.0",
"vuex-persist": "^3.1.3",
"webdav": "^4.11.2",
"ws": "^8.11.0",
"zip-stream": "^4.1.0"
"webdav": "^4.11.3",
"ws": "^8.14.2",
"zip-stream": "^4.1.1"
}
}

View File

@@ -18,7 +18,8 @@ module.exports = {
useExternalBookConverter: false,
acceptFileExt: '.fb2, .fb3, .html, .txt, .zip, .bz2, .gz, .rar, .epub, .mobi, .rtf, .doc, .docx, .pdf, .djvu, .jpg, .jpeg, .png',
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch'],
restricted: {},
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'bucEnabled', 'branch', 'networkLibraryLink', 'restricted'],
jembaDb: [
{
@@ -56,6 +57,9 @@ module.exports = {
ip: '0.0.0.0',
port: '33443',
accessToken: '',
shciForHost: {
'samlib.ru': 300000
},
}*/
],
@@ -74,5 +78,6 @@ module.exports = {
accessToken: '',
}
*/
networkLibraryLink: '',
};

View File

@@ -14,6 +14,7 @@ const propsToSave = [
'remoteStorage',
'bucEnabled',
'bucServer',
'networkLibraryLink',
];
let instance = null;
@@ -55,6 +56,7 @@ class ConfigManager {
await fs.ensureDir(config.dataDir);
this._userConfigFile = `${config.dataDir}/config.json`;
this._restrictedFile = `${config.dataDir}/restricted.json`;
this._config = config;
this.inited = true;
@@ -74,6 +76,10 @@ class ConfigManager {
return this._userConfigFile;
}
get restrictedFile() {
return this._restrictedFile;
}
set userConfigFile(value) {
if (value)
this._userConfigFile = value;
@@ -99,6 +105,12 @@ class ConfigManager {
} else {
await this.save();
}
if (await fs.pathExists(this.restrictedFile)) {
const data = JSON.parse(await fs.readFile(this.restrictedFile, 'utf8'));
this.config = {restricted: data};
}
} catch(e) {
throw new Error(`Error while loading "${this.userConfigFile}": ${e.message}`);
}

View File

@@ -20,6 +20,8 @@ class WebSocketController {
this.readerWorker = new ReaderWorker(config);
this.workerState = new WorkerState();
this.configHash = '';
if (config.bucEnabled) {
this.bucClient = new BUCClient(config);
}
@@ -119,8 +121,22 @@ class WebSocketController {
async getConfig(req, ws) {
if (Array.isArray(req.params)) {
const paramsSet = new Set(req.params);
const _configHash = req._configHash;
this.send(_.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))), req, ws);
let response = {_useCached: true};
//оптимизация, чтобы не отдавал большой конфиг каждый раз при обновлении страницы
if (!_configHash || _configHash !== this.configHash) {
if (!this.configHash) {
const webConfig = _.pick(this.config, this.config.webConfigParams);
this.configHash = await utils.getBufHash(Buffer.from(JSON.stringify(webConfig)), 'sha256', 'hex');
}
response = _.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x)));
response._configHash = this.configHash;
}
this.send(response, req, ws);
} else {
throw new Error('params is not an array');
}

View File

@@ -27,8 +27,8 @@ class BUCServer {
this.cleanQueryInterval = 300*dayMs;//интервал очистки устаревших
this.oldQueryInterval = 14*dayMs;//интервал устаревания запроса на обновление
this.checkingInterval = 5*hourMs;//интервал проверки обновления одного и того же файла
this.sameHostCheckInterval = 1000;//интервал проверки файла на том же сайте, не менее
this.checkingInterval = 1*dayMs;//интервал проверки обновления одного и того же файла
this.sameHostCheckInterval = 10*1000;//интервал проверки файла на том же сайте, не менее
} else {
this.maxCheckQueueLength = 10;//максимальная длина checkQueue
this.fillCheckQueuePeriod = 10*1000;//период пополнения очереди
@@ -51,6 +51,7 @@ class BUCServer {
this.checkQueue = [];
this.hostChecking = {};
this.shciForHost = this.config.shciForHost || {};//sameHostCheckInterval for host
this.main(); //no await
@@ -262,7 +263,7 @@ class BUCServer {
let unchanged = true;
let hash = '';
const headers = await this.down.head(row.id);
const headers = await this.down.head(row.id, {timeout: 10*1000});
const etag = headers['etag'] || '';
const modTime = headers['last-modified'] || '';
@@ -276,7 +277,7 @@ class BUCServer {
&& (!size || !row.size || (size !== row.size))
) {
const downdata = await this.down.load(row.id);
const downdata = await this.down.load(row.id, {timeout: 10*1000});
size = downdata.length;
hash = await utils.getBufHash(downdata, 'sha256', 'hex');
@@ -316,7 +317,12 @@ class BUCServer {
log(LM_ERR, `error ${row.id} > ${e.stack ? e.stack : e.message}`);
} finally {
(async() => {
await utils.sleep(this.sameHostCheckInterval);
let sameHostCheckInterval = this.shciForHost[url.hostname] || this.sameHostCheckInterval;
sameHostCheckInterval = Math.round((Math.random() - 0.5)*(sameHostCheckInterval*0.2) + sameHostCheckInterval);
log(`delay ${sameHostCheckInterval}ms for host '${url.hostname}'`);
await utils.sleep(sameHostCheckInterval);
this.hostChecking[url.hostname] = false;
})();
}
@@ -327,7 +333,7 @@ class BUCServer {
log(LM_ERR, e.stack);
}
await utils.sleep(10);
await utils.sleep(100);
}
}

View File

@@ -2,7 +2,7 @@ const https = require('https');
const axios = require('axios');
const utils = require('./utils');
const userAgent = 'Mozilla/5.0 (X11; HasCodingOs 1.0; Linux x64) AppleWebKit/637.36 (KHTML, like Gecko) Chrome/70.0.3112.101 Safari/637.36 HasBrowser/5.0';
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0';
class FileDownloader {
constructor(limitDownloadSize = 0) {
@@ -16,7 +16,6 @@ class FileDownloader {
headers: {
'accept-encoding': 'gzip, compress, deflate',
'user-agent': userAgent,
timeout: 300*1000,
},
httpsAgent: new https.Agent({
rejectUnauthorized: false // решение проблемы 'unable to verify the first certificate' для некоторых сайтов с валидным сертификатом
@@ -26,6 +25,9 @@ class FileDownloader {
if (opts)
options = Object.assign({}, opts, options);
if (!options.timeout)
options.timeout = 300*1000;//5 min
try {
const res = await axios.get(url, options);
@@ -77,8 +79,8 @@ class FileDownloader {
const options = {
headers: {
'user-agent': userAgent,
timeout: 10*1000,
},
timeout: 10*1000,
};
const res = await axios.head(url, options);

View File

@@ -24,6 +24,7 @@ class JembaReaderStorage {
getCache(id) {
const obj = this.cacheMap.get(id);
//обновляем время доступа и при чтении тоже
if (obj)
obj.time = Date.now();
return obj;
@@ -118,6 +119,7 @@ class JembaReaderStorage {
//identity необходимо для работы при нестабильной связи,
//одному и тому же клиенту разрешается перезаписывать данные при расхождении на 0 или 1 ревизию
const obj = this.getCache(id) || {};
const oldIdentity = obj.identity;
const sameClient = (identity && obj.identity === identity);
if (identity && obj.identity !== identity) {
obj.identity = identity;
@@ -126,8 +128,12 @@ class JembaReaderStorage {
const revDiff = items[id].rev - check.items[id].rev;
const allowUpdate = force || revDiff === 1 || (sameClient && (revDiff === 0 || revDiff === 1));
if (!allowUpdate)
if (!allowUpdate) {
log(LM_ERR, `JembaReaderStorage-Reject: revDiff: ${revDiff}, sameClient: ${sameClient}, oldIdentity: ${oldIdentity}, identity: ${identity}`);
return {state: 'reject', items: check.items};
}
}
const db = this.db;

View File

@@ -1,4 +1,5 @@
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const fs = require('fs-extra');
const express = require('express');