Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d806a07c60 | ||
|
|
c0ea096f1f | ||
|
|
011d4a1672 | ||
|
|
4836a737c6 | ||
|
|
5712b2ee17 | ||
|
|
32dd17694e | ||
|
|
3ebc932a6a | ||
|
|
8f351d9bef | ||
|
|
5ae3ea94e4 | ||
|
|
f203d453a4 | ||
|
|
0d5cba121b | ||
|
|
0cd6a48a46 | ||
|
|
4e07ce2b5c | ||
|
|
85a525e301 | ||
|
|
03e4a6d723 | ||
|
|
ab28af1abe | ||
|
|
7fceed5301 | ||
|
|
0077816afa | ||
|
|
cb01423147 | ||
|
|
61b0712d36 | ||
|
|
12d7843377 | ||
|
|
9293c0a0d4 | ||
|
|
bb9522197a | ||
|
|
450a2e0664 | ||
|
|
41e35f3ec8 | ||
|
|
a9bc98abe3 | ||
|
|
47bca03532 | ||
|
|
942021371c | ||
|
|
ea2f178730 | ||
|
|
4b5c8d9efe | ||
|
|
28ebf13c3a | ||
|
|
5d52e63dd9 | ||
|
|
1a0e024050 | ||
|
|
e627a0d970 | ||
|
|
48668d94ad | ||
|
|
e08c431dd9 | ||
|
|
5ee58ad6f0 | ||
|
|
ac0a4f0586 | ||
|
|
b6f4c153e5 | ||
|
|
4fdaf5f555 | ||
|
|
b4ee9d6c00 | ||
|
|
7c73c74730 | ||
|
|
c20aa089fa | ||
|
|
b0e15c22ea | ||
|
|
d58a2c065a | ||
|
|
53135e7ee8 | ||
|
|
5c48ca9e6c | ||
|
|
c4a280f3d8 | ||
|
|
ba2943c722 | ||
|
|
26f6ffc83a | ||
|
|
bcf075a72c | ||
|
|
02d458d192 | ||
|
|
a349d8af68 | ||
|
|
0dbaf32aac | ||
|
|
e8c41ef3a8 | ||
|
|
e43a44e986 | ||
|
|
f14b8ed277 | ||
|
|
bbfe8a64cb | ||
|
|
bcf3c2dab0 | ||
|
|
d5404fd260 | ||
|
|
54bc662e43 | ||
|
|
42546ca97e | ||
|
|
5c13cf0eb9 | ||
|
|
2a9d44ae9a | ||
|
|
38414ae7b6 | ||
|
|
3ecb3e80ac | ||
|
|
4968828488 | ||
|
|
4db3cd24df | ||
|
|
45c6d3da77 | ||
|
|
4aab1da3c6 | ||
|
|
bf5dfa1c15 | ||
|
|
7549bdd2b4 | ||
|
|
1bb2525ab2 | ||
|
|
22a556f612 | ||
|
|
056611e87c | ||
|
|
6debe24880 | ||
|
|
56559bddab | ||
|
|
9ec74eccb4 | ||
|
|
3d2f45c20d | ||
|
|
fb2eedd5ba | ||
|
|
e278b4a00e | ||
|
|
0beaa611f6 | ||
|
|
14ca2daa39 | ||
|
|
714eb3ae83 | ||
|
|
6286d663c9 | ||
|
|
b5db2079d2 | ||
|
|
b3b30b9bd9 | ||
|
|
0b6a726503 | ||
|
|
609334c5a6 | ||
|
|
4852c7aec3 | ||
|
|
b1e3d33694 | ||
|
|
2bfc557071 | ||
|
|
e1216109bc | ||
|
|
990b8f390c |
@@ -12,6 +12,7 @@
|
|||||||
"@babel"
|
"@babel"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
"es6": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"vue/max-attributes-per-line": "off",
|
"vue/max-attributes-per-line": "off",
|
||||||
"vue/html-self-closing": "off",
|
"vue/html-self-closing": "off",
|
||||||
"vue/no-v-html": "off",
|
"vue/no-v-html": "off",
|
||||||
|
"vue/no-v-model-argument": "off",
|
||||||
|
|
||||||
"strict": 0,
|
"strict": 0,
|
||||||
"indent": [0, 4, {
|
"indent": [0, 4, {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
@@ -29,7 +29,8 @@ async function main() {
|
|||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
@@ -46,7 +47,8 @@ async function main() {
|
|||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_linux-amd64.tar.gz';
|
||||||
|
|
||||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.tar.gz`));
|
||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ const { VueLoaderPlugin } = require('vue-loader');
|
|||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/*resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
vue: '@vue/compat'
|
ws: false,
|
||||||
|
//vue: '@vue/compat'
|
||||||
}
|
}
|
||||||
},*/
|
},
|
||||||
entry: [`${clientDir}/main.js`],
|
entry: [`${clientDir}/main.js`],
|
||||||
output: {
|
output: {
|
||||||
publicPath: '/app/',
|
publicPath: '/app/',
|
||||||
@@ -30,7 +31,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
resourceQuery: /^\?vue/,
|
resourceQuery: /^\?vue/,
|
||||||
use: path.resolve('build/includer.js')
|
use: path.resolve(__dirname, 'includer.js')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
@@ -62,34 +63,6 @@ module.exports = {
|
|||||||
filename: 'fonts/[name]-[hash:6][ext]'
|
filename: 'fonts/[name]-[hash:6][ext]'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*{
|
|
||||||
test: /\.gif$/,
|
|
||||||
loader: "url-loader",
|
|
||||||
options: {
|
|
||||||
name: "images/[name]-[hash:6].[ext]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.png$/,
|
|
||||||
loader: "url-loader",
|
|
||||||
options: {
|
|
||||||
name: "images/[name]-[hash:6].[ext]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.jpg$/,
|
|
||||||
loader: "file-loader",
|
|
||||||
options: {
|
|
||||||
name: "images/[name]-[hash:6].[ext]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(ttf|eot|woff|woff2)$/,
|
|
||||||
loader: "file-loader",
|
|
||||||
options: {
|
|
||||||
name: "fonts/[name]-[hash:6].[ext]"
|
|
||||||
}
|
|
||||||
},*/
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const util = require('util');
|
|||||||
const stream = require('stream');
|
const stream = require('stream');
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
@@ -29,7 +29,8 @@ async function main() {
|
|||||||
|
|
||||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||||
await pipeline(got.stream(sqliteRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
const res = await axios.get(sqliteRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/sqlite.tar.gz`));
|
||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
@@ -46,7 +47,8 @@ async function main() {
|
|||||||
// Скачиваем ipfs
|
// Скачиваем ipfs
|
||||||
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
const ipfsRemoteUrl = 'https://dist.ipfs.io/go-ipfs/v0.4.18/go-ipfs_v0.4.18_windows-amd64.zip';
|
||||||
|
|
||||||
await pipeline(got.stream(ipfsRemoteUrl), fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
const res = await axios.get(ipfsRemoteUrl, {responseType: 'stream'})
|
||||||
|
await pipeline(res.data, fs.createWriteStream(`${tempDownloadDir}/ipfs.zip`));
|
||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Misc {
|
|||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
|
|
||||||
const query = {params: [
|
const query = {params: [
|
||||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
|
||||||
]};
|
]};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ class Reader {
|
|||||||
const state = response.state;
|
const state = response.state;
|
||||||
if (!state)
|
if (!state)
|
||||||
throw new Error('Неверный ответ api');
|
throw new Error('Неверный ответ api');
|
||||||
if (response.state == 'error') {
|
if (state == 'error') {
|
||||||
throw new Error(response.error);
|
throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,16 +55,16 @@
|
|||||||
|
|
||||||
<div class="col fit tree">
|
<div class="col fit tree">
|
||||||
<div v-show="nodes.length" class="checkbox-tick-all">
|
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||||
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @input="makeTickAll" />
|
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
|
||||||
</div>
|
</div>
|
||||||
<q-tree
|
<q-tree
|
||||||
|
v-model:selected="selected"
|
||||||
|
v-model:ticked="ticked"
|
||||||
|
v-model:expanded="expanded"
|
||||||
class="q-my-xs"
|
class="q-my-xs"
|
||||||
:nodes="nodes"
|
:nodes="nodes"
|
||||||
node-key="key"
|
node-key="key"
|
||||||
tick-strategy="leaf"
|
tick-strategy="leaf"
|
||||||
v-model:selected="selected"
|
|
||||||
v-model:ticked="ticked"
|
|
||||||
v-model:expanded="expanded"
|
|
||||||
selected-color="black"
|
selected-color="black"
|
||||||
:filter="search"
|
:filter="search"
|
||||||
no-nodes-label="Закладок пока нет"
|
no-nodes-label="Закладок пока нет"
|
||||||
@@ -97,7 +97,7 @@ const componentOptions = {
|
|||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ticked: function() {
|
ticked() {
|
||||||
this.checkAllTicked();
|
this.checkAllTicked();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
class="col q-mr-sm"
|
class="col q-mr-sm"
|
||||||
rounded outlined dense
|
rounded outlined dense
|
||||||
bg-color="white"
|
bg-color="white"
|
||||||
placeholder="Скопируйте сюда URL книги"
|
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
|
||||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="address">
|
<!--div class="address">
|
||||||
<img class="logo" src="./assets/paypal.png">
|
<img class="logo" src="./assets/paypal.png">
|
||||||
<div class="para">
|
<div class="para">
|
||||||
{{ paypalAddress }}
|
{{ paypalAddress }}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div-->
|
||||||
|
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/bitcoin.png">
|
<img class="logo" src="./assets/bitcoin.png">
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
|||||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||||
|
|
||||||
const pages = {
|
const pages = {
|
||||||
'CommonHelpPage': CommonHelpPage,
|
'CommonHelpPage': CommonHelpPage,
|
||||||
'HotkeysHelpPage': HotkeysHelpPage,
|
'HotkeysHelpPage': HotkeysHelpPage,
|
||||||
'MouseHelpPage': MouseHelpPage,
|
'MouseHelpPage': MouseHelpPage,
|
||||||
'VersionHistoryPage': VersionHistoryPage,
|
'VersionHistoryPage': VersionHistoryPage,
|
||||||
'DonateHelpPage': DonateHelpPage,
|
//'DonateHelpPage': DonateHelpPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@@ -44,7 +44,7 @@ const tabs = [
|
|||||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||||
['HotkeysHelpPage', 'Клавиатура'],
|
['HotkeysHelpPage', 'Клавиатура'],
|
||||||
['VersionHistoryPage', 'История версий'],
|
['VersionHistoryPage', 'История версий'],
|
||||||
['DonateHelpPage', 'Помочь проекту'],
|
//['DonateHelpPage', 'Помочь проекту'],
|
||||||
];
|
];
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
@@ -73,7 +73,7 @@ class HelpPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activateDonateHelpPage() {
|
activateDonateHelpPage() {
|
||||||
this.selectedTab = 'DonateHelpPage';
|
//this.selectedTab = 'DonateHelpPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
activateVersionHistoryHelpPage() {
|
activateVersionHistoryHelpPage() {
|
||||||
|
|||||||
@@ -33,14 +33,15 @@ class VersionHistoryPage {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
let vh = [];
|
let vh = [];
|
||||||
for (const version of versionHistory) {
|
for (const v of versionHistory) {
|
||||||
vh.push(version.header);
|
vh.push(`${v.version} (${v.releaseDate})`);
|
||||||
}
|
}
|
||||||
this.versionHeader = vh;
|
this.versionHeader = vh;
|
||||||
|
|
||||||
let vc = [];
|
let vc = [];
|
||||||
for (const version of versionHistory) {
|
for (const v of versionHistory) {
|
||||||
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
let header = `${v.version} (${v.releaseDate})`;
|
||||||
|
vc.push({key: header, content: 'Версия ' + header + v.content});
|
||||||
}
|
}
|
||||||
this.versionContent = vc;
|
this.versionContent = vc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,21 +12,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
<q-input ref="input" v-model="bookUrl" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" placeholder="URL книги" @keydown="onInputKeydown">
|
<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"
|
||||||
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<input id="file" ref="file" type="file" style="display: none;" @change="loadFile" />
|
<input
|
||||||
|
id="file" ref="file" type="file"
|
||||||
|
style="display: none;"
|
||||||
|
:accept="acceptFileExt"
|
||||||
|
@change="loadFile"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||||
|
<q-icon class="q-mr-xs" name="la la-caret-square-up" size="24px" />
|
||||||
Загрузить файл с диска
|
Загрузить файл с диска
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||||
|
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||||
Из буфера обмена
|
Из буфера обмена
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
@@ -45,14 +55,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||||
|
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
|
||||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
|
||||||
|
|
||||||
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||||
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||||
|
|
||||||
|
<Dialog ref="dialog1" v-model="findBookVisible">
|
||||||
|
<template #header>
|
||||||
|
Подсказка ;-)
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Если вы хотите найти определенную книгу, добро пожаловать в
|
||||||
|
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
|
||||||
|
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,12 +85,15 @@ import vueComponent from '../../vueComponent.js';
|
|||||||
|
|
||||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||||
|
|
||||||
|
import Dialog from '../../share/Dialog.vue';
|
||||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||||
import {versionHistory} from '../versionHistory';
|
import {versionHistory} from '../versionHistory';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const componentOptions = {
|
const componentOptions = {
|
||||||
components: {
|
components: {
|
||||||
GithubCorner,
|
GithubCorner,
|
||||||
|
Dialog,
|
||||||
PasteTextPage,
|
PasteTextPage,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -77,6 +103,7 @@ class LoaderPage {
|
|||||||
bookUrl = null;
|
bookUrl = null;
|
||||||
loadPercent = 0;
|
loadPercent = 0;
|
||||||
pasteTextActive = false;
|
pasteTextActive = false;
|
||||||
|
findBookVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -109,14 +136,16 @@ class LoaderPage {
|
|||||||
return this.$store.state.config.version;
|
return this.$store.state.config.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get acceptFileExt() {
|
||||||
|
return this.$store.state.config.acceptFileExt;
|
||||||
|
}
|
||||||
|
|
||||||
get isExternalConverter() {
|
get isExternalConverter() {
|
||||||
return this.$store.state.config.useExternalBookConverter;
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
get clientVersion() {
|
get clientVersion() {
|
||||||
let v = versionHistory[0].header;
|
return versionHistory[0].version;
|
||||||
v = v.split(' ')[0];
|
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submitUrl() {
|
submitUrl() {
|
||||||
@@ -138,7 +167,7 @@ class LoaderPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadBufferClick() {
|
loadBufferClick() {
|
||||||
this.pasteTextToggle();
|
this.showPasteText();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBuffer(opts) {
|
loadBuffer(opts) {
|
||||||
@@ -148,6 +177,10 @@ class LoaderPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showPasteText() {
|
||||||
|
this.pasteTextActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
pasteTextToggle() {
|
pasteTextToggle() {
|
||||||
this.pasteTextActive = !this.pasteTextActive;
|
this.pasteTextActive = !this.pasteTextActive;
|
||||||
}
|
}
|
||||||
@@ -160,6 +193,10 @@ class LoaderPage {
|
|||||||
this.$emit('do-action', {action: 'donate'});
|
this.$emit('do-action', {action: 'donate'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findBook() {
|
||||||
|
this.findBookVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
openComments() {
|
openComments() {
|
||||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||||
}
|
}
|
||||||
@@ -168,26 +205,24 @@ class LoaderPage {
|
|||||||
window.open('http://old.omnireader.ru', '_blank');
|
window.open('http://old.omnireader.ru', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputKeydown(event) {
|
async onInputKeydown(event) {
|
||||||
if (event.key == 'Enter') {
|
if (event.key == 'Enter') {
|
||||||
|
await utils.sleep(100);
|
||||||
this.submitUrl();
|
this.submitUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
|
if (this.$refs.dialog1.active)
|
||||||
|
return true;
|
||||||
|
|
||||||
if (this.pasteTextActive) {
|
if (this.pasteTextActive) {
|
||||||
return this.$refs.pasteTextPage.keyHook(event);
|
return this.$refs.pasteTextPage.keyHook(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = this.$refs.input.getNativeElement();
|
const input = this.$refs.input.getNativeElement();
|
||||||
if (event.type == 'keydown' && document.activeElement !== input) {
|
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
|
||||||
const action = this.$root.readerActionByKeyEvent(event);
|
return true;
|
||||||
switch (action) {
|
|
||||||
case 'help':
|
|
||||||
this.openHelp(event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,35 @@
|
|||||||
<div class="column no-wrap">
|
<div class="column no-wrap">
|
||||||
<div v-show="toolBarActive" ref="header" class="header">
|
<div v-show="toolBarActive" ref="header" class="header">
|
||||||
<div ref="buttons" class="row justify-between no-wrap">
|
<div ref="buttons" class="row justify-between no-wrap">
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
||||||
<q-icon name="la la-arrow-left" size="32px" />
|
<q-icon name="la la-arrow-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
{{ rstore.readerActions['loader'] }}
|
{{ rstore.readerActions['loader'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
|
||||||
|
<q-icon name="la la-caret-square-up" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['loadFile'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
|
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
|
||||||
|
<q-icon name="la la-comment" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['loadBuffer'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
|
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
|
||||||
|
<q-icon name="la la-question" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['help'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
|
<div class="space"></div>
|
||||||
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
||||||
<q-icon name="la la-angle-left" size="32px" />
|
<q-icon name="la la-angle-left" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -86,9 +105,16 @@
|
|||||||
{{ rstore.readerActions['recentBooks'] }}
|
{{ rstore.readerActions['recentBooks'] }}
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="space"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="row no-wrap">
|
||||||
|
<button v-show="showToolButton['clickControl']" ref="clickControl" v-ripple class="tool-button" :class="buttonActiveClass('clickControl')" @click="buttonClick('clickControl')">
|
||||||
|
<q-icon name="la la-mouse" size="32px" />
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
|
{{ rstore.readerActions['clickControl'] }}
|
||||||
|
</q-tooltip>
|
||||||
|
</button>
|
||||||
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
||||||
<q-icon name="la la-unlink" size="32px" />
|
<q-icon name="la la-unlink" size="32px" />
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||||
@@ -136,7 +162,7 @@
|
|||||||
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||||
|
|
||||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -245,6 +271,8 @@ class Reader {
|
|||||||
rstore = {};
|
rstore = {};
|
||||||
|
|
||||||
loaderActive = false;
|
loaderActive = false;
|
||||||
|
loadFileActive = false;
|
||||||
|
loadBufferActive = false;
|
||||||
fullScreenActive = false;
|
fullScreenActive = false;
|
||||||
setPositionActive = false;
|
setPositionActive = false;
|
||||||
searchActive = false;
|
searchActive = false;
|
||||||
@@ -254,6 +282,7 @@ class Reader {
|
|||||||
contentsActive = false;
|
contentsActive = false;
|
||||||
libsActive = false;
|
libsActive = false;
|
||||||
recentBooksActive = false;
|
recentBooksActive = false;
|
||||||
|
clickControlActive = false;
|
||||||
offlineModeActive = false;
|
offlineModeActive = false;
|
||||||
settingsActive = false;
|
settingsActive = false;
|
||||||
|
|
||||||
@@ -310,7 +339,7 @@ class Reader {
|
|||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.paramPosIgnore = false;
|
this.paramPosIgnore = false;
|
||||||
}
|
}
|
||||||
}, 500, {maxWait: 5000});
|
}, 250, {maxWait: 5000});
|
||||||
|
|
||||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||||
this.debouncedSetRecentBook(newValue);
|
this.debouncedSetRecentBook(newValue);
|
||||||
@@ -324,8 +353,6 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateHeaderMinWidth();
|
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
await wallpaperStorage.init();
|
await wallpaperStorage.init();
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
@@ -372,6 +399,7 @@ class Reader {
|
|||||||
this.copyFullText = settings.copyFullText;
|
this.copyFullText = settings.copyFullText;
|
||||||
this.showClickMapPage = settings.showClickMapPage;
|
this.showClickMapPage = settings.showClickMapPage;
|
||||||
this.clickControl = settings.clickControl;
|
this.clickControl = settings.clickControl;
|
||||||
|
this.clickControlActive = this.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
this.enableSitesFilter = settings.enableSitesFilter;
|
this.enableSitesFilter = settings.enableSitesFilter;
|
||||||
@@ -388,11 +416,26 @@ class Reader {
|
|||||||
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateHeaderMinWidth();
|
|
||||||
|
|
||||||
this.loadWallpapers();//no await
|
this.loadWallpapers();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showHelpOnErrorIfNeeded(errorMessage) {
|
||||||
|
//небольшая эвристика
|
||||||
|
let i = errorMessage.indexOf('http://');
|
||||||
|
if (i < 0)
|
||||||
|
i = errorMessage.indexOf('https://');
|
||||||
|
|
||||||
|
errorMessage = errorMessage.substring(i + 7);
|
||||||
|
const perCount = errorMessage.split('%').length - 1;
|
||||||
|
|
||||||
|
if (perCount > errorMessage.length/3.2) {
|
||||||
|
this.$refs.dialogs.showUrlHelp();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
//wallpaper css
|
//wallpaper css
|
||||||
async loadWallpapers() {
|
async loadWallpapers() {
|
||||||
const wallpaperDataLength = await wallpaperStorage.getLength();
|
const wallpaperDataLength = await wallpaperStorage.getLength();
|
||||||
@@ -439,17 +482,6 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderMinWidth() {
|
|
||||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
|
||||||
if (this.$refs.buttons)
|
|
||||||
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
|
|
||||||
(async() => {
|
|
||||||
await utils.sleep(1000);
|
|
||||||
if (this.$refs.header)
|
|
||||||
this.$refs.header.style.overflowX = 'auto';
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkSetStorageAccessKey() {
|
checkSetStorageAccessKey() {
|
||||||
const q = this.$route.query;
|
const q = this.$route.query;
|
||||||
|
|
||||||
@@ -525,9 +557,7 @@ class Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get clientVersion() {
|
get clientVersion() {
|
||||||
let v = versionHistory[0].header;
|
return versionHistory[0].version;
|
||||||
v = v.split(' ')[0];
|
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
@@ -585,7 +615,20 @@ class Reader {
|
|||||||
//сохранение в serverStorage
|
//сохранение в serverStorage
|
||||||
if (value) {
|
if (value) {
|
||||||
await utils.sleep(500);
|
await utils.sleep(500);
|
||||||
await this.$refs.serverStorage.saveRecent(value);
|
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
if (!this.offlineModeActive)
|
||||||
|
this.$root.notify.error('Таймаут соединения');
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$refs.serverStorage.saveRecent(value);
|
||||||
|
} catch (e) {
|
||||||
|
if (!this.offlineModeActive)
|
||||||
|
this.$root.notify.error(e.message);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -646,6 +689,28 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadFileToggle() {
|
||||||
|
if (!this.loaderActive)
|
||||||
|
this.loaderToggle();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const page = this.$refs.page;
|
||||||
|
if (this.activePage == 'LoaderPage' && page.loadFileClick) {
|
||||||
|
page.loadFileClick();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBufferToggle() {
|
||||||
|
if (!this.loaderActive)
|
||||||
|
this.loaderToggle();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const page = this.$refs.page;
|
||||||
|
if (this.activePage == 'LoaderPage' && page.showPasteText) {
|
||||||
|
page.showPasteText();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setPositionToggle() {
|
setPositionToggle() {
|
||||||
this.setPositionActive = !this.setPositionActive;
|
this.setPositionActive = !this.setPositionActive;
|
||||||
const page = this.$refs.page;
|
const page = this.$refs.page;
|
||||||
@@ -773,6 +838,12 @@ class Reader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickControlToggle() {
|
||||||
|
const newSettings = _.cloneDeep(this.settings);
|
||||||
|
newSettings.clickControl = !this.clickControl;
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
|
||||||
offlineModeToggle() {
|
offlineModeToggle() {
|
||||||
this.offlineModeActive = !this.offlineModeActive;
|
this.offlineModeActive = !this.offlineModeActive;
|
||||||
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
||||||
@@ -861,6 +932,9 @@ class Reader {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'loader':
|
case 'loader':
|
||||||
|
case 'loadFile':
|
||||||
|
case 'loadBuffer':
|
||||||
|
case 'help':
|
||||||
case 'fullScreen':
|
case 'fullScreen':
|
||||||
case 'setPosition':
|
case 'setPosition':
|
||||||
case 'search':
|
case 'search':
|
||||||
@@ -870,6 +944,7 @@ class Reader {
|
|||||||
case 'contents':
|
case 'contents':
|
||||||
case 'libs':
|
case 'libs':
|
||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
|
case 'clickControl':
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (this.progressActive) {
|
if (this.progressActive) {
|
||||||
@@ -1106,7 +1181,9 @@ class Reader {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
if (!this.showHelpOnErrorIfNeeded(e.message)) {
|
||||||
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.checkNewVersionAvailable();
|
this.checkNewVersionAvailable();
|
||||||
}
|
}
|
||||||
@@ -1172,6 +1249,12 @@ class Reader {
|
|||||||
case 'loader':
|
case 'loader':
|
||||||
this.loaderToggle();
|
this.loaderToggle();
|
||||||
break;
|
break;
|
||||||
|
case 'loadFile':
|
||||||
|
this.loadFileToggle();
|
||||||
|
break;
|
||||||
|
case 'loadBuffer':
|
||||||
|
this.loadBufferToggle();
|
||||||
|
break;
|
||||||
case 'help':
|
case 'help':
|
||||||
this.helpToggle();
|
this.helpToggle();
|
||||||
break;
|
break;
|
||||||
@@ -1214,6 +1297,9 @@ class Reader {
|
|||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
this.recentBooksToggle();
|
this.recentBooksToggle();
|
||||||
break;
|
break;
|
||||||
|
case 'clickControl':
|
||||||
|
this.clickControlToggle();
|
||||||
|
break;
|
||||||
case 'offlineMode':
|
case 'offlineMode':
|
||||||
this.offlineModeToggle();
|
this.offlineModeToggle();
|
||||||
break;
|
break;
|
||||||
@@ -1316,13 +1402,14 @@ class Reader {
|
|||||||
if (!result && event.type == 'keydown') {
|
if (!result && event.type == 'keydown') {
|
||||||
const action = this.$root.readerActionByKeyEvent(event);
|
const action = this.$root.readerActionByKeyEvent(event);
|
||||||
|
|
||||||
if (action == 'loader') {
|
/*if (action == 'loader') {
|
||||||
result = this.doAction({action, event});
|
result = this.doAction({action, event});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result && this.activePage == 'TextPage') {
|
if (!result && this.activePage == 'TextPage') {
|
||||||
result = this.doAction({action, event});
|
result = this.doAction({action, event});
|
||||||
}
|
}*/
|
||||||
|
result = this.doAction({action, event});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -1335,12 +1422,33 @@ export default vueComponent(Reader);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
.header {
|
||||||
|
height: 50px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
background-color: #1B695F;
|
background-color: #1B695F;
|
||||||
color: #000;
|
color: #000;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
height: 50px;
|
overflow-y: hidden;
|
||||||
|
scrollbar-color: #c49a60 #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-track {
|
||||||
|
background-color: #e4e4e4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #c49a60;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #e4e4e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #b48a50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
|||||||
@@ -5,12 +5,17 @@
|
|||||||
Что нового:
|
Что нового:
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
<div style="line-height: 20px; min-width: 300px">
|
||||||
|
<div v-html="whatsNewContent"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||||
<span slot="footer">
|
|
||||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
<template #footer>
|
||||||
</span>
|
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
|
||||||
|
Больше не показывать
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog ref="dialog2" v-model="donationVisible">
|
<Dialog ref="dialog2" v-model="donationVisible">
|
||||||
@@ -49,17 +54,40 @@
|
|||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
<div class="row justify-center">
|
<div class="row justify-center">
|
||||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
|
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
|
||||||
Помочь проекту
|
Помочь проекту
|
||||||
</q-btn>
|
</q-btn-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span slot="footer">
|
<template #footer>
|
||||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||||
<br>
|
<br>
|
||||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">
|
||||||
</span>
|
Напомнить позже
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
||||||
|
<template #header>
|
||||||
|
Обнаружена невалидная ссылка в поле "URL книги".
|
||||||
|
<br>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div style="word-break: normal">
|
||||||
|
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
|
||||||
|
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
|
||||||
|
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
Если же вы пытаетесь вставить текст в читалку из буфера обмена, пожалуйста воспользуйтесь кнопкой
|
||||||
|
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||||
|
<q-icon class="q-mr-xs" name="la la-comment" size="24px" />
|
||||||
|
Из буфера обмена
|
||||||
|
</q-btn>
|
||||||
|
на странице загрузки.
|
||||||
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,6 +116,7 @@ class ReaderDialogs {
|
|||||||
whatsNewVisible = false;
|
whatsNewVisible = false;
|
||||||
whatsNewContent = '';
|
whatsNewContent = '';
|
||||||
donationVisible = false;
|
donationVisible = false;
|
||||||
|
urlHelpVisible = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -112,9 +141,9 @@ class ReaderDialogs {
|
|||||||
const whatsNew = versionHistory[0];
|
const whatsNew = versionHistory[0];
|
||||||
if (this.showWhatsNewDialog &&
|
if (this.showWhatsNewDialog &&
|
||||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||||
whatsNew.header != this.whatsNewContentHash) {
|
this.whatsNewHeader != this.whatsNewContentHash) {
|
||||||
await utils.sleep(2000);
|
await utils.sleep(2000);
|
||||||
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
|
||||||
this.whatsNewVisible = true;
|
this.whatsNewVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,6 +157,15 @@ class ReaderDialogs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showUrlHelp() {
|
||||||
|
this.urlHelpVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBufferClick() {
|
||||||
|
this.$emit('load-buffer-toggle');
|
||||||
|
this.urlHelpVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
donationDialogDisable() {
|
donationDialogDisable() {
|
||||||
this.donationVisible = false;
|
this.donationVisible = false;
|
||||||
if (this.showDonationDialog2020) {
|
if (this.showDonationDialog2020) {
|
||||||
@@ -160,8 +198,11 @@ class ReaderDialogs {
|
|||||||
|
|
||||||
whatsNewDisable() {
|
whatsNewDisable() {
|
||||||
this.whatsNewVisible = false;
|
this.whatsNewVisible = false;
|
||||||
const whatsNew = versionHistory[0];
|
this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
|
||||||
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
}
|
||||||
|
|
||||||
|
get whatsNewHeader() {
|
||||||
|
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mode() {
|
get mode() {
|
||||||
@@ -181,7 +222,7 @@ class ReaderDialogs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyHook() {
|
keyHook() {
|
||||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
|
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ class ServerStorage {
|
|||||||
newRecentPatch.rev++;
|
newRecentPatch.rev++;
|
||||||
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
newRecentPatch.data[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||||
|
|
||||||
let applyMod = this.cachedRecentMod.data;
|
const applyMod = this.cachedRecentMod.data;
|
||||||
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||||
|
|
||||||
@@ -627,7 +627,7 @@ class ServerStorage {
|
|||||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
if (!recurse && itemKey) {
|
if (!recurse && itemKey) {
|
||||||
this.savingRecent = false;
|
this.savingRecent = false;
|
||||||
this.saveRecent(itemKey, true);
|
await this.saveRecent(itemKey, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (result.state == 'success') {
|
} else if (result.state == 'success') {
|
||||||
@@ -728,7 +728,7 @@ class ServerStorage {
|
|||||||
const ids = id.split('.');
|
const ids = id.split('.');
|
||||||
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||||
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||||
items[utils.fromBase58(ids[1])] = decoded;
|
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item row">
|
<!--div class="item row">
|
||||||
<div class="label-6">Уведомление</div>
|
<div class="label-6">Уведомление</div>
|
||||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||||
Показывать "Оплатим хостинг вместе"
|
Показывать "Оплатим хостинг вместе"
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
Показывать уведомление "Оплатим хостинг вместе"
|
Показывать уведомление "Оплатим хостинг вместе"
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</div>
|
</div-->
|
||||||
|
|
||||||
<!---------------------------------------------->
|
<!---------------------------------------------->
|
||||||
<div class="part-header">Другое</div>
|
<div class="part-header">Другое</div>
|
||||||
|
|||||||
@@ -6,29 +6,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||||
<div v-html="page1"></div>
|
<div @copy.prevent="copyText" v-html="page1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
||||||
<div v-html="page2"></div>
|
<div @copy.prevent="copyText" v-html="page2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showStatusBar" ref="statusBar" class="layout">
|
<div v-show="showStatusBar" ref="statusBar" class="layout">
|
||||||
<div v-html="statusBar"></div>
|
<div v-html="statusBar"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="clickControl" ref="layoutEvents" class="layout events"
|
<div
|
||||||
|
v-show="clickControl" ref="layoutEvents" class="layout events"
|
||||||
oncontextmenu="return false;"
|
oncontextmenu="return false;"
|
||||||
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||||
@wheel.prevent.stop="onMouseWheel"
|
@wheel.prevent.stop="onMouseWheel"
|
||||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||||
>
|
>
|
||||||
<div v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
<div
|
||||||
|
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"
|
@click.prevent.stop="onStatusBarClick"
|
||||||
v-html="statusBarClickable"
|
v-html="statusBarClickable"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
<div
|
||||||
|
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
||||||
@mousedown.prevent.stop @touchstart.stop
|
@mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"
|
@click.prevent.stop="onStatusBarClick"
|
||||||
v-html="statusBarClickable"
|
v-html="statusBarClickable"
|
||||||
@@ -46,6 +49,7 @@ import vueComponent from '../../vueComponent.js';
|
|||||||
|
|
||||||
import {loadCSS} from 'fg-loadcss';
|
import {loadCSS} from 'fg-loadcss';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
import './TextPage.css';
|
import './TextPage.css';
|
||||||
|
|
||||||
@@ -1201,8 +1205,54 @@ class TextPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyText(event) {
|
||||||
|
//все это для того, чтобы правильно расставить переносы \n при копировании текста
|
||||||
|
//прямо с текущей страницы
|
||||||
|
|
||||||
|
//подготовка, вытаскиваем весь текст страницы
|
||||||
|
const lines = this.getLines(this.bookPos);
|
||||||
|
const decodedLines = [];
|
||||||
|
for (const line of lines.linesDown) {
|
||||||
|
let lineText = '';
|
||||||
|
for (const part of line.parts) {
|
||||||
|
lineText += part.text;
|
||||||
|
}
|
||||||
|
decodedLines.push({text: he.decode(lineText), first: line.first});
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const findDecoded = (line) => {
|
||||||
|
for (let j = i; j < decodedLines.length; j++) {
|
||||||
|
const decoded = decodedLines[j];
|
||||||
|
if (decoded.text.indexOf(line) >= 0) {
|
||||||
|
i = j;
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = document.getSelection();
|
||||||
|
const splitted = selection.toString().split(/[\n\r]/);
|
||||||
|
|
||||||
|
let filtered = '';
|
||||||
|
//формируем filtered, учитывая переносы из decodedLines
|
||||||
|
for (const line of splitted) {
|
||||||
|
const found = findDecoded(line);
|
||||||
|
if (found && found.first) {
|
||||||
|
filtered += (filtered ? '\n' : '') + line;
|
||||||
|
} else {
|
||||||
|
filtered += (filtered ? '\r ' : '') + line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//маленькие хитрости, убираем переносы по слогам
|
||||||
|
filtered = filtered.replace(/-\r /g, '').replace(/\r /g, ' ');
|
||||||
|
|
||||||
|
event.clipboardData.setData('text/plain', filtered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default vueComponent(TextPage);
|
export default vueComponent(TextPage);
|
||||||
|
|||||||
@@ -63,48 +63,6 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.cleanRecentBooks();
|
await this.cleanRecentBooks();
|
||||||
|
|
||||||
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
|
||||||
{
|
|
||||||
await this.convertFileToDiskPrefix();
|
|
||||||
if (this.recentRev > 10)
|
|
||||||
await bmRecentStoreOld.clear();
|
|
||||||
}
|
|
||||||
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
|
||||||
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
|
||||||
if (this.recentLast) {
|
|
||||||
this.recent[this.recentLast.key] = this.recentLast;
|
|
||||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
|
||||||
if (_.isObject(meta)) {
|
|
||||||
this.books[meta.key] = meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = null;
|
|
||||||
const len = await bmRecentStoreOld.length();
|
|
||||||
for (let i = len - 1; i >= 0; i--) {
|
|
||||||
key = await bmRecentStoreOld.key(i);
|
|
||||||
if (key) {
|
|
||||||
let r = await bmRecentStoreOld.getItem(key);
|
|
||||||
if (_.isObject(r) && r.key) {
|
|
||||||
this.recent[r.key] = r;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await bmRecentStoreOld.removeItem(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//размножение для дебага
|
|
||||||
/*if (key) {
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
const k = this.keyFromUrl(i.toString());
|
|
||||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
await bmRecentStoreNew.setItem('recent', this.recent);
|
|
||||||
this.recentRev = 1;
|
|
||||||
await bmRecentStoreNew.setItem('rev', this.recentRev);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recentChanged = true;
|
this.recentChanged = true;
|
||||||
@@ -374,7 +332,7 @@ class BookManager {
|
|||||||
//-- recent --------------------------------------------------------------
|
//-- recent --------------------------------------------------------------
|
||||||
async recentSetItem(item = null, skipCheck = false) {
|
async recentSetItem(item = null, skipCheck = false) {
|
||||||
const rev = await bmRecentStoreNew.getItem('rev');
|
const rev = await bmRecentStoreNew.getItem('rev');
|
||||||
if (rev != this.recentRev && !skipCheck) {
|
if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
|
||||||
const newRecent = await bmRecentStoreNew.getItem('recent');
|
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||||
Object.assign(this.recent, newRecent);
|
Object.assign(this.recent, newRecent);
|
||||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||||
@@ -455,33 +413,6 @@ class BookManager {
|
|||||||
return isDel;
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertFileToDiskPrefix() {
|
|
||||||
let isConverted = false;
|
|
||||||
|
|
||||||
const newRecent = {};
|
|
||||||
for (let key of Object.keys(this.recent)) {
|
|
||||||
let newKey = key;
|
|
||||||
let newUrl = this.recent[key].url;
|
|
||||||
|
|
||||||
if (newKey.indexOf('66696c65') == 0) {
|
|
||||||
newKey = newKey.replace(/^66696c65/, '6469736b');
|
|
||||||
if (newUrl)
|
|
||||||
newUrl = newUrl.replace(/^file/, 'disk');
|
|
||||||
isConverted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
newRecent[newKey] = this.recent[key];
|
|
||||||
newRecent[newKey].key = newKey;
|
|
||||||
if (newUrl)
|
|
||||||
newRecent[newKey].url = newUrl;
|
|
||||||
}
|
|
||||||
if (isConverted) {
|
|
||||||
this.recent = newRecent;
|
|
||||||
await this.recentSetItem(null, true);
|
|
||||||
}
|
|
||||||
return isConverted;
|
|
||||||
}
|
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
if (this.recentLastKey) {
|
if (this.recentLastKey) {
|
||||||
return this.recent[this.recentLastKey];
|
return this.recent[this.recentLastKey];
|
||||||
|
|||||||
@@ -1,51 +1,101 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
{
|
{
|
||||||
|
version: '0.11.6',
|
||||||
|
releaseDate: '2022-07-02',
|
||||||
|
showUntil: '2022-07-01',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>улучшено копирование текста прямо со страницы, для переводчиков</li>
|
||||||
|
<li>актуализация используемых пакетов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.5',
|
||||||
|
releaseDate: '2022-04-15',
|
||||||
|
showUntil: '2022-04-14',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>небольшие дополнения интерфейса</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.1',
|
||||||
|
releaseDate: '2021-12-03',
|
||||||
|
showUntil: '2021-12-02',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>переход на JembaDb вместо SQLite</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
version: '0.11.0',
|
||||||
|
releaseDate: '2021-11-18',
|
||||||
showUntil: '2021-11-17',
|
showUntil: '2021-11-17',
|
||||||
header: '0.11.0 (2021-11-18)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>переход на Vue 3</li>
|
<li>переход на Vue 3</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.3',
|
||||||
|
releaseDate: '2021-10-24',
|
||||||
showUntil: '2021-10-23',
|
showUntil: '2021-10-23',
|
||||||
header: '0.10.3 (2021-10-24)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.2',
|
||||||
|
releaseDate: '2021-10-19',
|
||||||
showUntil: '2021-10-18',
|
showUntil: '2021-10-18',
|
||||||
header: '0.10.2 (2021-10-19)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>актуализация версий пакетов и стека используемых технологий</li>
|
<li>актуализация версий пакетов и стека используемых технологий</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.1',
|
||||||
|
releaseDate: '2021-10-10',
|
||||||
showUntil: '2021-10-09',
|
showUntil: '2021-10-09',
|
||||||
header: '0.10.1 (2021-10-10)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.10.0',
|
||||||
|
releaseDate: '2021-02-09',
|
||||||
showUntil: '2021-02-16',
|
showUntil: '2021-02-16',
|
||||||
header: '0.10.0 (2021-02-09)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -54,12 +104,14 @@ export const versionHistory = [
|
|||||||
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||||
<li>немного улучшен парсинг fb2</li>
|
<li>немного улучшен парсинг fb2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.12',
|
||||||
|
releaseDate: '2020-12-18',
|
||||||
showUntil: '2020-12-17',
|
showUntil: '2020-12-17',
|
||||||
header: '0.9.12 (2020-12-18)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -68,23 +120,27 @@ export const versionHistory = [
|
|||||||
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||||
<li>улучшения работы конвертеров</li>
|
<li>улучшения работы конвертеров</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.11',
|
||||||
|
releaseDate: '2020-12-09',
|
||||||
showUntil: '2020-12-08',
|
showUntil: '2020-12-08',
|
||||||
header: '0.9.11 (2020-12-09)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>оптимизации, улучшения работы конвертеров</li>
|
<li>оптимизации, улучшения работы конвертеров</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.10',
|
||||||
|
releaseDate: '2020-12-03',
|
||||||
showUntil: '2020-12-10',
|
showUntil: '2020-12-10',
|
||||||
header: '0.9.10 (2020-12-03)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -92,69 +148,81 @@ export const versionHistory = [
|
|||||||
<li>добавлена поддержка Rar-архивов</li>
|
<li>добавлена поддержка Rar-архивов</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.9',
|
||||||
|
releaseDate: '2020-11-21',
|
||||||
showUntil: '2020-11-20',
|
showUntil: '2020-11-20',
|
||||||
header: '0.9.9 (2020-11-21)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>оптимизации, исправления багов</li>
|
<li>оптимизации, исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.8',
|
||||||
|
releaseDate: '2020-11-13',
|
||||||
showUntil: '2020-11-12',
|
showUntil: '2020-11-12',
|
||||||
header: '0.9.8 (2020-11-13)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлено окно "Оглавление/закладки"</li>
|
<li>добавлено окно "Оглавление/закладки"</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.7',
|
||||||
|
releaseDate: '2020-11-12',
|
||||||
showUntil: '2020-11-11',
|
showUntil: '2020-11-11',
|
||||||
header: '0.9.7 (2020-11-12)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.6',
|
||||||
|
releaseDate: '2020-11-06',
|
||||||
showUntil: '2020-11-05',
|
showUntil: '2020-11-05',
|
||||||
header: '0.9.6 (2020-11-06)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>завершена работа над новым окном "Библиотека"</li>
|
<li>завершена работа над новым окном "Библиотека"</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.5',
|
||||||
|
releaseDate: '2020-11-01',
|
||||||
showUntil: '2020-10-31',
|
showUntil: '2020-10-31',
|
||||||
header: '0.9.5 (2020-11-01)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.4',
|
||||||
|
releaseDate: '2020-10-29',
|
||||||
showUntil: '2020-10-28',
|
showUntil: '2020-10-28',
|
||||||
header: '0.9.4 (2020-10-29)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -162,23 +230,27 @@ export const versionHistory = [
|
|||||||
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.3',
|
||||||
|
releaseDate: '2020-05-21',
|
||||||
showUntil: '2020-05-20',
|
showUntil: '2020-05-20',
|
||||||
header: '0.9.3 (2020-05-21)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.2',
|
||||||
|
releaseDate: '2020-03-15',
|
||||||
showUntil: '2020-04-25',
|
showUntil: '2020-04-25',
|
||||||
header: '0.9.2 (2020-03-15)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -186,119 +258,139 @@ export const versionHistory = [
|
|||||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.1',
|
||||||
|
releaseDate: '2020-03-03',
|
||||||
showUntil: '2020-03-02',
|
showUntil: '2020-03-02',
|
||||||
header: '0.9.1 (2020-03-03)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>улучшение работы серверной части</li>
|
<li>улучшение работы серверной части</li>
|
||||||
<li>незначительные изменения интерфейса</li>
|
<li>незначительные изменения интерфейса</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.9.0',
|
||||||
|
releaseDate: '2020-02-26',
|
||||||
showUntil: '2020-02-25',
|
showUntil: '2020-02-25',
|
||||||
header: '0.9.0 (2020-02-26)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>переход на UI-фреймфорк Quasar</li>
|
<li>переход на UI-фреймфорк Quasar</li>
|
||||||
<li>незначительные изменения интерфейса</li>
|
<li>незначительные изменения интерфейса</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.4',
|
||||||
|
releaseDate: '2020-02-06',
|
||||||
showUntil: '2020-02-05',
|
showUntil: '2020-02-05',
|
||||||
header: '0.8.4 (2020-02-06)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлен paypal-адрес для пожертвований</li>
|
<li>добавлен paypal-адрес для пожертвований</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.3',
|
||||||
|
releaseDate: '2020-01-28',
|
||||||
showUntil: '2020-01-27',
|
showUntil: '2020-01-27',
|
||||||
header: '0.8.3 (2020-01-28)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||||
<li>внутренние оптимизации</li>
|
<li>внутренние оптимизации</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.2',
|
||||||
|
releaseDate: '2020-01-20',
|
||||||
showUntil: '2020-01-19',
|
showUntil: '2020-01-19',
|
||||||
header: '0.8.2 (2020-01-20)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>внутренние оптимизации</li>
|
<li>внутренние оптимизации</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.1',
|
||||||
|
releaseDate: '2020-01-07',
|
||||||
showUntil: '2020-01-06',
|
showUntil: '2020-01-06',
|
||||||
header: '0.8.1 (2020-01-07)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлена частичная поддержка формата FB3</li>
|
<li>добавлена частичная поддержка формата FB3</li>
|
||||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.8.0',
|
||||||
|
releaseDate: '2020-01-02',
|
||||||
showUntil: '2020-01-05',
|
showUntil: '2020-01-05',
|
||||||
header: '0.8.0 (2020-01-02)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>окончательный переход на https</li>
|
<li>окончательный переход на https</li>
|
||||||
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.9',
|
||||||
|
releaseDate: '2019-11-27',
|
||||||
showUntil: '2019-11-26',
|
showUntil: '2019-11-26',
|
||||||
header: '0.7.9 (2019-11-27)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.8',
|
||||||
|
releaseDate: '2019-11-25',
|
||||||
showUntil: '2019-11-24',
|
showUntil: '2019-11-24',
|
||||||
header: '0.7.8 (2019-11-25)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>улучшение html-фильтров для сайтов</li>
|
<li>улучшение html-фильтров для сайтов</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.7',
|
||||||
|
releaseDate: '2019-11-06',
|
||||||
showUntil: '2019-11-10',
|
showUntil: '2019-11-10',
|
||||||
header: '0.7.7 (2019-11-06)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -310,34 +402,40 @@ export const versionHistory = [
|
|||||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.6',
|
||||||
|
releaseDate: '2019-10-30',
|
||||||
showUntil: '2019-10-29',
|
showUntil: '2019-10-29',
|
||||||
header: '0.7.6 (2019-10-30)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.5',
|
||||||
|
releaseDate: '2019-10-22',
|
||||||
showUntil: '2019-10-21',
|
showUntil: '2019-10-21',
|
||||||
header: '0.7.5 (2019-10-22)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.3',
|
||||||
|
releaseDate: '2019-10-18',
|
||||||
showUntil: '2019-10-17',
|
showUntil: '2019-10-17',
|
||||||
header: '0.7.3 (2019-10-18)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -346,12 +444,14 @@ export const versionHistory = [
|
|||||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.1',
|
||||||
|
releaseDate: '2019-09-20',
|
||||||
showUntil: '2019-09-19',
|
showUntil: '2019-09-19',
|
||||||
header: '0.7.1 (2019-09-20)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -359,12 +459,14 @@ export const versionHistory = [
|
|||||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||||
<li>актуализирована справка</li>
|
<li>актуализирована справка</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.7.0',
|
||||||
|
releaseDate: '2019-09-07',
|
||||||
showUntil: '2019-10-01',
|
showUntil: '2019-10-01',
|
||||||
header: '0.7.0 (2019-09-07)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -375,23 +477,27 @@ export const versionHistory = [
|
|||||||
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||||
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.10',
|
||||||
|
releaseDate: '2019-07-21',
|
||||||
showUntil: '2019-07-20',
|
showUntil: '2019-07-20',
|
||||||
header: '0.6.10 (2019-07-21)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов</li>
|
<li>исправления багов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.9',
|
||||||
|
releaseDate: '2019-06-23',
|
||||||
showUntil: '2019-06-22',
|
showUntil: '2019-06-22',
|
||||||
header: '0.6.9 (2019-06-23)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -402,12 +508,14 @@ export const versionHistory = [
|
|||||||
<li>улучшены прогрессбары</li>
|
<li>улучшены прогрессбары</li>
|
||||||
<li>исправления недочетов, небольшие оптимизации</li>
|
<li>исправления недочетов, небольшие оптимизации</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.7',
|
||||||
|
releaseDate: '2019-05-30',
|
||||||
showUntil: '2019-06-05',
|
showUntil: '2019-06-05',
|
||||||
header: '0.6.7 (2019-05-30)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -420,36 +528,42 @@ export const versionHistory = [
|
|||||||
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||||
<li>исправления багов и недочетов</li>
|
<li>исправления багов и недочетов</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.6',
|
||||||
|
releaseDate: '2019-03-29',
|
||||||
showUntil: '2019-03-29',
|
showUntil: '2019-03-29',
|
||||||
header: '0.6.6 (2019-03-29)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||||
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.6.4',
|
||||||
|
releaseDate: '2019-03-24',
|
||||||
showUntil: '2019-03-24',
|
showUntil: '2019-03-24',
|
||||||
header: '0.6.4 (2019-03-24)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>исправления багов, оптимизации</li>
|
<li>исправления багов, оптимизации</li>
|
||||||
<li>добавлена возможность синхронизации данных между устройствами</li>
|
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.5.4',
|
||||||
|
releaseDate: '2019-03-04',
|
||||||
showUntil: '2019-03-04',
|
showUntil: '2019-03-04',
|
||||||
header: '0.5.4 (2019-03-04)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -458,12 +572,14 @@ export const versionHistory = [
|
|||||||
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||||
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.3.0',
|
||||||
|
releaseDate: '2019-02-17',
|
||||||
showUntil: '2019-02-17',
|
showUntil: '2019-02-17',
|
||||||
header: '0.3.0 (2019-02-17)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -471,12 +587,14 @@ export const versionHistory = [
|
|||||||
<li>улучшено распознавание текста</li>
|
<li>улучшено распознавание текста</li>
|
||||||
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.1.7',
|
||||||
|
releaseDate: '2019-02-14',
|
||||||
showUntil: '2019-02-14',
|
showUntil: '2019-02-14',
|
||||||
header: '0.1.7 (2019-02-14)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
@@ -486,17 +604,20 @@ export const versionHistory = [
|
|||||||
<li>добавлена возможность сброса настроек</li>
|
<li>добавлена возможность сброса настроек</li>
|
||||||
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
version: '0.1.0',
|
||||||
|
releaseDate: '2019-02-12',
|
||||||
showUntil: '2019-02-12',
|
showUntil: '2019-02-12',
|
||||||
header: '0.1.0 (2019-02-12)',
|
|
||||||
content:
|
content:
|
||||||
`
|
`
|
||||||
<ul>
|
<ul>
|
||||||
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ const plugins = {
|
|||||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||||
|
|
||||||
//const q: {Quasar, QuasarOptions: { config, components, directives, plugins }};
|
|
||||||
export default {
|
export default {
|
||||||
quasar: Quasar,
|
quasar: Quasar,
|
||||||
options: { config, components, directives, plugins },
|
options: { config, components, directives, plugins },
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function toBase58(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fromBase58(data) {
|
export function fromBase58(data) {
|
||||||
return bs58.decode(data);
|
return Buffer.from(bs58.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
//base-x слишком тормозит, используем sjcl
|
//base-x слишком тормозит, используем sjcl
|
||||||
@@ -107,6 +107,10 @@ export function fromBase64(data) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasProp(obj, prop) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
||||||
|
}
|
||||||
|
|
||||||
export function getObjDiff(oldObj, newObj, opts = {}) {
|
export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||||
const {
|
const {
|
||||||
exclude = [],
|
exclude = [],
|
||||||
@@ -126,7 +130,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
|||||||
for (const key of Object.keys(oldObj)) {
|
for (const key of Object.keys(oldObj)) {
|
||||||
const kp = `${keyPath}${key}`;
|
const kp = `${keyPath}${key}`;
|
||||||
|
|
||||||
if (newObj.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||||
if (ex.has(kp))
|
if (ex.has(kp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -149,7 +153,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
|||||||
if (exAdd.has(kp))
|
if (exAdd.has(kp))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!oldObj.hasOwnProperty(key)) {
|
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
|
||||||
result.add[key] = _.cloneDeep(newObj[key]);
|
result.add[key] = _.cloneDeep(newObj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +217,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
|
|||||||
|
|
||||||
const change = diff.change;
|
const change = diff.change;
|
||||||
for (const key of Object.keys(change)) {
|
for (const key of Object.keys(change)) {
|
||||||
if (result.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||||
if (_.isObject(change[key])) {
|
if (_.isObject(change[key])) {
|
||||||
result[key] = applyObjDiff(result[key], change[key], opts);
|
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import * as utils from '../../share/utils';
|
|||||||
import googleFonts from './fonts/fonts.json';
|
import googleFonts from './fonts/fonts.json';
|
||||||
|
|
||||||
const readerActions = {
|
const readerActions = {
|
||||||
'help': 'Вызвать cправку',
|
|
||||||
'loader': 'На страницу загрузки',
|
'loader': 'На страницу загрузки',
|
||||||
|
'loadFile': 'Загрузить файл с диска',
|
||||||
|
'loadBuffer': 'Загрузить из буфера обмена',
|
||||||
|
'help': 'Вызвать cправку',
|
||||||
'settings': 'Настроить',
|
'settings': 'Настроить',
|
||||||
'undoAction': 'Действие назад',
|
'undoAction': 'Действие назад',
|
||||||
'redoAction': 'Действие вперед',
|
'redoAction': 'Действие вперед',
|
||||||
@@ -15,6 +17,7 @@ const readerActions = {
|
|||||||
'copyText': 'Скопировать текст со страницы',
|
'copyText': 'Скопировать текст со страницы',
|
||||||
'convOptions': 'Настроить конвертирование',
|
'convOptions': 'Настроить конвертирование',
|
||||||
'refresh': 'Принудительно обновить книгу',
|
'refresh': 'Принудительно обновить книгу',
|
||||||
|
'clickControl': 'Управление кликом',
|
||||||
'offlineMode': 'Автономный режим (без интернета)',
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
'contents': 'Оглавление/закладки',
|
'contents': 'Оглавление/закладки',
|
||||||
'libs': 'Сетевая библиотека',
|
'libs': 'Сетевая библиотека',
|
||||||
@@ -35,6 +38,9 @@ const readerActions = {
|
|||||||
|
|
||||||
//readerActions[name]
|
//readerActions[name]
|
||||||
const toolButtons = [
|
const toolButtons = [
|
||||||
|
{name: 'loadFile', show: true},
|
||||||
|
{name: 'loadBuffer', show: true},
|
||||||
|
{name: 'help', show: true},
|
||||||
{name: 'undoAction', show: true},
|
{name: 'undoAction', show: true},
|
||||||
{name: 'redoAction', show: true},
|
{name: 'redoAction', show: true},
|
||||||
{name: 'fullScreen', show: true},
|
{name: 'fullScreen', show: true},
|
||||||
@@ -47,13 +53,16 @@ const toolButtons = [
|
|||||||
{name: 'contents', show: true},
|
{name: 'contents', show: true},
|
||||||
{name: 'libs', show: true},
|
{name: 'libs', show: true},
|
||||||
{name: 'recentBooks', show: true},
|
{name: 'recentBooks', show: true},
|
||||||
|
{name: 'clickControl', show: false},
|
||||||
{name: 'offlineMode', show: false},
|
{name: 'offlineMode', show: false},
|
||||||
];
|
];
|
||||||
|
|
||||||
//readerActions[name]
|
//readerActions[name]
|
||||||
const hotKeys = [
|
const hotKeys = [
|
||||||
{name: 'help', codes: ['F1', 'H']},
|
|
||||||
{name: 'loader', codes: ['Escape']},
|
{name: 'loader', codes: ['Escape']},
|
||||||
|
{name: 'loadFile', codes: ['F3']},
|
||||||
|
{name: 'loadBuffer', codes: ['F4']},
|
||||||
|
{name: 'help', codes: ['F1', 'H']},
|
||||||
{name: 'settings', codes: ['S']},
|
{name: 'settings', codes: ['S']},
|
||||||
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||||
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||||
@@ -61,12 +70,13 @@ const hotKeys = [
|
|||||||
{name: 'scrolling', codes: ['Z']},
|
{name: 'scrolling', codes: ['Z']},
|
||||||
{name: 'setPosition', codes: ['P']},
|
{name: 'setPosition', codes: ['P']},
|
||||||
{name: 'search', codes: ['Ctrl+F']},
|
{name: 'search', codes: ['Ctrl+F']},
|
||||||
{name: 'copyText', codes: ['Ctrl+C']},
|
{name: 'copyText', codes: ['Ctrl+Space']},
|
||||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||||
{name: 'refresh', codes: ['R']},
|
{name: 'refresh', codes: ['R']},
|
||||||
{name: 'contents', codes: ['C']},
|
{name: 'contents', codes: ['C']},
|
||||||
{name: 'libs', codes: ['L']},
|
{name: 'libs', codes: ['L']},
|
||||||
{name: 'recentBooks', codes: ['X']},
|
{name: 'recentBooks', codes: ['X']},
|
||||||
|
{name: 'clickControl', codes: ['Ctrl+B']},
|
||||||
{name: 'offlineMode', codes: ['O']},
|
{name: 'offlineMode', codes: ['O']},
|
||||||
|
|
||||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ server {
|
|||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
@@ -139,5 +140,6 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://fantasy-worlds.org;
|
proxy_pass http://fantasy-worlds.org;
|
||||||
|
proxy_hide_header x-frame-options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ server {
|
|||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_read_timeout 600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
12037
package-lock.json
generated
12037
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.11.0",
|
"version": "0.11.6",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||||
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -o dist/linux/liberama .",
|
"build:linux": "npm run build:client && node build/linux && pkg -t node14-linux-x64 -C GZip -o dist/linux/liberama .",
|
||||||
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -o dist/win/liberama .",
|
"build:win": "npm run build:client && node build/win && pkg -t node14-win-x64 -C GZip -o dist/win/liberama .",
|
||||||
"lint": "eslint --ext=.js,.vue client server",
|
"lint": "eslint --ext=.js,.vue client server",
|
||||||
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
||||||
"postinstall": "npm run build:client-dev && node build/linux"
|
"postinstall": "npm run build:client-dev && node build/linux"
|
||||||
@@ -28,16 +28,17 @@
|
|||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.16.0",
|
||||||
"@vue/compiler-sfc": "^3.2.22",
|
"@vue/compiler-sfc": "^3.2.22",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"copy-webpack-plugin": "^9.1.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.5.1",
|
"css-loader": "^6.5.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.1.3",
|
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
"eslint-plugin-vue": "^9.1.1",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"mini-css-extract-plugin": "^2.4.4",
|
"mini-css-extract-plugin": "^2.4.4",
|
||||||
|
"pkg": "^5.5.1",
|
||||||
"terser-webpack-plugin": "^5.2.5",
|
"terser-webpack-plugin": "^5.2.5",
|
||||||
"vue-eslint-parser": "^8.0.1",
|
"vue-eslint-parser": "^9.0.3",
|
||||||
"vue-loader": "^16.8.3",
|
"vue-loader": "^17.0.0",
|
||||||
"vue-style-loader": "^4.1.3",
|
"vue-style-loader": "^4.1.3",
|
||||||
"webpack": "^5.64.1",
|
"webpack": "^5.64.1",
|
||||||
"webpack-cli": "^4.9.1",
|
"webpack-cli": "^4.9.1",
|
||||||
@@ -49,16 +50,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/extras": "^1.12.0",
|
"@quasar/extras": "^1.12.0",
|
||||||
"@vue/compat": "^3.2.21",
|
"@vue/compat": "^3.2.21",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.27.2",
|
||||||
"base-x": "^3.0.9",
|
"base-x": "^4.0.0",
|
||||||
"chardet": "^1.4.0",
|
"chardet": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fg-loadcss": "^3.1.0",
|
"fg-loadcss": "^3.1.0",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^10.1.0",
|
||||||
"got": "^11.8.2",
|
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"jembadb": "^3.0.8",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
@@ -66,7 +67,6 @@
|
|||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"pidusage": "^3.0.0",
|
"pidusage": "^3.0.0",
|
||||||
"pkg": "^4.4.9",
|
|
||||||
"quasar": "^2.3.2",
|
"quasar": "^2.3.2",
|
||||||
"safe-buffer": "^5.2.1",
|
"safe-buffer": "^5.2.1",
|
||||||
"sanitize-html": "^2.5.3",
|
"sanitize-html": "^2.5.3",
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ module.exports = {
|
|||||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||||
|
|
||||||
useExternalBookConverter: false,
|
useExternalBookConverter: false,
|
||||||
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
|
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', 'branch'],
|
||||||
|
|
||||||
db: [
|
db: [
|
||||||
{
|
{
|
||||||
@@ -37,10 +38,18 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
jembaDb: [
|
||||||
|
{
|
||||||
|
dbName: 'reader-storage',
|
||||||
|
thread: true,
|
||||||
|
openAll: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
serverName: '1',
|
serverName: '1',
|
||||||
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader'
|
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top'
|
||||||
ip: '0.0.0.0',
|
ip: '0.0.0.0',
|
||||||
port: '33080',
|
port: '33080',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const BaseController = require('./BaseController');
|
const BaseController = require('./BaseController');
|
||||||
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
||||||
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
|
const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
|
||||||
const WorkerState = require('../core/WorkerState');//singleton
|
const WorkerState = require('../core/WorkerState');//singleton
|
||||||
|
|
||||||
class ReaderController extends BaseController {
|
class ReaderController extends BaseController {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
super(config);
|
super(config);
|
||||||
this.readerStorage = new ReaderStorage();
|
this.readerStorage = new JembaReaderStorage();
|
||||||
this.readerWorker = new ReaderWorker(config);
|
this.readerWorker = new ReaderWorker(config);
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const WebSocket = require ('ws');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
const ReaderWorker = require('../core/Reader/ReaderWorker');//singleton
|
||||||
const ReaderStorage = require('../core/Reader/ReaderStorage');//singleton
|
const JembaReaderStorage = require('../core/Reader/JembaReaderStorage');//singleton
|
||||||
const WorkerState = require('../core/WorkerState');//singleton
|
const WorkerState = require('../core/WorkerState');//singleton
|
||||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
const utils = require('../core/utils');
|
const utils = require('../core/utils');
|
||||||
@@ -15,7 +15,7 @@ class WebSocketController {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.isDevelopment = (config.branch == 'development');
|
this.isDevelopment = (config.branch == 'development');
|
||||||
|
|
||||||
this.readerStorage = new ReaderStorage();
|
this.readerStorage = new JembaReaderStorage();
|
||||||
this.readerWorker = new ReaderWorker(config);
|
this.readerWorker = new ReaderWorker(config);
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
|
|
||||||
@@ -55,8 +55,7 @@ class WebSocketController {
|
|||||||
ws.lastActivity = Date.now();
|
ws.lastActivity = Date.now();
|
||||||
|
|
||||||
//pong for WebSocketConnection
|
//pong for WebSocketConnection
|
||||||
if (req._rpo === 1)
|
this.send({_rok: 1}, req, ws);
|
||||||
this.send({_rok: 1}, req, ws);
|
|
||||||
|
|
||||||
switch (req.action) {
|
switch (req.action) {
|
||||||
case 'test':
|
case 'test':
|
||||||
|
|||||||
@@ -26,59 +26,6 @@ class WorkerController extends BaseController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: удалить бесполезную getStateFinish
|
|
||||||
async getStateFinish(req, res) {
|
|
||||||
const request = req.body;
|
|
||||||
let error = '';
|
|
||||||
try {
|
|
||||||
if (!request.workerId)
|
|
||||||
throw new Error(`key 'workerId' is wrong`);
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': 'text/json; charset=utf-8',
|
|
||||||
});
|
|
||||||
|
|
||||||
const splitter = '-- aod2t5hDXU32bUFyqlFE next status --';
|
|
||||||
const refreshPause = 200;
|
|
||||||
let i = 0;
|
|
||||||
let prevProgress = -1;
|
|
||||||
let prevState = '';
|
|
||||||
let state;
|
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
|
||||||
state = this.workerState.getState(request.workerId);
|
|
||||||
if (!state) break;
|
|
||||||
|
|
||||||
res.write(splitter + JSON.stringify(state));
|
|
||||||
res.flush();
|
|
||||||
|
|
||||||
if (state.state != 'finish' && state.state != 'error')
|
|
||||||
await utils.sleep(refreshPause);
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
|
|
||||||
i++;
|
|
||||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
|
||||||
res.write(splitter + JSON.stringify({state: 'error', error: 'Слишком долгое время ожидания'}));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
|
||||||
prevProgress = state.progress;
|
|
||||||
prevState = state.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
res.write(splitter + JSON.stringify({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
error = e.message;
|
|
||||||
}
|
|
||||||
//bad request
|
|
||||||
res.status(400).send({error});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WorkerController;
|
module.exports = WorkerController;
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ let instance = null;
|
|||||||
class AppLogger {
|
class AppLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
|
this.inited = false;
|
||||||
|
this.logFileName = '';
|
||||||
|
this.errLogFileName = '';
|
||||||
|
this.fatalLogFileName = '';
|
||||||
|
|
||||||
instance = this;
|
instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.inited = false;
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +26,16 @@ class AppLogger {
|
|||||||
|
|
||||||
if (config.loggingEnabled) {
|
if (config.loggingEnabled) {
|
||||||
await fs.ensureDir(config.logDir);
|
await fs.ensureDir(config.logDir);
|
||||||
|
|
||||||
|
this.logFileName = `${config.logDir}/${config.name}.log`;
|
||||||
|
this.errLogFileName = `${config.logDir}/${config.name}.err.log`;
|
||||||
|
this.fatalLogFileName = `${config.logDir}/${config.name}.fatal.log`;
|
||||||
|
|
||||||
loggerParams = [
|
loggerParams = [
|
||||||
{log: 'ConsoleLog'},
|
{log: 'ConsoleLog'},
|
||||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`},
|
{log: 'FileLog', fileName: this.logFileName},
|
||||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
|
{log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
|
||||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.fatal.log`, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
|
{log: 'FileLog', fileName: this.fatalLogFileName, exclude: [LM_OK, LM_INFO, LM_WARN, LM_ERR, LM_TOTAL]},//LM_FATAL only
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
server/core/AsyncExit.js
Normal file
105
server/core/AsyncExit.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
let instance = null;
|
||||||
|
|
||||||
|
const defaultTimeout = 15*1000;//15 sec
|
||||||
|
const exitSignals = ['SIGINT', 'SIGTERM', 'SIGBREAK', 'SIGHUP', 'uncaughtException'];
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class AsyncExit {
|
||||||
|
constructor(signals = exitSignals, codeOnSignal = 2) {
|
||||||
|
if (!instance) {
|
||||||
|
this.onSignalCallbacks = new Map();
|
||||||
|
this.callbacks = new Map();
|
||||||
|
this.afterCallbacks = new Map();
|
||||||
|
this.exitTimeout = defaultTimeout;
|
||||||
|
|
||||||
|
this._init(signals, codeOnSignal);
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
_init(signals, codeOnSignal) {
|
||||||
|
const runSingalCallbacks = async(signal, err, origin) => {
|
||||||
|
for (const signalCallback of this.onSignalCallbacks.keys()) {
|
||||||
|
try {
|
||||||
|
await signalCallback(signal, err, origin);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const signal of signals) {
|
||||||
|
process.once(signal, async(err, origin) => {
|
||||||
|
await runSingalCallbacks(signal, err, origin);
|
||||||
|
this.exit(codeOnSignal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSignal(signalCallback) {
|
||||||
|
if (!this.onSignalCallbacks.has(signalCallback)) {
|
||||||
|
this.onSignalCallbacks.set(signalCallback, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(exitCallback) {
|
||||||
|
if (!this.callbacks.has(exitCallback)) {
|
||||||
|
this.callbacks.set(exitCallback, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addAfter(exitCallback) {
|
||||||
|
if (!this.afterCallbacks.has(exitCallback)) {
|
||||||
|
this.afterCallbacks.set(exitCallback, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(exitCallback) {
|
||||||
|
if (this.callbacks.has(exitCallback)) {
|
||||||
|
this.callbacks.delete(exitCallback);
|
||||||
|
}
|
||||||
|
if (this.afterCallbacks.has(exitCallback)) {
|
||||||
|
this.afterCallbacks.delete(exitCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setExitTimeout(timeout) {
|
||||||
|
this.exitTimeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(code = 0) {
|
||||||
|
if (this.exiting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.exiting = true;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => { process.exit(code); }, this.exitTimeout);
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
for (const exitCallback of this.callbacks.keys()) {
|
||||||
|
try {
|
||||||
|
await exitCallback();
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const exitCallback of this.afterCallbacks.keys()) {
|
||||||
|
try {
|
||||||
|
await exitCallback();
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timer);
|
||||||
|
//console.log('Exited gracefully');
|
||||||
|
process.exit(code);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AsyncExit;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const got = require('got');
|
const axios = require('axios');
|
||||||
|
|
||||||
class FileDownloader {
|
class FileDownloader {
|
||||||
constructor(limitDownloadSize = 0) {
|
constructor(limitDownloadSize = 0) {
|
||||||
@@ -7,54 +7,82 @@ class FileDownloader {
|
|||||||
|
|
||||||
async load(url, callback, abort) {
|
async load(url, callback, abort) {
|
||||||
let errMes = '';
|
let errMes = '';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': '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'
|
'user-agent': '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'
|
||||||
},
|
},
|
||||||
responseType: 'buffer',
|
responseType: 'stream',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await got(url, Object.assign({}, options, {method: 'HEAD'}));
|
|
||||||
|
|
||||||
let estSize = 0;
|
|
||||||
if (response.headers['content-length']) {
|
|
||||||
estSize = response.headers['content-length'];
|
|
||||||
}
|
|
||||||
|
|
||||||
let prevProg = 0;
|
|
||||||
const request = got(url, options);
|
|
||||||
|
|
||||||
request.on('downloadProgress', progress => {
|
|
||||||
if (this.limitDownloadSize) {
|
|
||||||
if (progress.transferred > this.limitDownloadSize) {
|
|
||||||
errMes = 'Файл слишком большой';
|
|
||||||
request.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let prog = 0;
|
|
||||||
if (estSize)
|
|
||||||
prog = Math.round(progress.transferred/estSize*100);
|
|
||||||
else if (progress.transferred)
|
|
||||||
prog = Math.round(progress.transferred/(progress.transferred + 200000)*100);
|
|
||||||
|
|
||||||
if (prog != prevProg && callback)
|
|
||||||
callback(prog);
|
|
||||||
prevProg = prog;
|
|
||||||
|
|
||||||
if (abort && abort()) {
|
|
||||||
errMes = 'abort';
|
|
||||||
request.cancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (await request).body;
|
const res = await axios.get(url, options);
|
||||||
|
|
||||||
|
let estSize = 0;
|
||||||
|
if (res.headers['content-length']) {
|
||||||
|
estSize = res.headers['content-length'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estSize > this.limitDownloadSize) {
|
||||||
|
throw new Error('Файл слишком большой');
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevProg = 0;
|
||||||
|
let transferred = 0;
|
||||||
|
|
||||||
|
const download = this.streamToBuffer(res.data, (chunk) => {
|
||||||
|
transferred += chunk.length;
|
||||||
|
if (this.limitDownloadSize) {
|
||||||
|
if (transferred > this.limitDownloadSize) {
|
||||||
|
errMes = 'Файл слишком большой';
|
||||||
|
res.request.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prog = 0;
|
||||||
|
if (estSize)
|
||||||
|
prog = Math.round(transferred/estSize*100);
|
||||||
|
else
|
||||||
|
prog = Math.round(transferred/(transferred + 200000)*100);
|
||||||
|
|
||||||
|
if (prog != prevProg && callback)
|
||||||
|
callback(prog);
|
||||||
|
prevProg = prog;
|
||||||
|
|
||||||
|
if (abort && abort()) {
|
||||||
|
errMes = 'abort';
|
||||||
|
res.request.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await download;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errMes = (errMes ? errMes : error.message);
|
errMes = (errMes ? errMes : error.message);
|
||||||
throw new Error(errMes);
|
throw new Error(errMes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
streamToBuffer(stream, progress) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
if (!progress)
|
||||||
|
progress = () => {};
|
||||||
|
|
||||||
|
const _buf = [];
|
||||||
|
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
_buf.push(chunk);
|
||||||
|
progress(chunk);
|
||||||
|
});
|
||||||
|
stream.on('end', () => resolve(Buffer.concat(_buf)));
|
||||||
|
stream.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
stream.on('aborted', () => {
|
||||||
|
reject(new Error('aborted'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FileDownloader;
|
module.exports = FileDownloader;
|
||||||
@@ -25,7 +25,6 @@ class MegaStorage {
|
|||||||
this.debouncedSaveStats = _.debounce(() => {
|
this.debouncedSaveStats = _.debounce(() => {
|
||||||
this.saveStats().catch((e) => {
|
this.saveStats().catch((e) => {
|
||||||
log(LM_ERR, `MegaStorage::saveStats ${e.message}`);
|
log(LM_ERR, `MegaStorage::saveStats ${e.message}`);
|
||||||
//process.exit(1);
|
|
||||||
});
|
});
|
||||||
}, 5000, {'maxWait':6000});
|
}, 5000, {'maxWait':6000});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
Журналирование с буферизацией вывода
|
Журналирование с буферизацией вывода
|
||||||
*/
|
*/
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const ayncExit = new (require('./AsyncExit'))();
|
||||||
|
|
||||||
|
const sleep = (ms) => { return new Promise(resolve => setTimeout(resolve, ms)) };
|
||||||
|
|
||||||
global.LM_OK = 0;
|
global.LM_OK = 0;
|
||||||
global.LM_INFO = 1;
|
global.LM_INFO = 1;
|
||||||
@@ -46,12 +49,13 @@ class BaseLog {
|
|||||||
this.outputBuffer = [];
|
this.outputBuffer = [];
|
||||||
|
|
||||||
await this.flushImpl(this.data)
|
await this.flushImpl(this.data)
|
||||||
.catch(e => { console.log(e); process.exit(1); } );
|
.catch(e => { console.log(e); ayncExit.exit(1); } );
|
||||||
this.flushing = false;
|
this.flushing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(msgType, message) {
|
log(msgType, message) {
|
||||||
if (this.closed) { console.log(`Logger fatal error: log was closed (message to log: ${message}})`); process.exit(1); }
|
if (this.closed)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!this.exclude.has(msgType)) {
|
if (!this.exclude.has(msgType)) {
|
||||||
this.outputBuffer.push(message);
|
this.outputBuffer.push(message);
|
||||||
@@ -73,7 +77,7 @@ class BaseLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
async close() {
|
||||||
if (this.closed)
|
if (this.closed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -81,12 +85,13 @@ class BaseLog {
|
|||||||
clearInterval(this.iid);
|
clearInterval(this.iid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.flushing)
|
while (this.outputBufferLength) {
|
||||||
this.flushImplSync(this.data);
|
await this.flush();
|
||||||
this.flushImplSync(this.outputBuffer);
|
await sleep(1);
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
process.exit(1);
|
ayncExit.exit(1);
|
||||||
}
|
}
|
||||||
this.outputBufferLength = 0;
|
this.outputBufferLength = 0;
|
||||||
this.outputBuffer = [];
|
this.outputBuffer = [];
|
||||||
@@ -103,12 +108,14 @@ class FileLog extends BaseLog {
|
|||||||
this.rcid = 0;
|
this.rcid = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
async close() {
|
||||||
if (this.closed)
|
if (this.closed)
|
||||||
return;
|
return;
|
||||||
super.close();
|
await super.close();
|
||||||
if (this.fd)
|
if (this.fd) {
|
||||||
fs.closeSync(this.fd);
|
await fs.close(this.fd);
|
||||||
|
this.fd = null;
|
||||||
|
}
|
||||||
if (this.rcid)
|
if (this.rcid)
|
||||||
clearTimeout(this.rcid);
|
clearTimeout(this.rcid);
|
||||||
}
|
}
|
||||||
@@ -151,23 +158,15 @@ class FileLog extends BaseLog {
|
|||||||
}, LOG_ROTATE_FILE_CHECK_INTERVAL);
|
}, LOG_ROTATE_FILE_CHECK_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.write(this.fd, Buffer.from(data.join('')));
|
if (this.fd)
|
||||||
|
await fs.write(this.fd, Buffer.from(data.join('')));
|
||||||
}
|
}
|
||||||
|
|
||||||
flushImplSync(data) {
|
|
||||||
fs.writeSync(this.fd, Buffer.from(data.join('')));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConsoleLog extends BaseLog {
|
class ConsoleLog extends BaseLog {
|
||||||
async flushImpl(data) {
|
async flushImpl(data) {
|
||||||
process.stdout.write(data.join(''));
|
process.stdout.write(data.join(''));
|
||||||
}
|
}
|
||||||
|
|
||||||
flushImplSync(data) {
|
|
||||||
process.stdout.write(data.join(''));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------
|
//------------------------------------------------------------------
|
||||||
@@ -178,7 +177,7 @@ const factory = {
|
|||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
|
|
||||||
constructor(params = null, cleanupCallback = null) {
|
constructor(params = null) {
|
||||||
this.handlers = [];
|
this.handlers = [];
|
||||||
if (params) {
|
if (params) {
|
||||||
params.forEach((logParams) => {
|
params.forEach((logParams) => {
|
||||||
@@ -187,12 +186,22 @@ class Logger {
|
|||||||
this.handlers.push(new loggerClass(logParams));
|
this.handlers.push(new loggerClass(logParams));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cleanupCallback = cleanupCallback || (() => {});
|
|
||||||
this.cleanup(cleanupCallback);
|
this.closed = false;
|
||||||
|
ayncExit.onSignal((signal, err) => {
|
||||||
|
this.log(LM_FATAL, `Signal "${signal}" received, error: "${(err.stack ? err.stack : err)}", exiting...`);
|
||||||
|
});
|
||||||
|
ayncExit.addAfter(this.close.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ` +
|
||||||
|
`${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}.` +
|
||||||
|
`${date.getMilliseconds().toString().padStart(3, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareMessage(msgType, message) {
|
prepareMessage(msgType, message) {
|
||||||
return (new Date().toISOString()) + ` ${msgTypeToStr[msgType]}: ${message}\n`;
|
return this.formatDate(new Date()) + ` ${msgTypeToStr[msgType]}: ${message}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(msgType, message) {
|
log(msgType, message) {
|
||||||
@@ -203,47 +212,18 @@ class Logger {
|
|||||||
|
|
||||||
const mes = this.prepareMessage(msgType, message);
|
const mes = this.prepareMessage(msgType, message);
|
||||||
|
|
||||||
for (let i = 0; i < this.handlers.length; i++)
|
if (!this.closed) {
|
||||||
this.handlers[i].log(msgType, mes);
|
for (let i = 0; i < this.handlers.length; i++)
|
||||||
|
this.handlers[i].log(msgType, mes);
|
||||||
|
} else {
|
||||||
|
console.log(mes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
async close() {
|
||||||
for (let i = 0; i < this.handlers.length; i++)
|
for (let i = 0; i < this.handlers.length; i++)
|
||||||
this.handlers[i].close();
|
await this.handlers[i].close();
|
||||||
}
|
this.closed = true;
|
||||||
|
|
||||||
cleanup(callback) {
|
|
||||||
// attach user callback to the process event emitter
|
|
||||||
// if no callback, it will still exit gracefully on Ctrl-C
|
|
||||||
callback = callback || (() => {});
|
|
||||||
process.on('cleanup', callback);
|
|
||||||
|
|
||||||
// do app specific cleaning before exiting
|
|
||||||
process.on('exit', () => {
|
|
||||||
this.close();
|
|
||||||
process.emit('cleanup');
|
|
||||||
});
|
|
||||||
|
|
||||||
// catch ctrl+c event and exit normally
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
this.log(LM_FATAL, 'Ctrl-C pressed, exiting...');
|
|
||||||
process.exit(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
this.log(LM_FATAL, 'Kill signal, exiting...');
|
|
||||||
process.exit(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
//catch uncaught exceptions, trace, then exit normally
|
|
||||||
process.on('uncaughtException', e => {
|
|
||||||
try {
|
|
||||||
this.log(LM_FATAL, e.stack);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e.stack);
|
|
||||||
}
|
|
||||||
process.exit(99);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
server/core/Reader/JembaReaderStorage.js
Normal file
122
server/core/Reader/JembaReaderStorage.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const utils = require('../utils');
|
||||||
|
const JembaConnManager = require('../../db/JembaConnManager');//singleton
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class JembaReaderStorage {
|
||||||
|
constructor() {
|
||||||
|
if (!instance) {
|
||||||
|
this.connManager = new JembaConnManager();
|
||||||
|
this.db = this.connManager.db['reader-storage'];
|
||||||
|
this.periodicCleanCache(3*3600*1000);//1 раз в 3 часа
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doAction(act) {
|
||||||
|
if (!_.isObject(act.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
|
let result = {};
|
||||||
|
switch (act.action) {
|
||||||
|
case 'check':
|
||||||
|
result = await this.checkItems(act.items);
|
||||||
|
break;
|
||||||
|
case 'get':
|
||||||
|
result = await this.getItems(act.items);
|
||||||
|
break;
|
||||||
|
case 'set':
|
||||||
|
result = await this.setItems(act.items, act.force);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown action');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkItems(items) {
|
||||||
|
let result = {state: 'success', items: {}};
|
||||||
|
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
for (const id of Object.keys(items)) {
|
||||||
|
if (this.cache[id]) {
|
||||||
|
result.items[id] = this.cache[id];
|
||||||
|
} else {
|
||||||
|
const rows = await db.select({//SQL`SELECT rev FROM storage WHERE id = ${id}`
|
||||||
|
table: 'storage',
|
||||||
|
map: '(r) => ({rev: r.rev})',
|
||||||
|
where: `@@id(${db.esc(id)})`
|
||||||
|
});
|
||||||
|
const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
|
||||||
|
result.items[id] = {rev};
|
||||||
|
this.cache[id] = result.items[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItems(items) {
|
||||||
|
let result = {state: 'success', items: {}};
|
||||||
|
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
for (const id of Object.keys(items)) {
|
||||||
|
const rows = await db.select({//SQL`SELECT rev, data FROM storage WHERE id = ${id}`);
|
||||||
|
table: 'storage',
|
||||||
|
where: `@@id(${db.esc(id)})`
|
||||||
|
});
|
||||||
|
const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
|
||||||
|
const data = (rows.length && rows[0].data ? rows[0].data : '');
|
||||||
|
result.items[id] = {rev, data};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setItems(items, force) {
|
||||||
|
let check = await this.checkItems(items);
|
||||||
|
|
||||||
|
//сначала проверим совпадение ревизий
|
||||||
|
for (const id of Object.keys(items)) {
|
||||||
|
if (!_.isString(items[id].data))
|
||||||
|
throw new Error('items.data is not a string');
|
||||||
|
|
||||||
|
if (!force && check.items[id].rev + 1 !== items[id].rev)
|
||||||
|
return {state: 'reject', items: check.items};
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = this.db;
|
||||||
|
const newRev = {};
|
||||||
|
for (const id of Object.keys(items)) {
|
||||||
|
await db.insert({//SQL`INSERT OR REPLACE INTO storage (id, rev, time, data) VALUES (${id}, ${items[id].rev}, strftime('%s','now'), ${items[id].data})`);
|
||||||
|
table: 'storage',
|
||||||
|
replace: true,
|
||||||
|
rows: [{id, rev: items[id].rev, time: utils.toUnixTime(Date.now()), data: items[id].data}],
|
||||||
|
});
|
||||||
|
newRev[id] = {rev: items[id].rev};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this.cache, newRev);
|
||||||
|
|
||||||
|
return {state: 'success'};
|
||||||
|
}
|
||||||
|
|
||||||
|
periodicCleanCache(timeout) {
|
||||||
|
this.cache = {};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.periodicCleanCache(timeout);
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = JembaReaderStorage;
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
const SQL = require('sql-template-strings');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ConnManager = require('../../db/ConnManager');//singleton
|
|
||||||
|
|
||||||
let instance = null;
|
|
||||||
|
|
||||||
//singleton
|
|
||||||
class ReaderStorage {
|
|
||||||
constructor() {
|
|
||||||
if (!instance) {
|
|
||||||
this.connManager = new ConnManager();
|
|
||||||
this.storagePool = this.connManager.pool.readerStorage;
|
|
||||||
this.periodicCleanCache(3*3600*1000);//1 раз в 3 часа
|
|
||||||
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async doAction(act) {
|
|
||||||
if (!_.isObject(act.items))
|
|
||||||
throw new Error('items is not an object');
|
|
||||||
|
|
||||||
let result = {};
|
|
||||||
switch (act.action) {
|
|
||||||
case 'check':
|
|
||||||
result = await this.checkItems(act.items);
|
|
||||||
break;
|
|
||||||
case 'get':
|
|
||||||
result = await this.getItems(act.items);
|
|
||||||
break;
|
|
||||||
case 'set':
|
|
||||||
result = await this.setItems(act.items, act.force);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error('Unknown action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkItems(items) {
|
|
||||||
let result = {state: 'success', items: {}};
|
|
||||||
|
|
||||||
const dbh = await this.storagePool.get();
|
|
||||||
try {
|
|
||||||
for (const id of Object.keys(items)) {
|
|
||||||
if (this.cache[id]) {
|
|
||||||
result.items[id] = this.cache[id];
|
|
||||||
} else {
|
|
||||||
const rows = await dbh.all(SQL`SELECT rev FROM storage WHERE id = ${id}`);
|
|
||||||
const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
|
|
||||||
result.items[id] = {rev};
|
|
||||||
this.cache[id] = result.items[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
dbh.ret();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItems(items) {
|
|
||||||
let result = {state: 'success', items: {}};
|
|
||||||
|
|
||||||
const dbh = await this.storagePool.get();
|
|
||||||
try {
|
|
||||||
for (const id of Object.keys(items)) {
|
|
||||||
const rows = await dbh.all(SQL`SELECT rev, data FROM storage WHERE id = ${id}`);
|
|
||||||
const rev = (rows.length && rows[0].rev ? rows[0].rev : 0);
|
|
||||||
const data = (rows.length && rows[0].data ? rows[0].data : '');
|
|
||||||
result.items[id] = {rev, data};
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
dbh.ret();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setItems(items, force) {
|
|
||||||
let check = await this.checkItems(items);
|
|
||||||
|
|
||||||
//сначала проверим совпадение ревизий
|
|
||||||
for (const id of Object.keys(items)) {
|
|
||||||
if (!_.isString(items[id].data))
|
|
||||||
throw new Error('items.data is not a string');
|
|
||||||
|
|
||||||
if (!force && check.items[id].rev + 1 !== items[id].rev)
|
|
||||||
return {state: 'reject', items: check.items};
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbh = await this.storagePool.get();
|
|
||||||
await dbh.run('BEGIN');
|
|
||||||
try {
|
|
||||||
const newRev = {};
|
|
||||||
for (const id of Object.keys(items)) {
|
|
||||||
await dbh.run(SQL`INSERT OR REPLACE INTO storage (id, rev, time, data) VALUES (${id}, ${items[id].rev}, strftime('%s','now'), ${items[id].data})`);
|
|
||||||
newRev[id] = {rev: items[id].rev};
|
|
||||||
}
|
|
||||||
await dbh.run('COMMIT');
|
|
||||||
|
|
||||||
Object.assign(this.cache, newRev);
|
|
||||||
} catch (e) {
|
|
||||||
await dbh.run('ROLLBACK');
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
dbh.ret();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {state: 'success'};
|
|
||||||
}
|
|
||||||
|
|
||||||
periodicCleanCache(timeout) {
|
|
||||||
this.cache = {};
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.periodicCleanCache(timeout);
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ReaderStorage;
|
|
||||||
@@ -7,6 +7,7 @@ class RemoteWebDavStorage {
|
|||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = Object.assign({}, config);
|
this.config = Object.assign({}, config);
|
||||||
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
|
this.config.maxContentLength = this.config.maxContentLength || 10*1024*1024;
|
||||||
|
this.config.maxBodyLength = this.config.maxContentLength;
|
||||||
this.wdc = createClient(config.url, this.config);
|
this.wdc = createClient(config.url, this.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class RemoteWebDavStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async writeFile(filename, data) {
|
async writeFile(filename, data) {
|
||||||
return await this.wdc.putFileContents(filename, data, { maxContentLength: this.config.maxContentLength })
|
return await this.wdc.putFileContents(filename, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlink(filename) {
|
async unlink(filename) {
|
||||||
@@ -38,7 +39,7 @@ class RemoteWebDavStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async readFile(filename) {
|
async readFile(filename) {
|
||||||
return await this.wdc.getFileContents(filename, { maxContentLength: this.config.maxContentLength })
|
return await this.wdc.getFileContents(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
async mkdir(dirname) {
|
async mkdir(dirname) {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const cleanPeriod = 5*1000;//5 секунд
|
|||||||
class WebSocketConnection {
|
class WebSocketConnection {
|
||||||
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
||||||
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
|
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
|
||||||
//const ws = 'ws';//for nodejs
|
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
|
||||||
this.WebSocket = (isBrowser ? WebSocket : null/*for nodejs require(ws)*/);
|
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.listeners = [];
|
this.listeners = [];
|
||||||
@@ -166,7 +165,7 @@ class WebSocketConnection {
|
|||||||
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
|
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
|
||||||
const requestId = this.requestId;//реентерабельность!!!
|
const requestId = this.requestId;//реентерабельность!!!
|
||||||
|
|
||||||
this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1
|
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||||
|
|
||||||
let resp = {};
|
let resp = {};
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function toBase36(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fromBase36(data) {
|
function fromBase36(data) {
|
||||||
return bs36.decode(data);
|
return Buffer.from(bs36.decode(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function bufferRemoveZeroes(buf) {
|
function bufferRemoveZeroes(buf) {
|
||||||
@@ -38,6 +38,10 @@ function sleep(ms) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toUnixTime(time) {
|
||||||
|
return parseInt(time/1000);
|
||||||
|
}
|
||||||
|
|
||||||
function randomHexString(len) {
|
function randomHexString(len) {
|
||||||
return crypto.randomBytes(len).toString('hex')
|
return crypto.randomBytes(len).toString('hex')
|
||||||
}
|
}
|
||||||
@@ -126,6 +130,7 @@ module.exports = {
|
|||||||
bufferRemoveZeroes,
|
bufferRemoveZeroes,
|
||||||
getFileHash,
|
getFileHash,
|
||||||
sleep,
|
sleep,
|
||||||
|
toUnixTime,
|
||||||
randomHexString,
|
randomHexString,
|
||||||
touchFile,
|
touchFile,
|
||||||
spawnProcess,
|
spawnProcess,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//TODO: удалить модуль в 2023г
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
const SqliteConnectionPool = require('./SqliteConnectionPool');
|
const SqliteConnectionPool = require('./SqliteConnectionPool');
|
||||||
|
|||||||
42
server/db/Converter.js
Normal file
42
server/db/Converter.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//TODO: удалить модуль в 2023г
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
class Converter {
|
||||||
|
async run(config) {
|
||||||
|
log('Converter start');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const connManager = new (require('./ConnManager'))();//singleton
|
||||||
|
const storagePool = connManager.pool.readerStorage;
|
||||||
|
|
||||||
|
const jembaConnManager = new (require('./JembaConnManager'))();//singleton
|
||||||
|
const db = jembaConnManager.db['reader-storage'];
|
||||||
|
|
||||||
|
const srcDbPath = `${config.dataDir}/reader-storage.sqlite`;
|
||||||
|
if (!await fs.pathExists(srcDbPath)) {
|
||||||
|
log(LM_WARN, ' Source DB does not exist, nothing to do');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.select({table: 'storage', count: true});
|
||||||
|
if (rows.length && rows[0].count != 0) {
|
||||||
|
log(LM_WARN, ` Destination table already exists (found ${rows[0].count} items), nothing to do`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbSrc = await storagePool.get();
|
||||||
|
try {
|
||||||
|
const rows = await dbSrc.all(`SELECT * FROM storage`);
|
||||||
|
await db.insert({table: 'storage', rows});
|
||||||
|
log(` Inserted ${rows.length} items`);
|
||||||
|
} finally {
|
||||||
|
dbSrc.ret();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
log('Converter finish');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Converter;
|
||||||
190
server/db/JembaConnManager.js
Normal file
190
server/db/JembaConnManager.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const ayncExit = new (require('../core/AsyncExit'))();//singleton
|
||||||
|
const { JembaDb, JembaDbThread } = require('jembadb');
|
||||||
|
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||||
|
|
||||||
|
const jembaMigrations = require('./jembaMigrations');
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
//singleton
|
||||||
|
class JembaConnManager {
|
||||||
|
constructor() {
|
||||||
|
if (!instance) {
|
||||||
|
this.inited = false;
|
||||||
|
this._db = {};
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(config, forceAutoRepair = false, migs = jembaMigrations, undoLastMigration = false) {
|
||||||
|
if (this.inited)
|
||||||
|
throw new Error('JembaConnManager initialized already');
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
|
this._db = {};
|
||||||
|
|
||||||
|
ayncExit.add(this.close.bind(this));
|
||||||
|
|
||||||
|
for (const dbConfig of this.config.jembaDb) {
|
||||||
|
const dbPath = `${this.config.dataDir}/db/${dbConfig.dbName}`;
|
||||||
|
|
||||||
|
//бэкап
|
||||||
|
if (!dbConfig.noBak && await fs.pathExists(dbPath)) {
|
||||||
|
const bakFile = `${dbPath}.bak`;
|
||||||
|
await fs.remove(bakFile);
|
||||||
|
await fs.copy(dbPath, bakFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbConn = null;
|
||||||
|
if (dbConfig.thread) {
|
||||||
|
dbConn = new JembaDbThread();
|
||||||
|
} else {
|
||||||
|
dbConn = new JembaDb();
|
||||||
|
}
|
||||||
|
this._db[dbConfig.dbName] = dbConn;
|
||||||
|
|
||||||
|
log(`Open "${dbConfig.dbName}" begin`);
|
||||||
|
await dbConn.lock({
|
||||||
|
dbPath,
|
||||||
|
create: true,
|
||||||
|
softLock: true,
|
||||||
|
|
||||||
|
tableDefaults: {
|
||||||
|
cacheSize: dbConfig.cacheSize,
|
||||||
|
compressed: dbConfig.compressed,
|
||||||
|
forceFileClosing: dbConfig.forceFileClosing,
|
||||||
|
typeCompatMode: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dbConfig.openAll || forceAutoRepair || dbConfig.autoRepair) {
|
||||||
|
try {
|
||||||
|
await dbConn.openAll();
|
||||||
|
} catch(e) {
|
||||||
|
if ((forceAutoRepair || dbConfig.autoRepair) &&
|
||||||
|
(
|
||||||
|
e.message.indexOf('corrupted') >= 0
|
||||||
|
|| e.message.indexOf('Unexpected token') >= 0
|
||||||
|
|| e.message.indexOf('invalid stored block lengths') >= 0
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
log(LM_ERR, e);
|
||||||
|
log(`Open "${dbConfig.dbName}" with auto repair`);
|
||||||
|
await dbConn.openAll({autoRepair: true});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Open "${dbConfig.dbName}" end`);
|
||||||
|
|
||||||
|
//миграции
|
||||||
|
const mig = migs[dbConfig.dbName];
|
||||||
|
if (mig && mig.data) {
|
||||||
|
const applied = await this.migrate(dbConn, mig.data, mig.table, undoLastMigration);
|
||||||
|
if (applied.length)
|
||||||
|
log(`${applied.length} migrations applied to "${dbConfig.dbName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
for (const dbConfig of this.config.jembaDb) {
|
||||||
|
if (this._db[dbConfig.dbName])
|
||||||
|
await this._db[dbConfig.dbName].unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._db = {};
|
||||||
|
this.inited = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(db, migs, table, undoLastMigration) {
|
||||||
|
const migrations = _.cloneDeep(migs).sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
|
if (!migrations.length) {
|
||||||
|
throw new Error('No migration data');
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations.map(migration => {
|
||||||
|
const data = migration.data;
|
||||||
|
if (!data.up || !data.down) {
|
||||||
|
throw new Error(`The ${migration.id}:${migration.name} does not contain 'up' or 'down' instructions`);
|
||||||
|
} else {
|
||||||
|
migration.up = data.up;
|
||||||
|
migration.down = data.down;
|
||||||
|
}
|
||||||
|
delete migration.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a database table for migrations meta data if it doesn't exist
|
||||||
|
// id, name, up, down
|
||||||
|
await db.create({
|
||||||
|
table,
|
||||||
|
quietIfExists: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the list of already applied migrations
|
||||||
|
let dbMigrations = await db.select({
|
||||||
|
table,
|
||||||
|
sort: '(a, b) => a.id - b.id'
|
||||||
|
});
|
||||||
|
|
||||||
|
const execUpDown = async(items) => {
|
||||||
|
for (const item of items) {
|
||||||
|
const action = item[0];
|
||||||
|
await db[action](item[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Undo migrations that exist only in the database but not in migs,
|
||||||
|
// also undo the last migration if the undoLastMigration
|
||||||
|
const lastMigration = migrations[migrations.length - 1];
|
||||||
|
for (const migration of dbMigrations.slice().sort((a, b) => b.id - a.id)) {
|
||||||
|
if (!migrations.some(x => x.id === migration.id) ||
|
||||||
|
(undoLastMigration && migration.id === lastMigration.id)) {
|
||||||
|
await execUpDown(migration.down);
|
||||||
|
await db.delete({
|
||||||
|
table,
|
||||||
|
where: `@@id(${db.esc(migration.id)})`
|
||||||
|
});
|
||||||
|
dbMigrations = dbMigrations.filter(x => x.id !== migration.id);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pending migrations
|
||||||
|
let applied = [];
|
||||||
|
const lastMigrationId = dbMigrations.length ? dbMigrations[dbMigrations.length - 1].id : 0;
|
||||||
|
for (const migration of migrations) {
|
||||||
|
if (migration.id > lastMigrationId) {
|
||||||
|
await execUpDown(migration.up);
|
||||||
|
await db.insert({
|
||||||
|
table,
|
||||||
|
rows: [migration],
|
||||||
|
});
|
||||||
|
applied.push(migration.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
if (!this.inited)
|
||||||
|
throw new Error('JembaConnManager not inited');
|
||||||
|
|
||||||
|
return this._db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = JembaConnManager;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//TODO: удалить модуль в 2023г
|
||||||
const sqlite3 = require('sqlite3');
|
const sqlite3 = require('sqlite3');
|
||||||
const sqlite = require('sqlite');
|
const sqlite = require('sqlite');
|
||||||
|
|
||||||
|
|||||||
4
server/db/jembaMigrations/index.js
Normal file
4
server/db/jembaMigrations/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
//'app': require('./jembaMigrations/app'),
|
||||||
|
'reader-storage': require('./reader-storage'),
|
||||||
|
};
|
||||||
13
server/db/jembaMigrations/reader-storage/001-create.js
Normal file
13
server/db/jembaMigrations/reader-storage/001-create.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: [
|
||||||
|
//CREATE TABLE storage (id TEXT PRIMARY KEY, rev INTEGER, time INTEGER, data TEXT);
|
||||||
|
['create', {
|
||||||
|
table: 'storage'
|
||||||
|
}],
|
||||||
|
],
|
||||||
|
down: [
|
||||||
|
['drop', {
|
||||||
|
table: 'storage'
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
};
|
||||||
6
server/db/jembaMigrations/reader-storage/index.js
Normal file
6
server/db/jembaMigrations/reader-storage/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
table: 'migration1',
|
||||||
|
data: [
|
||||||
|
{id: 1, name: 'create', data: require('./001-create')}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ const compression = require('compression');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const WebSocket = require ('ws');
|
const WebSocket = require ('ws');
|
||||||
|
|
||||||
|
const ayncExit = new (require('./core/AsyncExit'))();
|
||||||
|
|
||||||
|
let log = null;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
//config
|
//config
|
||||||
const configManager = new (require('./config'))();//singleton
|
const configManager = new (require('./config'))();//singleton
|
||||||
@@ -18,7 +22,7 @@ async function init() {
|
|||||||
//logger
|
//logger
|
||||||
const appLogger = new (require('./core/AppLogger'))();//singleton
|
const appLogger = new (require('./core/AppLogger'))();//singleton
|
||||||
await appLogger.init(config);
|
await appLogger.init(config);
|
||||||
const log = appLogger.log;
|
log = appLogger.log;
|
||||||
|
|
||||||
//dirs
|
//dirs
|
||||||
log(`${config.name} v${config.version}, Node.js ${process.version}`);
|
log(`${config.name} v${config.version}, Node.js ${process.version}`);
|
||||||
@@ -41,6 +45,13 @@ async function init() {
|
|||||||
//connections
|
//connections
|
||||||
const connManager = new (require('./db/ConnManager'))();//singleton
|
const connManager = new (require('./db/ConnManager'))();//singleton
|
||||||
await connManager.init(config);
|
await connManager.init(config);
|
||||||
|
|
||||||
|
const jembaConnManager = new (require('./db/JembaConnManager'))();//singleton
|
||||||
|
await jembaConnManager.init(config, argv['auto-repair']);
|
||||||
|
|
||||||
|
//converter SQLITE => JembaDb
|
||||||
|
const converter = new (require('./db/Converter'))();
|
||||||
|
await converter.run(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -96,13 +107,15 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
try {
|
try {
|
||||||
await init();
|
await init();
|
||||||
await main();
|
await main();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
if (log)
|
||||||
process.exit(1);
|
log(LM_FATAL, e.stack);
|
||||||
|
else
|
||||||
|
console.error(e.stack);
|
||||||
|
ayncExit.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -31,7 +31,6 @@ function initRoutes(app, wss, config) {
|
|||||||
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
|
['POST', '/api/reader/upload-file', [upload.single('file'), reader.uploadFile.bind(reader)], [aAll], {}],
|
||||||
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
|
['POST', '/api/reader/restore-cached-file', reader.restoreCachedFile.bind(reader), [aAll], {}],
|
||||||
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
|
['POST', '/api/worker/get-state', worker.getState.bind(worker), [aAll], {}],
|
||||||
['POST', '/api/worker/get-state-finish', worker.getStateFinish.bind(worker), [aAll], {}],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
//to app
|
//to app
|
||||||
|
|||||||
Reference in New Issue
Block a user