Compare commits
107 Commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c9fd7678d | ||
|
|
01313d66b2 | ||
|
|
eaeacbfb1b | ||
|
|
5328998c21 | ||
|
|
ee066c7c4b | ||
|
|
130aebb514 | ||
|
|
dbec1e630e | ||
|
|
583b966616 | ||
|
|
9e509ac845 | ||
|
|
4ea2d8918e | ||
|
|
6667688193 | ||
|
|
30a1629f23 | ||
|
|
ba50faeebb | ||
|
|
3c0d784e3d | ||
|
|
3e75310e1f | ||
|
|
2b01d6d8d7 | ||
|
|
be6d60d7a9 | ||
|
|
3c0815d55b | ||
|
|
abd8584cb8 | ||
|
|
5a910f80b3 | ||
|
|
67bdfd853e | ||
|
|
fc8e986acb | ||
|
|
64539785c2 | ||
|
|
f530455146 | ||
|
|
70dc66e1ae | ||
|
|
3e5894d9e0 | ||
|
|
d7ac9d1bfc | ||
|
|
5160c5fb75 | ||
|
|
d9c7964410 | ||
|
|
110952b4c4 | ||
|
|
ece17dc0dd | ||
|
|
35e1087531 | ||
|
|
59c4b62770 | ||
|
|
4be9ce5ff3 | ||
|
|
92a811cabd | ||
|
|
897cdc8ac7 | ||
|
|
418ff482ae | ||
|
|
8858d6d1f2 | ||
|
|
41f8a28631 | ||
|
|
da0771d5e5 | ||
|
|
c03995367a | ||
|
|
0430105061 | ||
|
|
afd4d02dad | ||
|
|
d634ebf14c | ||
|
|
613230256a | ||
|
|
2da1736c99 | ||
|
|
1914092520 | ||
|
|
4a6f93a14f | ||
|
|
9da8142078 | ||
|
|
cafdb5b04b | ||
|
|
697774978e | ||
|
|
8c2c2fe2fc | ||
|
|
e3770463a1 | ||
|
|
d3ad23e9e4 | ||
|
|
79d1e0b30d | ||
|
|
1370bae4d6 | ||
|
|
01fbdf38fa | ||
|
|
be6b07a0cf | ||
|
|
1b057029c8 | ||
|
|
b6b567f20b | ||
|
|
c4c109fe0e | ||
|
|
4c8c921b03 | ||
|
|
69a2e5cda3 | ||
|
|
c2adf8d5b8 | ||
|
|
5c8d257923 | ||
|
|
55dae33e60 | ||
|
|
57d8e9061f | ||
|
|
4642679842 | ||
|
|
ba18743fab | ||
|
|
e739356733 | ||
|
|
cae4aed8d2 | ||
|
|
6c6a08d8e0 | ||
|
|
deafbae945 | ||
|
|
0b23c609f1 | ||
|
|
0359061321 | ||
|
|
bc7a5f6be4 | ||
|
|
be36f8f6e8 | ||
|
|
3b8d084c76 | ||
|
|
ce1cdca6a0 | ||
|
|
2f380dce1b | ||
|
|
63b7bb24cf | ||
|
|
2401ef8d16 | ||
|
|
62df3c0197 | ||
|
|
ba2dbca226 | ||
|
|
810b131b92 | ||
|
|
1d5bcde293 | ||
|
|
2fcf584e40 | ||
|
|
ecc6791892 | ||
|
|
8bf19c1e69 | ||
|
|
273ab4ae60 | ||
|
|
ec8fedc73d | ||
|
|
e6b1d4b032 | ||
|
|
a89572f85f | ||
|
|
bf4f5bc88b | ||
|
|
f4ce1f337e | ||
|
|
5e8afa15b2 | ||
|
|
7b1d0bb778 | ||
|
|
c0aec66f0f | ||
|
|
31481453f5 | ||
|
|
9724ec230c | ||
|
|
9e4be96522 | ||
|
|
91097515f2 | ||
|
|
230c3bb5b2 | ||
|
|
7a71db9de4 | ||
|
|
7261afc428 | ||
|
|
ddde7d038b | ||
|
|
4d3d66fbe2 |
@@ -115,6 +115,10 @@ Options:
|
||||
|
||||
// Подключение себя, как клиента, к серверу обновлений
|
||||
"bucServer": false
|
||||
|
||||
// Сcылка для открытия в новом окне брауpера по клику на кнопку "Сетевая библиотека"
|
||||
// Если не задано, открывается внутренний менеджер библиотек с использванием фрейма
|
||||
"networkLibraryLink": "http://samlib.ru/"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
@@ -45,6 +47,34 @@ class App {
|
||||
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 = '';
|
||||
@@ -56,7 +86,7 @@ class App {
|
||||
|
||||
}
|
||||
return cachedRoute;
|
||||
}
|
||||
};
|
||||
|
||||
this.$router.beforeEach((to, from, next) => {
|
||||
//распознавание хоста, если присутствует домен 3-уровня "b.", то разрешена только определенная страница
|
||||
@@ -112,6 +142,8 @@ class App {
|
||||
window.addEventListener('resize', (event) => {
|
||||
this.$root.eventHook('resize', event);
|
||||
});
|
||||
|
||||
this.setNightMode();
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@@ -122,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) {
|
||||
//проверим, не получен ли конфиг ранее
|
||||
@@ -145,38 +180,6 @@ class App {
|
||||
})();
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
|
||||
get isCollapse() {
|
||||
return this.uistate.asideBarCollapse;
|
||||
}
|
||||
|
||||
get asideWidth() {
|
||||
if (this.uistate.asideBarCollapse) {
|
||||
return 64;
|
||||
} else {
|
||||
return 170;
|
||||
}
|
||||
}
|
||||
|
||||
get buttonCollapseIcon() {
|
||||
if (this.uistate.asideBarCollapse) {
|
||||
return 'el-icon-d-arrow-right';
|
||||
} else {
|
||||
return 'el-icon-d-arrow-left';
|
||||
}
|
||||
}
|
||||
|
||||
get appName() {
|
||||
if (this.isCollapse)
|
||||
return '<br><br>';
|
||||
else
|
||||
return `${this.config.name} <br>v${this.config.version}`;
|
||||
}
|
||||
|
||||
get apiError() {
|
||||
return this.state.apiError;
|
||||
}
|
||||
@@ -185,6 +188,15 @@ 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') {
|
||||
@@ -207,26 +219,15 @@ class App {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get isReaderActive() {
|
||||
return (this.rootRoute == '/reader' || this.rootRoute == '/external-libs');
|
||||
}
|
||||
|
||||
redirectIfNeeded() {
|
||||
if ((this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama')) {
|
||||
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 = 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} });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,22 +237,151 @@ 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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,7 +110,7 @@
|
||||
</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" allow="clipboard-read; clipboard-write"></iframe>
|
||||
</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>
|
||||
@@ -309,6 +312,7 @@ class ExternalLibs {
|
||||
inpxUrl = '';
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.oldStartLink = '';
|
||||
this.justOpened = true;
|
||||
this.$root.addEventHook('key', this.keyHook);
|
||||
@@ -401,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'});
|
||||
}
|
||||
@@ -445,6 +451,11 @@ class ExternalLibs {
|
||||
}
|
||||
}
|
||||
|
||||
updateSets(sets) {
|
||||
if (sets.nightMode !== this.nightMode)
|
||||
this.commit('reader/nightModeToggle');
|
||||
}
|
||||
|
||||
commitLibs(libs) {
|
||||
this.sendMessage({type: 'libs', data: libs});
|
||||
}
|
||||
@@ -493,14 +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) {
|
||||
let title = `${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||
if (this.inpxReady && this.inpxTitle)
|
||||
title = `${this.inpxTitle} ${lu.removeProtocol(this.inpxUrl)}`;
|
||||
result += ` | ${title}`;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"> `;
|
||||
for (const part of parts)
|
||||
text += part.text;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<Window @close="close" style="z-index: 200">
|
||||
<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>
|
||||
@@ -51,7 +51,7 @@ const tabs = [
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
['DonateHelpPage', 'Помочь проекту'],
|
||||
//['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
const componentOptions = {
|
||||
|
||||
@@ -72,7 +72,7 @@ p {
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
color: var(--text-anchor-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
@@ -158,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});
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ export default vueComponent(LoaderPage);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
color: var(--text-anchor-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
|
||||
@@ -115,6 +115,12 @@
|
||||
|
||||
<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%">
|
||||
@@ -136,7 +142,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main col row relative-position">
|
||||
<div class="col row relative-position main">
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="activePage"
|
||||
@@ -290,6 +296,8 @@ class Reader {
|
||||
contentsActive = false;
|
||||
libsActive = false;
|
||||
recentBooksActive = false;
|
||||
|
||||
nightModeActive = false;
|
||||
clickControlActive = false;
|
||||
settingsActive = false;
|
||||
|
||||
@@ -385,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);
|
||||
@@ -434,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
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -462,8 +462,8 @@ 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;
|
||||
@@ -552,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});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -580,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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
//подготовка к следующему шагу, ищем книгу по 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};
|
||||
|
||||
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) {
|
||||
@@ -744,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;
|
||||
@@ -1006,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
|
||||
@@ -1014,10 +1049,16 @@ class Reader {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -1119,6 +1160,7 @@ class Reader {
|
||||
case 'contents':
|
||||
case 'libs':
|
||||
case 'recentBooks':
|
||||
case 'nightMode':
|
||||
case 'clickControl':
|
||||
case 'offlineMode':
|
||||
case 'settings':
|
||||
@@ -1167,7 +1209,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;
|
||||
@@ -1225,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();
|
||||
@@ -1235,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;
|
||||
@@ -1346,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) ||
|
||||
@@ -1394,8 +1455,6 @@ class Reader {
|
||||
if (!this.showHelpOnErrorIfNeeded(url)) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
} finally {
|
||||
this.checkNewVersionAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1525,6 +1584,9 @@ class Reader {
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'nightMode':
|
||||
this.nightModeToggle();
|
||||
break;
|
||||
case 'clickControl':
|
||||
this.clickControlToggle();
|
||||
break;
|
||||
@@ -1674,15 +1736,15 @@ export default vueComponent(Reader);
|
||||
}
|
||||
|
||||
.main {
|
||||
background-color: #EBE2C9;
|
||||
color: #000;
|
||||
background-color: var(--bg-loader-color);
|
||||
color: var(--text-app-color);
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
margin: 0px 2px 7px 2px;
|
||||
padding: 0;
|
||||
color: #3E843E;
|
||||
background-color: #E6EDF4;
|
||||
color: var(--text-tb-normal);
|
||||
background-color: var(--bg-tb-normal);
|
||||
min-height: 38px;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
@@ -1694,34 +1756,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<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>
|
||||
|
||||
<q-dialog ref="dialog2" v-model="donationVisible" style="z-index: 100" no-route-dismiss no-esc-dismiss no-backdrop-dismiss>
|
||||
<div class="column bg-white no-wrap q-pa-md">
|
||||
<div class="column bg-dialog no-wrap q-pa-md">
|
||||
<div class="row justify-center q-mb-md">
|
||||
Здравствуйте, дорогие читатели!
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
<div style="word-break: normal">
|
||||
Если вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
|
||||
<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>
|
||||
@@ -131,7 +131,7 @@ class ReaderDialogs {
|
||||
|
||||
async init() {
|
||||
await this.showWhatsNew();
|
||||
await this.showDonation();
|
||||
//await this.showDonation();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
@@ -233,7 +233,7 @@ export default vueComponent(ReaderDialogs);
|
||||
|
||||
<style scoped>
|
||||
.clickable {
|
||||
color: blue;
|
||||
color: var(--text-anchor-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -36,29 +36,29 @@
|
||||
<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%">
|
||||
На текущую книгу
|
||||
@@ -71,7 +71,7 @@
|
||||
class="q-ml-sm q-mt-xs"
|
||||
outlined dense
|
||||
style="width: 185px"
|
||||
bg-color="white"
|
||||
bg-color="input"
|
||||
placeholder="Найти"
|
||||
@click.stop
|
||||
>
|
||||
@@ -86,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
|
||||
@@ -140,7 +140,7 @@
|
||||
class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
|
||||
:style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
|
||||
>
|
||||
<div class="text-green-10" style="font-size: 80%">
|
||||
<div :class="dark ? 'text-lime-4' : 'text-green-10'" style="font-size: 80%">
|
||||
{{ item.desc.author }}
|
||||
</div>
|
||||
<div style="font-size: 75%">
|
||||
@@ -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%">
|
||||
@@ -349,6 +349,10 @@ class RecentBooksPage {
|
||||
return this.$store.state.config.bucEnabled && this.bucEnabled;
|
||||
}
|
||||
|
||||
get dark() {
|
||||
return this.$store.state.reader.settings.nightMode;
|
||||
}
|
||||
|
||||
async updateTableData() {
|
||||
if (!this.inited)
|
||||
return;
|
||||
@@ -589,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();
|
||||
@@ -847,7 +876,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -870,15 +899,15 @@ export default vueComponent(RecentBooksPage);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -895,7 +924,6 @@ export default vueComponent(RecentBooksPage);
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
margin: 10px 6px 0px 3px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.row-info-bottom {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<q-input
|
||||
ref="input" v-model="needle"
|
||||
class="col" outlined dense
|
||||
bg-color="input"
|
||||
placeholder="Найти"
|
||||
@keydown="inputKeyDown"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export default vueComponent(SetPositionPage);
|
||||
.slider {
|
||||
margin: 0 20px 0 20px;
|
||||
height: 35px;
|
||||
background-color: #efefef;
|
||||
background-color: var(--bg-input-color);
|
||||
border-radius: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
Качество
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.pdfQuality" class="col-5" :min="10" :max="100">
|
||||
<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>
|
||||
@@ -93,7 +93,7 @@
|
||||
Качество
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.djvuQuality" class="col-5" :min="10" :max="100">
|
||||
<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>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="fit column">
|
||||
<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
|
||||
class="bg-grey-4 text-grey-7"
|
||||
class="bg-menu-2 text-menu"
|
||||
>
|
||||
<q-tab name="mouse" label="Мышь/тачскрин" />
|
||||
<q-tab name="keyboard" label="Клавиатура" />
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
@@ -234,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 {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="sets-item row">
|
||||
<!--div class="sets-item row">
|
||||
<div class="sets-label label">
|
||||
Уведомление
|
||||
</div>
|
||||
@@ -63,27 +63,13 @@
|
||||
Показывать диалог для сбора пожертвований
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="sets-part-header">
|
||||
Другое
|
||||
</div>
|
||||
|
||||
<div class="sets-item row">
|
||||
<div class="sets-label label">
|
||||
Обработка
|
||||
</div>
|
||||
<q-checkbox v-model="form.lazyParseEnabled" size="xs" 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="sets-item row">
|
||||
<div class="sets-label label">
|
||||
Парам. в URL
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Тип
|
||||
</div>
|
||||
<q-select
|
||||
v-model="form.pageChangeAnimation" class="col-left" :options="pageChangeAnimationOptions"
|
||||
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
|
||||
/>
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="sets-label label">
|
||||
Скорость
|
||||
</div>
|
||||
<NumInput v-model="form.pageChangeAnimationSpeed" class="col-left" :min="0" :max="100" :disable="form.pageChangeAnimation == ''" />
|
||||
<NumInput v-model="form.pageChangeAnimationSpeed" bg-color="input" class="col-left" :min="0" :max="100" :disable="form.pageChangeAnimation == ''" />
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<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
|
||||
/>
|
||||
@@ -37,13 +38,13 @@
|
||||
</div>
|
||||
<div class="sets-item row">
|
||||
<div class="sets-label label"></div>
|
||||
<q-btn class="sets-button" dense no-caps @click="addProfile">
|
||||
<q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="addProfile">
|
||||
Добавить
|
||||
</q-btn>
|
||||
<q-btn class="sets-button" dense no-caps @click="delProfile">
|
||||
<q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="delProfile">
|
||||
Удалить
|
||||
</q-btn>
|
||||
<q-btn class="sets-button" dense no-caps @click="delAllProfiles">
|
||||
<q-btn class="sets-button" color="btn2" text-color="app" dense no-caps @click="delAllProfiles">
|
||||
Удалить все
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -63,7 +64,7 @@
|
||||
|
||||
<div class="sets-item row">
|
||||
<div class="sets-label label"></div>
|
||||
<q-btn class="sets-button" style="width: 250px" dense no-caps @click="showServerStorageKey">
|
||||
<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>
|
||||
ключ доступа
|
||||
@@ -104,13 +105,13 @@
|
||||
|
||||
<div class="sets-item row">
|
||||
<div class="sets-label label"></div>
|
||||
<q-btn class="sets-button" style="width: 250px" dense no-caps @click="enterServerStorageKey">
|
||||
<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" style="width: 250px" dense no-caps @click="generateServerStorageKey">
|
||||
<q-btn class="sets-button" color="btn2" text-color="app" style="width: 250px" dense no-caps @click="generateServerStorageKey">
|
||||
Сгенерировать новый ключ
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -357,6 +358,6 @@ export default vueComponent(ProfilesTab);
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
color: var(--text-anchor-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="fit sets-tab-panel">
|
||||
<div class="sets-item row">
|
||||
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">
|
||||
<q-btn class="col q-ma-sm" color="btn2" text-color="app" dense no-caps @click="setDefaults">
|
||||
Установить по умолчанию
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
<q-tabs
|
||||
ref="tabs"
|
||||
v-model="selectedTab"
|
||||
class="bg-grey-3 text-grey-9"
|
||||
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
|
||||
@@ -35,7 +35,7 @@
|
||||
<!-- Профили --------------------------------------------------------------------->
|
||||
<ProfilesTab v-if="selectedTab == 'profiles'" :form="form" />
|
||||
<!-- Вид ------------------------------------------------------------------------->
|
||||
<ViewTab v-if="selectedTab == 'view'" :form="form" />
|
||||
<ViewTab v-if="selectedTab == 'view'" :form="form" @tab-event="tabEvent" />
|
||||
<!-- Кнопки ---------------------------------------------------------------------->
|
||||
<ToolBarTab v-if="selectedTab == 'toolbar'" :form="form" />
|
||||
<!-- Управление ------------------------------------------------------------------>
|
||||
@@ -178,6 +178,7 @@ class SettingsPage {
|
||||
|
||||
switch (event.action) {
|
||||
case 'set-defaults': this.setDefaults(); break;
|
||||
case 'night-mode': this.$emit('do-action', {action: 'nightMode'}); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
Разница размеров
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.bucSizeDiff" style="width: 200px" />
|
||||
<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>
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="sets-label label"></div>
|
||||
<div class="col-4"></div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.bucCancelDays" :min="1" :max="10000" />
|
||||
<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>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<q-input
|
||||
v-model="textColorFiltered"
|
||||
class="col-left no-mp"
|
||||
bg-color="input"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
@@ -43,6 +44,7 @@
|
||||
<q-input
|
||||
v-model="bgColorFiltered"
|
||||
class="col-left no-mp"
|
||||
bg-color="input"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
@@ -71,6 +73,7 @@
|
||||
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
|
||||
>
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-select
|
||||
v-model="form.fontName" class="col-left" :options="fontsOptions" :disable="form.webFontName != ''"
|
||||
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" :options="webFontsOptions"
|
||||
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
|
||||
>
|
||||
@@ -36,7 +36,7 @@
|
||||
Размер
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.fontSize" class="col-left" :min="5" :max="200" />
|
||||
<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>
|
||||
@@ -49,7 +49,7 @@
|
||||
Сдвиг
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="vertShift" class="col-left" :min="-100" :max="100">
|
||||
<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>
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
Режим
|
||||
</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">
|
||||
@@ -20,13 +27,13 @@
|
||||
Отступ границ
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.indentLR" class="col-left" :min="0" :max="2000">
|
||||
<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" class="col" :min="0" :max="2000">
|
||||
<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>
|
||||
@@ -39,7 +46,7 @@
|
||||
Отступ внутри
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.dualIndentLR" class="col-left" :min="0" :max="2000">
|
||||
<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>
|
||||
@@ -60,6 +67,7 @@
|
||||
<q-input
|
||||
v-model="dualDivColorFiltered"
|
||||
class="col-left no-mp"
|
||||
bg-color="input"
|
||||
outlined dense
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
@@ -89,7 +97,7 @@
|
||||
Прозрачность
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.dualDivColorAlpha" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
|
||||
<NumInput v-model="form.dualDivColorAlpha" bg-color="input" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +106,7 @@
|
||||
Ширина (px)
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.dualDivWidth" class="col-left" :min="0" :max="100">
|
||||
<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>
|
||||
@@ -111,7 +119,7 @@
|
||||
Высота (%)
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.dualDivHeight" class="col-left" :min="0" :max="100">
|
||||
<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>
|
||||
@@ -124,13 +132,13 @@
|
||||
Пунктир
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.dualDivStrokeFill" class="col-left" :min="0" :max="2000">
|
||||
<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" class="col" :min="0" :max="2000">
|
||||
<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>
|
||||
@@ -143,7 +151,7 @@
|
||||
Ширина тени
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.dualDivShadowWidth" class="col-left" :min="0" :max="100" />
|
||||
<NumInput v-model="form.dualDivShadowWidth" bg-color="input" class="col-left" :min="0" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,6 +193,7 @@ class Mode {
|
||||
|
||||
isFormChanged = false;
|
||||
dualDivColorFiltered = '';
|
||||
nightMode = false;
|
||||
|
||||
created() {
|
||||
this.formChanged();//no await
|
||||
@@ -202,11 +211,17 @@ class Mode {
|
||||
&& (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);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<q-input
|
||||
v-model="statusBarColorFiltered"
|
||||
class="col-left no-mp"
|
||||
bg-color="input"
|
||||
outlined dense
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
@@ -52,7 +53,7 @@
|
||||
Прозрачность
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.statusBarColorAlpha" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
|
||||
<NumInput v-model="form.statusBarColorAlpha" bg-color="input" class="col-left" :min="0" :max="1" :digits="2" :step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +62,7 @@
|
||||
Высота
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.statusBarHeight" class="col-left" :min="5" :max="100" />
|
||||
<NumInput v-model="form.statusBarHeight" bg-color="input" class="col-left" :min="5" :max="100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Интервал
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.lineInterval" class="col-left" :min="0" :max="200" />
|
||||
<NumInput v-model="form.lineInterval" bg-color="input" class="col-left" :min="0" :max="200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
Параграф
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.p" class="col-left" :min="0" :max="2000" />
|
||||
<NumInput v-model="form.p" bg-color="input" class="col-left" :min="0" :max="2000" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
Сдвиг
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.textVertShift" class="col-left" :min="-100" :max="100">
|
||||
<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>
|
||||
@@ -43,7 +43,7 @@
|
||||
Скроллинг
|
||||
</div>
|
||||
<div class="col row">
|
||||
<NumInput v-model="form.scrollingDelay" class="col-left" :min="1" :max="10000">
|
||||
<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>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<div class="q-px-sm" />
|
||||
<q-select
|
||||
v-model="form.scrollingType" class="col" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
|
||||
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
|
||||
>
|
||||
@@ -81,7 +81,7 @@
|
||||
Компактность
|
||||
</div>
|
||||
<div class="q-px-sm" />
|
||||
<NumInput v-model="form.compactTextPerc" class="col" :min="0" :max="100">
|
||||
<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>
|
||||
@@ -105,7 +105,7 @@
|
||||
Добавлять пустые
|
||||
</div>
|
||||
<div class="q-px-sm" />
|
||||
<NumInput v-model="form.addEmptyParagraphs" class="col" :min="0" :max="2" />
|
||||
<NumInput v-model="form.addEmptyParagraphs" bg-color="input" class="col" :min="0" :max="2" />
|
||||
</div>
|
||||
|
||||
<div class="sets-item row">
|
||||
@@ -135,7 +135,7 @@
|
||||
Высота не более
|
||||
</div>
|
||||
<div class="q-px-sm" />
|
||||
<NumInput v-model="form.imageHeightLines" class="col" :min="1" :max="100" :disable="!form.showImages">
|
||||
<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>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div class="fit column">
|
||||
<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
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
class="no-mp bg-menu-2 text-menu"
|
||||
>
|
||||
<q-tab name="mode" label="Режим" />
|
||||
<q-tab name="color" label="Цвет" />
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="q-mb-sm" />
|
||||
|
||||
<div class="col sets-tab-panel">
|
||||
<Mode v-if="selectedTab == 'mode'" :form="form" />
|
||||
<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" />
|
||||
@@ -61,6 +61,14 @@ class ViewTab {
|
||||
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);
|
||||
|
||||
@@ -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>' : '');
|
||||
|
||||
@@ -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() {
|
||||
@@ -297,6 +337,8 @@ class TextPage {
|
||||
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
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';
|
||||
|
||||
@@ -433,10 +475,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;
|
||||
@@ -460,8 +498,6 @@ class TextPage {
|
||||
await this.calcPropsAndLoadFonts();
|
||||
|
||||
this.refreshTime();
|
||||
if (this.lazyParseEnabled)
|
||||
this.lazyParsePara();
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
@@ -838,36 +874,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;
|
||||
@@ -949,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});
|
||||
}
|
||||
@@ -1052,6 +1074,7 @@ class TextPage {
|
||||
if (this.startTouch) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onTouchEnd(event) {
|
||||
@@ -1136,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) {
|
||||
@@ -1143,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) {
|
||||
@@ -1245,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);
|
||||
@@ -1280,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>
|
||||
@@ -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 == 'section') {
|
||||
sectionLevel--;
|
||||
}
|
||||
if (tag == 'title') {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
inTitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`</${tag}>`, 0);
|
||||
}
|
||||
if (tag == 'section') {
|
||||
sectionLevel--;
|
||||
}
|
||||
|
||||
if (tag == 'p') {
|
||||
inPara = false;
|
||||
}
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`</${tag}>`, 0);
|
||||
}
|
||||
|
||||
if (tag == 'subtitle') {
|
||||
isFirstTitlePara = false;
|
||||
bold = false;
|
||||
center = false;
|
||||
inSubtitle = false;
|
||||
}
|
||||
if (tag == 'p') {
|
||||
inPara = 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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -1,4 +1,138 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
version: '1.2.8',
|
||||
releaseDate: '2025-06-04',
|
||||
showUntil: '2025-06-03',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.2.7',
|
||||
releaseDate: '2025-02-22',
|
||||
showUntil: '2025-02-21',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>отключена форма для сбора донатов</li>
|
||||
<li>мелкие оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.2.6',
|
||||
releaseDate: '2024-10-03',
|
||||
showUntil: '2024-10-02',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления из-за нарушения авторских прав</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.2.4',
|
||||
releaseDate: '2024-08-27',
|
||||
showUntil: '2024-08-26',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.2.3',
|
||||
releaseDate: '2024-08-02',
|
||||
showUntil: '2024-08-01',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.2.2',
|
||||
releaseDate: '2024-07-28',
|
||||
showUntil: '2024-07-27',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено отображение примечаний на месте, по клику на сноске (#50)</li>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.2.0',
|
||||
releaseDate: '2024-03-25',
|
||||
showUntil: '2024-03-24',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в списке загруженных, книга в архив (из архива) переносится теперь со всей группой своих версий</li>
|
||||
<li>добавлена возможность задавать в конфиге любую ссылку для кнопки "Сетевая библиотека", параметр networkLibraryLink (#47)</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.1.3',
|
||||
releaseDate: '2023-02-06',
|
||||
showUntil: '2023-02-05',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.1.2',
|
||||
releaseDate: '2023-01-22',
|
||||
showUntil: '2023-01-21',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.1.1',
|
||||
releaseDate: '2023-01-11',
|
||||
showUntil: '2023-01-15',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена опция "Ночной режим" и кнопка на панель</li>
|
||||
<li>исправление багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '1.0.0',
|
||||
releaseDate: '2022-12-18',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
outlined dense
|
||||
input-style="text-align: center"
|
||||
class="no-mp"
|
||||
:class="(error ? 'error' : '')"
|
||||
:disable="disable"
|
||||
:mask="mask"
|
||||
:error="error"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #prepend>
|
||||
@@ -236,23 +236,16 @@ export default vueComponent(NumInput);
|
||||
border-radius: 15px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
color: #bbb;
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -148,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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createStore } from 'vuex';
|
||||
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';
|
||||
|
||||
@@ -13,7 +12,6 @@ const vuexLocal = new VuexPersistence();
|
||||
|
||||
export default createStore(Object.assign({}, root, {
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
reader,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import miscApi from '../../api/misc';
|
||||
// initial state
|
||||
const state = {
|
||||
name: null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import * as utils from '../../share/utils';
|
||||
import googleFonts from './fonts/fonts.json';
|
||||
|
||||
@@ -21,6 +22,7 @@ const readerActions = {
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'convOptions': 'Настроить конвертирование',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'nightMode': 'Ночной режим',
|
||||
'clickControl': 'Управление кликом',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
@@ -57,6 +59,7 @@ const toolButtons = [
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
{name: 'recentBooks', show: true},
|
||||
{name: 'nightMode', show: true},
|
||||
{name: 'clickControl', show: true},
|
||||
{name: 'offlineMode', show: true},
|
||||
];
|
||||
@@ -80,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']},
|
||||
|
||||
@@ -157,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
|
||||
|
||||
@@ -164,7 +172,6 @@ const settingDefaults = {
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
|
||||
allowUrlParamBookPos: false,
|
||||
lazyParseEnabled: false,
|
||||
copyFullText: false,
|
||||
showClickMapPage: true,
|
||||
clickControl: true,
|
||||
@@ -218,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});
|
||||
@@ -228,6 +237,33 @@ function addDefaultsToSettings(settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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: '',
|
||||
@@ -287,9 +323,9 @@ const state = {
|
||||
whatsNewContentHash: '',
|
||||
donationNextPopup: Date.now() + dayMs*30,
|
||||
currentProfile: '',
|
||||
settings: Object.assign({}, settingDefaults),
|
||||
settings: _.cloneDeep(settingDefaults),
|
||||
settingsRev: {},
|
||||
libs: false,
|
||||
libs: {},
|
||||
libsRev: 0,
|
||||
};
|
||||
|
||||
@@ -332,13 +368,31 @@ const mutations = {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
8653
package-lock.json
generated
8653
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
68
package.json
68
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "liberama",
|
||||
"version": "1.0.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user