Compare commits
292 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c06552278 | ||
|
|
000f8dde82 | ||
|
|
9ffc218002 | ||
|
|
68a188f099 | ||
|
|
8829bb3810 | ||
|
|
5164d2f536 | ||
|
|
451538fcf7 | ||
|
|
82a02ef339 | ||
|
|
b834d4951f | ||
|
|
edc3b669be | ||
|
|
522826311d | ||
|
|
e69b9951d5 | ||
|
|
c6300222ea | ||
|
|
5aa6ee899c | ||
|
|
4b76f97d2b | ||
|
|
5ccfe71c55 | ||
|
|
97fc902cdb | ||
|
|
7e935951d7 | ||
|
|
810c6d68d2 | ||
|
|
003dc70f4f | ||
|
|
371ff64a95 | ||
|
|
b0de5adbf3 | ||
|
|
d1d2b07c33 | ||
|
|
d9b2444c1a | ||
|
|
e7fae27031 | ||
|
|
eb0c7b0a32 | ||
|
|
3d7ad0dd9a | ||
|
|
ae04feb311 | ||
|
|
7b59f911ef | ||
|
|
d3444da647 | ||
|
|
66738d0c9c | ||
|
|
7e187acd68 | ||
|
|
c751372a54 | ||
|
|
7fc98fc7da | ||
|
|
b56f45694e | ||
|
|
091ca521ef | ||
|
|
c7a17b0a76 | ||
|
|
26468b996a | ||
|
|
c4e240d87c | ||
|
|
04713f47c8 | ||
|
|
37ab3493db | ||
|
|
a4cb3c628e | ||
|
|
8492da8a13 | ||
|
|
98d7c64a56 | ||
|
|
25f121e5ed | ||
|
|
4c8797c99c | ||
|
|
1155aa285d | ||
|
|
239bbb8263 | ||
|
|
e6b9330108 | ||
|
|
935b767c2e | ||
|
|
8acf3295b5 | ||
|
|
48c3a12fa0 | ||
|
|
a1dea514b7 | ||
|
|
d4788439cb | ||
|
|
0a60ad354c | ||
|
|
c565a20344 | ||
|
|
735ee88f0b | ||
|
|
9405ce2cc0 | ||
|
|
115277d88a | ||
|
|
6925c11dbd | ||
|
|
984d835892 | ||
|
|
23353a4960 | ||
|
|
955bcda032 | ||
|
|
81ad5d7a2c | ||
|
|
dada7980ec | ||
|
|
511a308646 | ||
|
|
65c8f2cc81 | ||
|
|
238c18bc48 | ||
|
|
873a08fee1 | ||
|
|
7e89228803 | ||
|
|
fc630923a4 | ||
|
|
928f911d03 | ||
|
|
7ffcd3fe1b | ||
|
|
0efbaf643a | ||
|
|
f1bf8e54ae | ||
|
|
b4aa6ab6c8 | ||
|
|
72431f0202 | ||
|
|
04a326c0e4 | ||
|
|
931966f4f3 | ||
|
|
8808cc4779 | ||
|
|
988c959eba | ||
|
|
c0b658d9e6 | ||
|
|
3190246f34 | ||
|
|
d957b4a5f9 | ||
|
|
bef9e5705c | ||
|
|
eb2affa518 | ||
|
|
07b9a3c033 | ||
|
|
3ca14ae06a | ||
|
|
7caa0c2112 | ||
|
|
9c69f5bc01 | ||
|
|
125a2e0f17 | ||
|
|
1b4360b897 | ||
|
|
4775d6e47b | ||
|
|
33fc553c55 | ||
|
|
25cad81c50 | ||
|
|
02a2099c1f | ||
|
|
1cda186b1a | ||
|
|
f10291b6c6 | ||
|
|
26ab5d6765 | ||
|
|
5edeed0747 | ||
|
|
c878ce432f | ||
|
|
81798897c8 | ||
|
|
63840fadbc | ||
|
|
36aa057035 | ||
|
|
30afd2421c | ||
|
|
53a1d90bd8 | ||
|
|
2ecf6beef2 | ||
|
|
85910a20e9 | ||
|
|
66cf7790b3 | ||
|
|
4a9eb7e4bb | ||
|
|
07446696c1 | ||
|
|
a29f9d9a4b | ||
|
|
d49c9baec3 | ||
|
|
8c9d4a12ee | ||
|
|
fce69e4657 | ||
|
|
b387509f88 | ||
|
|
8dc8bdc0d6 | ||
|
|
00caae8363 | ||
|
|
2ead8570a7 | ||
|
|
408315466b | ||
|
|
c651836554 | ||
|
|
03a1e70fce | ||
|
|
ab5a11a24f | ||
|
|
8cd6ed472c | ||
|
|
055181b744 | ||
|
|
e331a3920b | ||
|
|
c62bccb470 | ||
|
|
ea351ea293 | ||
|
|
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 | ||
|
|
e6f6cd4ff3 | ||
|
|
7deb745651 | ||
|
|
70f3ca8067 | ||
|
|
bb8497a997 | ||
|
|
2127e2ec0a | ||
|
|
9be4011d54 | ||
|
|
c534edfeb5 | ||
|
|
adc8cd7243 | ||
|
|
522d2d3b9c | ||
|
|
046933a05e | ||
|
|
9143288de2 | ||
|
|
6053ca6c0e | ||
|
|
084197530e | ||
|
|
9f366ca811 | ||
|
|
7c07e6f004 | ||
|
|
3d4d7e0342 | ||
|
|
1a8f241aad | ||
|
|
33e938b76a | ||
|
|
e2db546066 | ||
|
|
def9ee52e2 | ||
|
|
1afe10be03 | ||
|
|
fa44641fa2 | ||
|
|
9a1ef85c93 | ||
|
|
b848cf5aa7 | ||
|
|
8057e18ebc | ||
|
|
76e09ef34e | ||
|
|
00cb2dc274 | ||
|
|
ed46e91432 | ||
|
|
88d75fb0d8 | ||
|
|
a1d7a73459 | ||
|
|
687f89729b | ||
|
|
6bf678e01f | ||
|
|
a18aec2f96 | ||
|
|
1c0cf303a0 | ||
|
|
5c7ae73982 | ||
|
|
4e9c69a1cf | ||
|
|
78375be8bf | ||
|
|
b684725094 | ||
|
|
ff52602c3a | ||
|
|
ce704c5e26 | ||
|
|
4503e4ed17 | ||
|
|
01c384c43a | ||
|
|
dda2de58a8 | ||
|
|
0365acbf7a | ||
|
|
bbf1ab7180 | ||
|
|
83bf1f1d3a | ||
|
|
fdf04fed0e | ||
|
|
acce32bfa7 | ||
|
|
614c45ac7d | ||
|
|
c4c0199a1b | ||
|
|
a53ebb9355 | ||
|
|
06e12930c7 | ||
|
|
0f7655773a | ||
|
|
26660461d4 | ||
|
|
b41ee91db5 | ||
|
|
746dd8d37a | ||
|
|
fb4a57027d | ||
|
|
c97660bed0 | ||
|
|
fd8c8812a3 | ||
|
|
0101392858 | ||
|
|
cc3f82d693 | ||
|
|
d21997c918 | ||
|
|
74fec12f5c | ||
|
|
59525f8fa7 | ||
|
|
3c6d3befb2 | ||
|
|
dfa72c80bc | ||
|
|
830c066ebf | ||
|
|
c432388515 | ||
|
|
476deba93a | ||
|
|
ffb4f2386d |
6
.babelrc
Normal file
6
.babelrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"presets": [['@babel/preset-env', { "targets": { "esmodules": true } }]],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }]
|
||||
]
|
||||
}
|
||||
19
.eslintrc
19
.eslintrc
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
"parser": "@babel/eslint-parser",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/essential"
|
||||
"plugin:vue/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"vue",
|
||||
"html",
|
||||
"node"
|
||||
"@babel"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
@@ -24,6 +25,14 @@
|
||||
"LM_TOTAL": false
|
||||
},
|
||||
"rules": {
|
||||
"vue/html-indent": ["warn", 4, {
|
||||
"alignAttributesVertically": false
|
||||
}],
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/no-v-html": "off",
|
||||
"vue/no-v-model-argument": "off",
|
||||
|
||||
"strict": 0,
|
||||
"indent": [0, 4, {
|
||||
"SwitchCase": 1
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Браузерная онлайн-читалка книг и децентрализованная библиотека.
|
||||
|
||||
Читалка [OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||
Читалка <img src="https://omnireader.ru/favicon.ico" width="14px"/>[OmniReader](https://omnireader.ru) является частью данного проекта, размещенной на VPS:
|
||||
|
||||

|
||||

|
||||
@@ -11,7 +11,7 @@
|
||||
Для разворачивания читалки на чистом VPS с нуля смотрите [docs/omnireader.ru](docs/omnireader.ru/README.md)
|
||||
|
||||
## Сборка проекта
|
||||
Необходима версия node.js не ниже 10.
|
||||
Необходима версия node.js не ниже 14.
|
||||
|
||||
```
|
||||
$ git clone https://github.com/bookpauk/liberama
|
||||
|
||||
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
@@ -24,12 +24,13 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-linux-x64/node_sqlite3.node`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-linux-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-linux-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
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}`);
|
||||
|
||||
//распаковываем
|
||||
@@ -46,7 +47,8 @@ async function main() {
|
||||
// Скачиваем ipfs
|
||||
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}`);
|
||||
|
||||
//распаковываем
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
const path = require('path');
|
||||
//const webpack = require('webpack');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const DefinePlugin = require('webpack').DefinePlugin;
|
||||
const { VueLoaderPlugin } = require('vue-loader');
|
||||
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
alias: {
|
||||
ws: false,
|
||||
//vue: '@vue/compat'
|
||||
}
|
||||
},
|
||||
entry: [`${clientDir}/main.js`],
|
||||
output: {
|
||||
publicPath: '/app/',
|
||||
@@ -14,58 +20,61 @@ module.exports = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader"
|
||||
loader: 'vue-loader',
|
||||
/*options: {
|
||||
compilerOptions: {
|
||||
compatConfig: {
|
||||
MODE: 2
|
||||
}
|
||||
}
|
||||
}*/
|
||||
},
|
||||
{
|
||||
test: /\.includer$/,
|
||||
resourceQuery: /^\?vue/,
|
||||
use: path.resolve('build/includer.js')
|
||||
use: path.resolve(__dirname, 'includer.js')
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
query: {
|
||||
options: {
|
||||
presets: [['@babel/preset-env', { targets: { esmodules: true } }]],
|
||||
plugins: [
|
||||
'syntax-dynamic-import',
|
||||
'transform-decorators-legacy',
|
||||
'transform-class-properties',
|
||||
// ["component", { "libraryName": "element-ui", "styleLibraryName": `~${clientDir}/theme` } ]
|
||||
['@babel/plugin-proposal-decorators', { legacy: true }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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: /\.(gif|png)$/,
|
||||
type: 'asset/inline',
|
||||
},
|
||||
{
|
||||
test: /\.jpg$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "images/[name]-[hash:6].[ext]"
|
||||
}
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'images/[name]-[hash:6][ext]'
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|woff|woff2)$/,
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "fonts/[name]-[hash:6].[ext]"
|
||||
}
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name]-[hash:6][ext]'
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
__QUASAR_SSR__: false,
|
||||
__QUASAR_SSR_SERVER__: false,
|
||||
__QUASAR_SSR_CLIENT__: false,
|
||||
__QUASAR_VERSION__: false,
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const { merge } = require('webpack-merge');
|
||||
const baseWpConfig = require('./webpack.base.config');
|
||||
|
||||
baseWpConfig.entry.unshift('webpack-hot-middleware/client');
|
||||
@@ -13,7 +13,7 @@ const clientDir = path.resolve(__dirname, '../client');
|
||||
|
||||
module.exports = merge(baseWpConfig, {
|
||||
mode: 'development',
|
||||
devtool: "#inline-source-map",
|
||||
devtool: 'inline-source-map',
|
||||
output: {
|
||||
path: `${publicDir}/app`,
|
||||
filename: 'bundle.js'
|
||||
@@ -38,6 +38,6 @@ module.exports = merge(baseWpConfig, {
|
||||
template: `${clientDir}/index.html.template`,
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
||||
new CopyWebpackPlugin({patterns: [{from: `${clientDir}/assets/*`, to: `${publicDir}/`}]})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const path = require('path');
|
||||
//const webpack = require('webpack');
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const { merge } = require('webpack-merge');
|
||||
const baseWpConfig = require('./webpack.base.config');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
//const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const {GenerateSW} = require('workbox-webpack-plugin');
|
||||
@@ -34,19 +34,18 @@ module.exports = merge(baseWpConfig, {
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
output: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin()
|
||||
new CssMinimizerWebpackPlugin()
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([publicDir], {root: path.resolve(__dirname, '..')}),
|
||||
//new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [`${publicDir}/**`] }),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].[contenthash].css"
|
||||
}),
|
||||
@@ -54,7 +53,9 @@ module.exports = merge(baseWpConfig, {
|
||||
template: `${clientDir}/index.html.template`,
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||
new CopyWebpackPlugin({patterns:
|
||||
[{from: `${clientDir}/assets/*`, to: `${publicDir}/`, context: `${clientDir}/assets` }]
|
||||
}),
|
||||
new GenerateSW({
|
||||
cacheId: 'liberama',
|
||||
swDest: `${publicDir}/service-worker.js`,
|
||||
|
||||
12
build/win.js
12
build/win.js
@@ -4,7 +4,7 @@ const util = require('util');
|
||||
const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const axios = require('axios');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
@@ -24,12 +24,13 @@ async function main() {
|
||||
await fs.ensureDir(tempDownloadDir);
|
||||
|
||||
//sqlite3
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v4.1.1/node-v72-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/node-v72-win32-x64/node_sqlite3.node`;
|
||||
const sqliteRemoteUrl = 'https://mapbox-node-binary.s3.amazonaws.com/sqlite3/v5.0.2/napi-v3-win32-x64.tar.gz';
|
||||
const sqliteDecompressedFilename = `${tempDownloadDir}/napi-v3-win32-x64/node_sqlite3.node`;
|
||||
|
||||
if (!await fs.pathExists(sqliteDecompressedFilename)) {
|
||||
// Скачиваем node_sqlite3.node для винды, т.к. pkg не включает его в сборку
|
||||
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}`);
|
||||
|
||||
//распаковываем
|
||||
@@ -46,7 +47,8 @@ async function main() {
|
||||
// Скачиваем ipfs
|
||||
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}`);
|
||||
|
||||
//распаковываем
|
||||
|
||||
@@ -9,7 +9,7 @@ class Misc {
|
||||
async loadConfig() {
|
||||
|
||||
const query = {params: [
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
|
||||
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'acceptFileExt', 'branch',
|
||||
]};
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import * as utils from '../share/utils';
|
||||
import * as cryptoUtils from '../share/cryptoUtils';
|
||||
import wsc from './webSocketConnection';
|
||||
|
||||
const api = axios.create({
|
||||
@@ -119,32 +120,7 @@ class Reader {
|
||||
estSize = response.headers['content-length'];
|
||||
}
|
||||
} catch (e) {
|
||||
//восстановим при необходимости файл на сервере из удаленного облака
|
||||
let response = null
|
||||
|
||||
try {
|
||||
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
response = await api.post('/restore-cached-file', {path: url});
|
||||
response = response.data;
|
||||
}
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
const workerId = response.workerId;
|
||||
if (!workerId)
|
||||
throw new Error('Неверный ответ api');
|
||||
|
||||
response = await this.getWorkerStateFinish(workerId);
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
if (response.size && estSize < 0) {
|
||||
estSize = response.size;
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
return estSize;
|
||||
@@ -174,11 +150,10 @@ class Reader {
|
||||
return await axios.get(url, options);
|
||||
}
|
||||
|
||||
async uploadFile(file, maxUploadFileSize, callback) {
|
||||
if (!maxUploadFileSize)
|
||||
maxUploadFileSize = 10*1024*1024;
|
||||
async uploadFile(file, maxUploadFileSize = 10*1024*1024, callback) {
|
||||
if (file.size > maxUploadFileSize)
|
||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
|
||||
@@ -219,12 +194,41 @@ class Reader {
|
||||
const state = response.state;
|
||||
if (!state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (response.state == 'error') {
|
||||
if (state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
makeUrlFromBuf(buf) {
|
||||
const key = utils.toHex(cryptoUtils.sha256(buf));
|
||||
return `disk://${key}`;
|
||||
}
|
||||
|
||||
async uploadFileBuf(buf, url) {
|
||||
if (!url)
|
||||
url = this.makeUrlFromBuf(buf);
|
||||
|
||||
let response;
|
||||
try {
|
||||
await axios.head(url.replace('disk://', '/upload/'), {headers: {'Cache-Control': 'no-cache'}});
|
||||
response = await wsc.message(await wsc.send({action: 'upload-file-touch', url}));
|
||||
} catch (e) {
|
||||
response = await wsc.message(await wsc.send({action: 'upload-file-buf', buf}));
|
||||
}
|
||||
|
||||
if (response.error)
|
||||
throw new Error(response.error);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getUploadedFileBuf(url) {
|
||||
url = url.replace('disk://', '/upload/');
|
||||
return (await axios.get(url)).data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new Reader();
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 1.3 KiB |
@@ -1,25 +1,28 @@
|
||||
<template>
|
||||
<div class="fit row">
|
||||
<Notify ref="notify"/>
|
||||
<StdDialog ref="stdDialog"/>
|
||||
<keep-alive v-if="showPage">
|
||||
<router-view class="col"></router-view>
|
||||
</keep-alive>
|
||||
<Notify ref="notify" />
|
||||
<StdDialog ref="stdDialog" />
|
||||
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive v-if="showPage">
|
||||
<component :is="Component" class="col" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from './vueComponent.js';
|
||||
|
||||
import Notify from './share/Notify.vue';
|
||||
import StdDialog from './share/StdDialog.vue';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
|
||||
import miscApi from '../api/misc';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Notify,
|
||||
StdDialog,
|
||||
@@ -31,8 +34,9 @@ export default @Component({
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
class App extends Vue {
|
||||
};
|
||||
class App {
|
||||
_options = componentOptions;
|
||||
showPage = false;
|
||||
|
||||
itemRuText = {
|
||||
@@ -54,7 +58,7 @@ class App extends Vue {
|
||||
//root route
|
||||
let cachedRoute = '';
|
||||
let cachedPath = '';
|
||||
this.$root.rootRoute = () => {
|
||||
this.$root.getRootRoute = () => {
|
||||
if (this.$route.path != cachedPath) {
|
||||
cachedPath = this.$route.path;
|
||||
const m = cachedPath.match(/^(\/[^/]*).*$/i);
|
||||
@@ -73,46 +77,50 @@ class App extends Vue {
|
||||
}
|
||||
});
|
||||
|
||||
// set-app-title
|
||||
this.$root.$on('set-app-title', this.setAppTitle);
|
||||
this.$root.isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
|
||||
//global keyHooks
|
||||
this.keyHooks = [];
|
||||
this.keyHook = (event) => {
|
||||
for (const hook of this.keyHooks)
|
||||
// setAppTitle
|
||||
this.$root.setAppTitle = this.setAppTitle;
|
||||
|
||||
//sanitize
|
||||
this.$root.sanitize = sanitizeHtml;
|
||||
|
||||
//global event hooks
|
||||
this.eventHooks = {};
|
||||
this.$root.eventHook = (hookName, event) => {
|
||||
if (!this.eventHooks[hookName])
|
||||
return;
|
||||
for (const hook of this.eventHooks[hookName])
|
||||
hook(event);
|
||||
}
|
||||
|
||||
this.$root.addKeyHook = (hook) => {
|
||||
if (this.keyHooks.indexOf(hook) < 0)
|
||||
this.keyHooks.push(hook);
|
||||
this.$root.addEventHook = (hookName, hook) => {
|
||||
if (!this.eventHooks[hookName])
|
||||
this.eventHooks[hookName] = [];
|
||||
if (this.eventHooks[hookName].indexOf(hook) < 0)
|
||||
this.eventHooks[hookName].push(hook);
|
||||
}
|
||||
|
||||
this.$root.removeKeyHook = (hook) => {
|
||||
const i = this.keyHooks.indexOf(hook);
|
||||
this.$root.removeEventHook = (hookName, hook) => {
|
||||
if (!this.eventHooks[hookName])
|
||||
return;
|
||||
const i = this.eventHooks[hookName].indexOf(hook);
|
||||
if (i >= 0)
|
||||
this.keyHooks.splice(i, 1);
|
||||
this.eventHooks[hookName].splice(i, 1);
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', (event) => {
|
||||
this.keyHook(event);
|
||||
this.$root.eventHook('key', event);
|
||||
});
|
||||
document.addEventListener('keypress', (event) => {
|
||||
this.keyHook(event);
|
||||
this.$root.eventHook('key', event);
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
this.keyHook(event);
|
||||
this.$root.eventHook('key', event);
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
this.$root.$emit('resize');
|
||||
});
|
||||
}
|
||||
|
||||
routerReady() {
|
||||
return new Promise ((resolve) => {
|
||||
this.$router.onReady(() => {
|
||||
resolve();
|
||||
});
|
||||
window.addEventListener('resize', (event) => {
|
||||
this.$root.eventHook('resize', event);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,14 +150,14 @@ class App extends Vue {
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist();
|
||||
}
|
||||
await this.routerReady();
|
||||
await this.$router.isReady();
|
||||
this.redirectIfNeeded();
|
||||
})();
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
this.commit('uistate/setAsideBarCollapse', !this.uistate.asideBarCollapse);
|
||||
this.$root.$emit('resize');
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
|
||||
get isCollapse() {
|
||||
@@ -184,7 +192,7 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
get rootRoute() {
|
||||
return this.$root.rootRoute();
|
||||
return this.$root.getRootRoute();
|
||||
}
|
||||
|
||||
setAppTitle(title) {
|
||||
@@ -194,7 +202,7 @@ class App extends Vue {
|
||||
} else if (this.mode == 'omnireader') {
|
||||
document.title = `Omni Reader - всегда с вами`;
|
||||
} else if (this.config && this.mode !== null) {
|
||||
document.title = `${this.config.name} - ${this.itemRuText[this.$root.rootRoute]}`;
|
||||
document.title = `${this.config.name} - ${this.itemRuText[this.rootRoute]}`;
|
||||
}
|
||||
} else {
|
||||
document.title = title;
|
||||
@@ -239,6 +247,8 @@ class App extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(App);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Book extends Vue {
|
||||
class Book {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Book);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Card extends Vue {
|
||||
class Card {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Card);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
const selfRoute = '/cardindex';
|
||||
@@ -21,20 +22,32 @@ const tab2Route = [
|
||||
];
|
||||
let lastActiveTab = null;
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
selectedTab: function(newValue, oldValue) {
|
||||
selectedTab: function(newValue) {
|
||||
lastActiveTab = newValue;
|
||||
this.setRouteByTab(newValue);
|
||||
},
|
||||
curRoute: function(newValue, oldValue) {
|
||||
curRoute: function(newValue) {
|
||||
this.setTabByRoute(newValue);
|
||||
},
|
||||
},
|
||||
})
|
||||
class CardIndex extends Vue {
|
||||
};
|
||||
class CardIndex {
|
||||
_options = componentOptions;
|
||||
selectedTab = null;
|
||||
|
||||
created() {
|
||||
this.$watch(
|
||||
() => this.$route.path,
|
||||
(newValue) => {
|
||||
if (newValue == '/cardindex' && this.isReader) {
|
||||
this.$router.replace({ path: '/reader' });
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.setTabByRoute(this.curRoute);
|
||||
}
|
||||
@@ -57,12 +70,22 @@ class CardIndex extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get curRoute() {
|
||||
const m = this.$route.path.match(/^(\/[^\/]*\/[^\/]*).*$/i);
|
||||
const m = this.$route.path.match(/^(\/[^/]*\/[^/]*).*$/i);
|
||||
return (m ? m[1] : this.$route.path);
|
||||
}
|
||||
|
||||
get isReader() {
|
||||
return (this.mode !== null && (this.mode == 'reader' || this.mode == 'omnireader' || this.mode == 'liberama.top'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(CardIndex);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class History extends Vue {
|
||||
class History {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(History);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Search extends Vue {
|
||||
class Search {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Search);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,63 +1,79 @@
|
||||
<template>
|
||||
<Window ref="window" width="600px" height="95%" @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Настроить закладки
|
||||
</template>
|
||||
|
||||
<div class="col column fit">
|
||||
<div class="row items-center top-panel bg-grey-3">
|
||||
<q-btn class="q-mr-md" round dense color="blue" icon="la la-check" @click.stop="openSelected" size="16px" :disabled="!selected">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть выбранную закладку</q-tooltip>
|
||||
<q-btn :disabled="!selected" class="q-mr-md" round dense color="blue" icon="la la-check" size="16px" @click.stop="openSelected">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Открыть выбранную закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-input class="col" ref="search" rounded outlined dense bg-color="white" placeholder="Найти" v-model="search">
|
||||
<template v-slot:append>
|
||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch"/>
|
||||
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
|
||||
<template #append>
|
||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col row">
|
||||
<div class="left-panel column items-center no-wrap bg-grey-3">
|
||||
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="14px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||
<q-btn class="q-my-sm" round dense color="blue" icon="la la-plus" size="14px" @click.stop="addBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Добавить закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-minus" @click.stop="delBookmark" size="14px" :disabled="!ticked.length">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить отмеченные закладки</q-tooltip>
|
||||
<q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-minus" size="14px" @click.stop="delBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Удалить отмеченные закладки
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-edit" @click.stop="editBookmark" size="14px" :disabled="!selected || selected.indexOf('r-') == 0">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Редактировать закладку</q-tooltip>
|
||||
<q-btn :disabled="!selected || selected.indexOf('r-') == 0" class="q-mb-sm" round dense color="blue" icon="la la-edit" size="14px" @click.stop="editBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Редактировать закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" @click.stop="moveBookmark(false)" size="14px" :disabled="!ticked.length">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вверх</q-tooltip>
|
||||
<q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-up" size="14px" @click.stop="moveBookmark(false)">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Переместить отмеченные вверх
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" @click.stop="moveBookmark(true)" size="14px" :disabled="!ticked.length">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Переместить отмеченные вниз</q-tooltip>
|
||||
<q-btn :disabled="!ticked.length" class="q-mb-sm" round dense color="blue" icon="la la-arrow-down" size="14px" @click.stop="moveBookmark(true)">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Переместить отмеченные вниз
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" @click.stop="setDefaultBookmarks" size="14px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Установить по умолчанию</q-tooltip>
|
||||
<q-btn class="q-mb-sm" round dense color="blue" icon="la la-broom" size="14px" @click.stop="setDefaultBookmarks">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Установить по умолчанию
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<div class="space"/>
|
||||
<div class="space" />
|
||||
</div>
|
||||
|
||||
<div class="col fit tree">
|
||||
<div v-show="nodes.length" class="checkbox-tick-all">
|
||||
<q-checkbox v-model="tickAll" @input="makeTickAll" size="36px" label="Выбрать все" />
|
||||
<q-checkbox v-model="tickAll" size="36px" label="Выбрать все" @update:model-value="makeTickAll" />
|
||||
</div>
|
||||
<q-tree
|
||||
v-model:selected="selected"
|
||||
v-model:ticked="ticked"
|
||||
v-model:expanded="expanded"
|
||||
class="q-my-xs"
|
||||
:nodes="nodes"
|
||||
node-key="key"
|
||||
tick-strategy="leaf"
|
||||
:selected.sync="selected"
|
||||
:ticked.sync="ticked"
|
||||
:expanded.sync="expanded"
|
||||
selected-color="black"
|
||||
:filter="search"
|
||||
no-nodes-label="Закладок пока нет"
|
||||
no-results-label="Ничего не найдено"
|
||||
>
|
||||
<template v-slot:default-header="p">
|
||||
<div class="q-px-xs" :class="{selected: selected == p.key}">{{ p.node.label }}</div>
|
||||
<template #default-header="p">
|
||||
<div class="q-px-xs" :class="{selected: selected == p.key}">
|
||||
{{ p.node.label }}
|
||||
</div>
|
||||
</template>
|
||||
</q-tree>
|
||||
</div>
|
||||
@@ -68,32 +84,31 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as lu from '../linkUtils';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
|
||||
const BookmarkSettingsProps = Vue.extend({
|
||||
props: {
|
||||
libs: Object,
|
||||
addBookmarkVisible: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
ticked: function() {
|
||||
ticked() {
|
||||
this.checkAllTicked();
|
||||
},
|
||||
}
|
||||
})
|
||||
class BookmarkSettings extends BookmarkSettingsProps {
|
||||
};
|
||||
class BookmarkSettings {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
libs: Object,
|
||||
addBookmarkVisible: Boolean,
|
||||
};
|
||||
|
||||
search = '';
|
||||
selected = '';
|
||||
ticked = [];
|
||||
@@ -308,6 +323,8 @@ class BookmarkSettings extends BookmarkSettingsProps {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(BookmarkSettings);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,82 +1,114 @@
|
||||
<template>
|
||||
<Window ref="window" @close="close" margin="2px">
|
||||
<template slot="header">
|
||||
<Window ref="window" margin="2px" @close="close">
|
||||
<template #header>
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
<template slot="buttons">
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px"/>
|
||||
<template #buttons>
|
||||
<span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||
<q-icon name="la la-plus" size="16px"/>
|
||||
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
|
||||
<q-icon name="la la-plus" size="16px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||
<q-icon name="la la-minus" size="16px"/>
|
||||
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
|
||||
<q-icon name="la la-minus" size="16px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
|
||||
</span>
|
||||
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||
<q-icon name="la la-question-circle" size="16px"/>
|
||||
<span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
|
||||
<q-icon name="la la-question-circle" size="16px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-show="ready" class="col column" style="min-width: 600px">
|
||||
<div class="row items-center q-px-sm" style="height: 50px">
|
||||
<q-select class="q-mr-sm" ref="rootLink" v-model="rootLink" :options="rootLinkOptions" @input="rootLinkInput"
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
<q-select
|
||||
ref="rootLink"
|
||||
v-model="rootLink"
|
||||
class="q-mr-sm"
|
||||
:options="rootLinkOptions"
|
||||
style="width: 230px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" @click.stop="addBookmark" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить закладку</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn round dense color="blue" icon="la la-bars" @click.stop="bookmarkSettings" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template v-slot:selected>
|
||||
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-select class="q-mr-sm" ref="selectedLink" v-model="selectedLink" :options="selectedLinkOptions" @input="selectedLinkInput" style="width: 50px"
|
||||
outlined dense emit-value map-options display-value-sanitize options-sanitize
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Закладки</q-tooltip>
|
||||
<template #prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-plus" size="12px" @click.stop="addBookmark">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Добавить закладку
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn round dense color="blue" icon="la la-bars" size="12px" @click.stop="bookmarkSettings">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Настроить закладки
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template #selected>
|
||||
<div style="overflow: hidden; white-space: nowrap;">
|
||||
{{ rootLinkWithoutProtocol }}
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги"
|
||||
<q-select
|
||||
ref="selectedLink"
|
||||
v-model="selectedLink"
|
||||
class="q-mr-sm"
|
||||
:options="selectedLinkOptions"
|
||||
style="width: 50px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Закладки
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="bookUrl"
|
||||
class="col q-mr-sm"
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
|
||||
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" @click="goToLink(selectedLink)" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Вернуться на стартовую страницу</q-tooltip>
|
||||
<template #prepend>
|
||||
<q-btn class="q-mr-xs" round dense color="blue" icon="la la-home" size="12px" @click="goToLink(selectedLink)">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Вернуться на стартовую страницу
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn round dense color="blue" icon="la la-angle-double-down" @click="openBookUrlInFrame" size="12px" :disabled="!bookUrl">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Загрузить URL во фрейм</q-tooltip>
|
||||
<q-btn :disabled="!bookUrl" round dense color="blue" icon="la la-angle-double-down" size="12px" @click="openBookUrlInFrame">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Загрузить URL во фрейм
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-btn round dense color="blue" icon="la la-cog" @click.stop="optionsVisible = true" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Опции</q-tooltip>
|
||||
<template #append>
|
||||
<q-btn round dense color="blue" icon="la la-cog" size="12px" @click.stop="optionsVisible = true">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Опции
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn rounded color="green-7" no-caps size="14px" @click="submitUrl" :disabled="!bookUrl">Открыть
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Открыть в читалке</q-tooltip>
|
||||
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
|
||||
Открыть
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Открыть в читалке
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
|
||||
<div class="col fit" ref="frameBox" style="position: relative;">
|
||||
<div ref="frameBox" class="col fit" style="position: relative;">
|
||||
<div ref="frameWrap" class="overflow-hidden">
|
||||
<iframe v-if="frameVisible" ref="frame" :src="frameSrc" frameborder="0"></iframe>
|
||||
</div>
|
||||
@@ -84,41 +116,68 @@
|
||||
</div>
|
||||
|
||||
<Dialog ref="dialogAddBookmark" v-model="addBookmarkVisible">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="row items-center">
|
||||
<q-icon class="q-mr-sm" name="la la-bookmark" size="28px"></q-icon>
|
||||
<div v-if="addBookmarkMode == 'edit'">Редактировать закладку</div>
|
||||
<div v-else>Добавить закладку</div>
|
||||
<div v-if="addBookmarkMode == 'edit'">
|
||||
Редактировать закладку
|
||||
</div>
|
||||
<div v-else>
|
||||
Добавить закладку
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="q-mx-md row">
|
||||
<q-input ref="bookmarkLink" class="col q-mr-sm" outlined dense bg-color="white" v-model="bookmarkLink" @keydown="bookmarkLinkKeyDown"
|
||||
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus">
|
||||
<q-input
|
||||
ref="bookmarkLink"
|
||||
v-model="bookmarkLink"
|
||||
class="col q-mr-sm"
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Ссылка для закладки" maxlength="2000" @focus="selectAllOnFocus" @keydown="bookmarkLinkKeyDown"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-select class="q-mr-sm" ref="defaultRootLink" v-model="defaultRootLink" :options="defaultRootLinkOptions" @input="defaultRootLinkInput" style="width: 50px"
|
||||
<q-select
|
||||
ref="defaultRootLink"
|
||||
v-model="defaultRootLink"
|
||||
class="q-mr-sm"
|
||||
:options="defaultRootLinkOptions"
|
||||
style="width: 50px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
|
||||
>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Предустановленные ссылки</q-tooltip>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Предустановленные ссылки
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md q-mt-md">
|
||||
<q-input class="col q-mr-sm" ref="bookmarkDesc" outlined dense bg-color="white" v-model="bookmarkDesc" @keydown="bookmarkDescKeyDown"
|
||||
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus">
|
||||
<q-input
|
||||
ref="bookmarkDesc"
|
||||
v-model="bookmarkDesc"
|
||||
class="col q-mr-sm"
|
||||
outlined dense
|
||||
bg-color="white"
|
||||
placeholder="Описание" style="width: 400px" maxlength="100" @focus="selectAllOnFocus" @keydown="bookmarkDescKeyDown"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark" :disabled="!bookmarkLink">OK</q-btn>
|
||||
<template #footer>
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</q-btn>
|
||||
<q-btn :disabled="!bookmarkLink" class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okAddBookmark">
|
||||
OK
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="options" v-model="optionsVisible">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<div class="row items-center">
|
||||
<q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
|
||||
Опции
|
||||
@@ -131,22 +190,29 @@
|
||||
<q-checkbox v-model="openInFrameOnAdd" size="36px" label="Активировать новую закладку после добавления" />
|
||||
</div>
|
||||
|
||||
<template slot="footer">
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">OK</q-btn>
|
||||
<template #footer>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="optionsVisible = false">
|
||||
OK
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<BookmarkSettings v-if="bookmarkSettingsActive" ref="bookmarkSettings" :libs="libs" :addBookmarkVisible="addBookmarkVisible"
|
||||
@do-action="doAction" @close="closeBookmarkSettings">
|
||||
<BookmarkSettings
|
||||
v-if="bookmarkSettingsActive"
|
||||
ref="bookmarkSettings"
|
||||
:libs="libs"
|
||||
:add-bookmark-visible="addBookmarkVisible"
|
||||
@do-action="doAction" @close="closeBookmarkSettings"
|
||||
>
|
||||
</BookmarkSettings>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import Window from '../share/Window.vue';
|
||||
@@ -162,20 +228,20 @@ const proxySubst = {
|
||||
'http://fantasy-worlds.org': 'http://b.liberama.top:23580',
|
||||
};
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
Dialog,
|
||||
BookmarkSettings
|
||||
},
|
||||
watch: {
|
||||
libs: function() {
|
||||
libs() {
|
||||
this.loadLibs();
|
||||
},
|
||||
defaultRootLink: function() {
|
||||
defaultRootLink() {
|
||||
this.updateBookmarkLink();
|
||||
},
|
||||
bookUrl: function(newValue) {
|
||||
bookUrl(newValue) {
|
||||
const value = lu.addProtocol(newValue);
|
||||
const subst = this.makeProxySubst(value, true);
|
||||
if (value != subst) {
|
||||
@@ -184,7 +250,7 @@ export default @Component({
|
||||
});
|
||||
}
|
||||
},
|
||||
bookmarkLink: function(newValue) {
|
||||
bookmarkLink(newValue) {
|
||||
const value = lu.addProtocol(newValue);
|
||||
const subst = this.makeProxySubst(value, true);
|
||||
if (value != subst) {
|
||||
@@ -193,18 +259,26 @@ export default @Component({
|
||||
});
|
||||
}
|
||||
},
|
||||
closeAfterSubmit: function(newValue) {
|
||||
closeAfterSubmit(newValue) {
|
||||
this.commitProp('closeAfterSubmit', newValue);
|
||||
},
|
||||
openInFrameOnEnter: function(newValue) {
|
||||
openInFrameOnEnter(newValue) {
|
||||
this.commitProp('openInFrameOnEnter', newValue);
|
||||
},
|
||||
openInFrameOnAdd: function(newValue) {
|
||||
openInFrameOnAdd(newValue) {
|
||||
this.commitProp('openInFrameOnAdd', newValue);
|
||||
},
|
||||
rootLink() {
|
||||
this.rootLinkInput();
|
||||
},
|
||||
selectedLink() {
|
||||
this.selectedLinkInput();
|
||||
},
|
||||
}
|
||||
})
|
||||
class ExternalLibs extends Vue {
|
||||
};
|
||||
class ExternalLibs {
|
||||
_options = componentOptions;
|
||||
|
||||
ready = false;
|
||||
frameVisible = false;
|
||||
rootLink = '';
|
||||
@@ -233,9 +307,9 @@ class ExternalLibs extends Vue {
|
||||
created() {
|
||||
this.oldStartLink = '';
|
||||
this.justOpened = true;
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
this.$root.addEventHook('key', this.keyHook);
|
||||
|
||||
this.$root.$on('resize', async() => {
|
||||
this.$root.addEventHook('resize', async() => {
|
||||
await utils.sleep(200);
|
||||
this.frameResize();
|
||||
});
|
||||
@@ -252,29 +326,6 @@ class ExternalLibs extends Vue {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
//Поправка метода toggleOption компонента select фреймворка quasar, необходимо другое поведение
|
||||
//$emit('input'.. вызывается всегда
|
||||
this.toggleOption = function(opt, keepOpen) {
|
||||
if (this.editable !== true || opt === void 0 || this.isOptionDisabled(opt) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optValue = this.getOptionValue(opt);
|
||||
|
||||
if (this.multiple !== true) {
|
||||
if (keepOpen !== true) {
|
||||
this.updateInputValue(this.fillInput === true ? this.getOptionLabel(opt) : '', true, true);
|
||||
this.hidePopup();
|
||||
}
|
||||
|
||||
this.$refs.target !== void 0 && this.$refs.target.focus();
|
||||
this.$emit('input', this.emitValue === true ? optValue : opt);
|
||||
}
|
||||
};
|
||||
|
||||
this.$refs.rootLink.toggleOption = this.toggleOption;
|
||||
this.$refs.selectedLink.toggleOption = this.toggleOption;
|
||||
|
||||
(async() => {
|
||||
//подождем this.mode
|
||||
let i = 0;
|
||||
@@ -412,7 +463,7 @@ class ExternalLibs extends Vue {
|
||||
if (this.ready && this.selectedLink) {
|
||||
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||
}
|
||||
this.$root.$emit('set-app-title', result);
|
||||
this.$root.setAppTitle(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -621,9 +672,9 @@ class ExternalLibs extends Vue {
|
||||
|
||||
this.addBookmarkMode = mode;
|
||||
this.addBookmarkVisible = true;
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(async() => {
|
||||
await this.$refs.dialogAddBookmark.waitShown();
|
||||
this.$refs.bookmarkLink.focus();
|
||||
this.$refs.defaultRootLink.toggleOption = this.toggleOption;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,10 +689,6 @@ class ExternalLibs extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
defaultRootLinkInput() {
|
||||
this.updateBookmarkLink();
|
||||
}
|
||||
|
||||
bookmarkLinkKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this.$refs.bookmarkDesc.focus();
|
||||
@@ -704,7 +751,7 @@ class ExternalLibs extends Vue {
|
||||
this.commitLibs(libs);
|
||||
} else if (item.c != this.bookmarkDesc) {
|
||||
if (await this.$root.stdDialog.confirm(`Такая закладка уже существует с другим описанием.<br>` +
|
||||
`Заменить '${this.$sanitize(item.c)}' на '${this.$sanitize(this.bookmarkDesc)}'?`, ' ')) {
|
||||
`Заменить '${this.$root.sanitize(item.c)}' на '${this.$root.sanitize(this.bookmarkDesc)}'?`, ' ')) {
|
||||
item.c = this.bookmarkDesc;
|
||||
this.commitLibs(libs);
|
||||
} else
|
||||
@@ -810,7 +857,7 @@ class ExternalLibs extends Vue {
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.$root.rootRoute() == '/external-libs') {
|
||||
if (this.$root.getRootRoute() == '/external-libs') {
|
||||
if (this.$root.stdDialog.active)
|
||||
return false;
|
||||
|
||||
@@ -836,6 +883,8 @@ class ExternalLibs extends Vue {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ExternalLibs);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -845,14 +894,15 @@ class ExternalLibs extends Vue {
|
||||
background-color: #A0A0A0;
|
||||
}
|
||||
|
||||
.full-screen-button {
|
||||
.header-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.full-screen-button:hover {
|
||||
background-color: #69C05F;
|
||||
.header-button:hover {
|
||||
color: white;
|
||||
background-color: #39902F;
|
||||
}
|
||||
|
||||
.transparent-layout {
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Help extends Vue {
|
||||
class Help {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Help);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Income extends Vue {
|
||||
class Income {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Income);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class NotFound404 extends Vue {
|
||||
class NotFound404 {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(NotFound404);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import {sleep} from '../../../share/utils';
|
||||
import {clickMap, clickMapText} from '../share/clickMap';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class ClickMapPage extends Vue {
|
||||
class ClickMapPage {
|
||||
fontSize = '200%';
|
||||
|
||||
created() {
|
||||
@@ -53,6 +50,8 @@ class ClickMapPage extends Vue {
|
||||
await sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ClickMapPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,125 +1,146 @@
|
||||
<template>
|
||||
<Window width="600px" ref="window" @close="close">
|
||||
<template slot="header">
|
||||
<Window ref="window" width="600px" @close="close">
|
||||
<template #header>
|
||||
Оглавление/закладки
|
||||
</template>
|
||||
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
inline-label
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||
<q-tab name="images" icon="la la-image" label="Изображения" />
|
||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
inline-label
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||
<q-tab name="images" icon="la la-image" label="Изображения" />
|
||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
<div class="q-mb-sm" />
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
||||
<div>
|
||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
|
||||
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
|
||||
<div>
|
||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||
<div :ref="`mainitem${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
|
||||
</div>
|
||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
|
||||
</div>
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div :style="item.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||
<div class="column justify-center">
|
||||
{{ item.perc }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
|
||||
</div>
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div :style="item.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||
<div class="column justify-center">{{ item.perc }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
||||
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
|
||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||
<div class="no-expand-button"></div>
|
||||
<div :style="subitem.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
|
||||
<div class="column justify-center">{{ subitem.perc }}%</div>
|
||||
|
||||
<div v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
|
||||
<div
|
||||
v-for="subitem in item.list"
|
||||
:ref="`subitem${subitem.key}`"
|
||||
:key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}"
|
||||
>
|
||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||
<div class="no-expand-button"></div>
|
||||
<div :style="subitem.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
|
||||
<div class="column justify-center">
|
||||
{{ subitem.perc }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
|
||||
Оглавление отсутствует
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'images'">
|
||||
<div>
|
||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div class="image-thumb-box row justify-center items-center">
|
||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
|
||||
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
|
||||
</div>
|
||||
<div class="no-expand-button column justify-center items-center">
|
||||
<div class="image-num">{{ item.num }}</div>
|
||||
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
|
||||
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
|
||||
<div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
|
||||
</div>
|
||||
<div :style="item.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||
<div class="column justify-center">{{ item.perc }}%</div>
|
||||
</div>
|
||||
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
|
||||
Оглавление отсутствует
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
|
||||
Изображения отсутствуют
|
||||
</div>
|
||||
|
||||
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
|
||||
<div>
|
||||
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||
<div :ref="`image${item.key}`" class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||
<div class="image-thumb-box row justify-center items-center">
|
||||
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center">
|
||||
<i class="loading-img-icon la la-images"></i>
|
||||
</div>
|
||||
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]" />
|
||||
</div>
|
||||
<div class="no-expand-button column justify-center items-center">
|
||||
<div class="image-num">
|
||||
{{ item.num }}
|
||||
</div>
|
||||
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">
|
||||
JPG
|
||||
</div>
|
||||
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">
|
||||
PNG
|
||||
</div>
|
||||
<div v-show="!item.local" class="image-type it-net-color row justify-center">
|
||||
INET
|
||||
</div>
|
||||
</div>
|
||||
<div :style="item.indentStyle"></div>
|
||||
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||
<div class="column justify-center">
|
||||
{{ item.perc }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
|
||||
Изображения отсутствуют
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
|
||||
<div class="column justify-center items-center" style="height: 100px">
|
||||
Раздел находится в разработке
|
||||
<div v-show="selectedTab == 'bookmarks'" class="tab-panel">
|
||||
<div class="column justify-center items-center" style="height: 100px">
|
||||
Раздел находится в разработке
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
//import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ContentsPageProps = Vue.extend({
|
||||
props: {
|
||||
bookPos: Number,
|
||||
isVisible: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
watch: {
|
||||
bookPos: function() {
|
||||
bookPos() {
|
||||
this.updateBookPosSelection();
|
||||
}
|
||||
},
|
||||
selectedTab() {
|
||||
this.updateBookPosScrollTop();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ContentsPage extends ContentsPageProps {
|
||||
};
|
||||
class ContentsPage {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
bookPos: Number,
|
||||
isVisible: Boolean,
|
||||
};
|
||||
|
||||
selectedTab = 'contents';
|
||||
contents = [];
|
||||
images = [];
|
||||
@@ -255,9 +276,9 @@ class ContentsPage extends ContentsPageProps {
|
||||
const {id, local} = ims[i];
|
||||
const bin = this.parsed.binary[id];
|
||||
if (local)
|
||||
this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : ''));
|
||||
this.imageSrc[id] = (bin ? `data:${bin.type};base64,${bin.data}` : '');
|
||||
else
|
||||
this.$set(this.imageSrc, id, id);
|
||||
this.imageSrc[id] = id;
|
||||
this.imageLoaded[id] = true;
|
||||
await utils.sleep(5);
|
||||
}
|
||||
@@ -268,31 +289,30 @@ class ContentsPage extends ContentsPageProps {
|
||||
if (!this.isVisible)
|
||||
return;
|
||||
|
||||
await utils.sleep(50);
|
||||
await this.$nextTick();
|
||||
const bp = this.bookPos;
|
||||
|
||||
for (let i = 0; i < this.contents.length; i++) {
|
||||
const item = this.contents[i];
|
||||
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= item.offset && bp < nextOffset) {
|
||||
item.isBookPos = true;
|
||||
} else if (item.isBookPos) {
|
||||
item.isBookPos = false;
|
||||
}
|
||||
|
||||
for (let j = 0; j < item.list.length; j++) {
|
||||
const subitem = item.list[j];
|
||||
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||
|
||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||
subitem.isBookPos = true;
|
||||
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||
this.updateBookPosScrollTop('contents', item, subitem, j);
|
||||
} else if (subitem.isBookPos) {
|
||||
subitem.isBookPos = false;
|
||||
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||
}
|
||||
}
|
||||
|
||||
if (bp >= item.offset && bp < nextOffset) {
|
||||
this.$set(this.contents, i, Object.assign(item, {isBookPos: true}));
|
||||
} else if (item.isBookPos) {
|
||||
this.$set(this.contents, i, Object.assign(item, {isBookPos: false}));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.images.length; i++) {
|
||||
@@ -300,11 +320,96 @@ class ContentsPage extends ContentsPageProps {
|
||||
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||
|
||||
if (bp >= img.offset && bp < nextOffset) {
|
||||
this.$set(this.images, i, Object.assign(img, {isBookPos: true}));
|
||||
this.images[i].isBookPos = true;
|
||||
} else if (img.isBookPos) {
|
||||
this.$set(this.images, i, Object.assign(img, {isBookPos: false}));
|
||||
this.images[i].isBookPos = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateBookPosScrollTop();
|
||||
}
|
||||
|
||||
/*getOffsetTop(key) {
|
||||
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
|
||||
return (el ? el.offsetTop : 0);
|
||||
}*/
|
||||
|
||||
async updateBookPosScrollTop() {
|
||||
try {
|
||||
await this.$nextTick();
|
||||
|
||||
if (this.selectedTab == 'contents') {
|
||||
let item;
|
||||
let subitem;
|
||||
let i;
|
||||
|
||||
//ищем выделенные item
|
||||
for(const _item of this.contents) {
|
||||
if (_item.isBookPos) {
|
||||
item = _item;
|
||||
for (let ii = 0; ii < item.list.length; ii++) {
|
||||
const _subitem = item.list[ii];
|
||||
if (_subitem.isBookPos) {
|
||||
subitem = _subitem;
|
||||
i = ii;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
//вычисляем и смещаем tabPanel.scrollTop
|
||||
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
|
||||
let elShift = 0;
|
||||
if (subitem && item.expanded) {
|
||||
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
|
||||
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
|
||||
} else {
|
||||
elShift = el.offsetHeight;
|
||||
}
|
||||
|
||||
const tabPanel = this.$refs.tabPanelContents;
|
||||
const halfH = tabPanel.clientHeight/2;
|
||||
const newScrollTop = el.offsetTop - halfH - elShift;
|
||||
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||
tabPanel.scrollTop = newScrollTop;
|
||||
}
|
||||
|
||||
if (this.selectedTab == 'images') {
|
||||
let item;
|
||||
|
||||
//ищем выделенные item
|
||||
for(const _item of this.images) {
|
||||
if (_item.isBookPos) {
|
||||
item = _item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
//вычисляем и смещаем tabPanel.scrollTop
|
||||
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
|
||||
|
||||
const tabPanel = this.$refs.tabPanelImages;
|
||||
const halfH = tabPanel.clientHeight/2;
|
||||
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
|
||||
|
||||
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
|
||||
tabPanel.scrollTop = newScrollTop;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
getFirstElem(items) {
|
||||
return (Array.isArray(items) ? items[0] : items);
|
||||
}
|
||||
|
||||
async expandClick(key) {
|
||||
@@ -312,17 +417,17 @@ class ContentsPage extends ContentsPageProps {
|
||||
const expanded = !item.expanded;
|
||||
|
||||
if (!expanded) {
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
subitems.style.height = '0';
|
||||
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||
subdiv.style.height = '0';
|
||||
await utils.sleep(200);
|
||||
}
|
||||
|
||||
this.$set(this.contents, key, Object.assign({}, item, {expanded}));
|
||||
this.contents[key].expanded = expanded;
|
||||
|
||||
if (expanded) {
|
||||
await this.$nextTick();
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
subitems.style.height = subitems.scrollHeight + 'px';
|
||||
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
|
||||
subdiv.style.height = subdiv.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +447,8 @@ class ContentsPage extends ContentsPageProps {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ContentsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Скопировать текст
|
||||
</template>
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
})
|
||||
class CopyTextPage extends Vue {
|
||||
};
|
||||
class CopyTextPage {
|
||||
_options = componentOptions;
|
||||
|
||||
text = null;
|
||||
initStep = null;
|
||||
initPercentage = 0;
|
||||
@@ -101,6 +102,8 @@ class CopyTextPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(CopyTextPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,15 +18,20 @@
|
||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||
</ul>
|
||||
|
||||
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
||||
<p>
|
||||
В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").
|
||||
</p>
|
||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||
|
||||
<div v-show="mode == 'omnireader' || mode == 'liberama.top'">
|
||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<p>
|
||||
Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>{{ bookmarkText }}</strong>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyText(bookmarkText, 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<br>или перетащив на панель закладок следующую ссылку:
|
||||
@@ -41,14 +46,11 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import {copyTextToClipboard} from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class CommonHelpPage extends Vue {
|
||||
class CommonHelpPage {
|
||||
created() {
|
||||
}
|
||||
|
||||
@@ -69,6 +71,8 @@ class CommonHelpPage extends Vue {
|
||||
this.$root.notify.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(CommonHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,49 +1,68 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="box">
|
||||
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||
<p class="p">
|
||||
Вы можете пожертвовать на развитие проекта любую сумму:
|
||||
</p>
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/yoomoney.png">
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">Пожертвовать</q-btn><br>
|
||||
<div class="para">{{ yooAddress }}
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
|
||||
Пожертвовать
|
||||
</q-btn><br>
|
||||
<div class="para">
|
||||
{{ yooAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<!--div class="address">
|
||||
<img class="logo" src="./assets/paypal.png">
|
||||
<div class="para">{{ paypalAddress }}
|
||||
<div class="para">
|
||||
{{ paypalAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/bitcoin.png">
|
||||
<div class="para">{{ bitcoinAddress }}
|
||||
<div class="para">
|
||||
{{ bitcoinAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/litecoin.png">
|
||||
<div class="para">{{ litecoinAddress }}
|
||||
<div class="para">
|
||||
{{ litecoinAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/monero.png">
|
||||
<div class="para">{{ moneroAddress }}
|
||||
<div class="para">
|
||||
{{ moneroAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,13 +72,11 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import {copyTextToClipboard} from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class DonateHelpPage extends Vue {
|
||||
class DonateHelpPage {
|
||||
yooAddress = '410018702323056';
|
||||
paypalAddress = 'bookpauk@gmail.com';
|
||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||
@@ -81,6 +98,8 @@ class DonateHelpPage extends Vue {
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(DonateHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Справка
|
||||
</template>
|
||||
|
||||
<div class="col column" style="min-width: 600px">
|
||||
<q-btn-toggle
|
||||
v-model="selectedTab"
|
||||
toggle-color="primary"
|
||||
no-caps unelevated
|
||||
:options="buttons"
|
||||
/>
|
||||
<div class="separator"></div>
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
inline-label
|
||||
class="bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<keep-alive>
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
></component>
|
||||
<component :is="activePage" ref="page" class="col"></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</Window>
|
||||
@@ -23,22 +29,21 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
//import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
|
||||
const pages = {
|
||||
'CommonHelpPage': CommonHelpPage,
|
||||
'HotkeysHelpPage': HotkeysHelpPage,
|
||||
'MouseHelpPage': MouseHelpPage,
|
||||
'VersionHistoryPage': VersionHistoryPage,
|
||||
'DonateHelpPage': DonateHelpPage,
|
||||
//'DonateHelpPage': DonateHelpPage,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
@@ -46,13 +51,15 @@ const tabs = [
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
['DonateHelpPage', 'Помочь проекту'],
|
||||
//['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: Object.assign({ Window }, pages),
|
||||
})
|
||||
class HelpPage extends Vue {
|
||||
};
|
||||
class HelpPage {
|
||||
_options = componentOptions;
|
||||
|
||||
selectedTab = 'CommonHelpPage';
|
||||
|
||||
close() {
|
||||
@@ -73,7 +80,7 @@ class HelpPage extends Vue {
|
||||
}
|
||||
|
||||
activateDonateHelpPage() {
|
||||
this.selectedTab = 'DonateHelpPage';
|
||||
//this.selectedTab = 'DonateHelpPage';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
@@ -87,12 +94,10 @@ class HelpPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(HelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div style="font-size: 120%">
|
||||
<div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
|
||||
<div class="text-h6 text-bold">
|
||||
Доступны следующие клавиатурные команды:
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="q-mb-md" style="width: 550px">
|
||||
<div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div>
|
||||
<UserHotKeys v-model="userHotKeys" readonly/>
|
||||
<div class="text-right text-italic" style="font-size: 80%">
|
||||
* Изменить сочетания клавиш можно в настройках
|
||||
</div>
|
||||
<UserHotKeys v-model="userHotKeys" readonly />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
UserHotKeys,
|
||||
},
|
||||
})
|
||||
class HotkeysHelpPage extends Vue {
|
||||
};
|
||||
class HotkeysHelpPage {
|
||||
_options = componentOptions;
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
@@ -36,6 +41,8 @@ class HotkeysHelpPage extends Vue {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(HotkeysHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,21 +3,28 @@
|
||||
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
|
||||
<ul>
|
||||
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
||||
<div class="click-map-page">
|
||||
<ClickMapPage ref="clickMapPage"></ClickMapPage>
|
||||
</div>
|
||||
<div class="click-map-page">
|
||||
<ClickMapPage ref="clickMapPage"></ClickMapPage>
|
||||
</div>
|
||||
|
||||
<li><b>ПКМ</b> - показать/скрыть панель управления</li>
|
||||
<li><b>СКМ</b> - вкл./выкл. плавный скроллинг текста</li>
|
||||
<br>
|
||||
<li>Жесты для тачскрина:</li>
|
||||
<ul>
|
||||
<li style="list-style-type: square">от центра вверх: на весь экран</li>
|
||||
<li style="list-style-type: square">от центра вниз: плавный скроллинг</li>
|
||||
<li style="list-style-type: square">от центра вправо: увеличить скорость скроллинга</li>
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
<li style="list-style-type: square">
|
||||
от центра вверх: на весь экран
|
||||
</li>
|
||||
<li style="list-style-type: square">
|
||||
от центра вниз: плавный скроллинг
|
||||
</li>
|
||||
<li style="list-style-type: square">
|
||||
от центра вправо: увеличить скорость скроллинга
|
||||
</li>
|
||||
<li style="list-style-type: square">
|
||||
от центра влево: уменьшить скорость скроллинга
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</ul>
|
||||
* Для управления с помощью мыши/тачскрина необходимо установить галочку "Включить управление кликом" в настройках
|
||||
</div>
|
||||
@@ -25,17 +32,18 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import ClickMapPage from '../../ClickMapPage/ClickMapPage.vue';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
ClickMapPage,
|
||||
},
|
||||
})
|
||||
class MouseHelpPage extends Vue {
|
||||
};
|
||||
class MouseHelpPage {
|
||||
_options = componentOptions;
|
||||
|
||||
created() {
|
||||
}
|
||||
|
||||
@@ -44,6 +52,8 @@ class MouseHelpPage extends Vue {
|
||||
this.$refs.clickMapPage.$el.style.backgroundColor = '#478355';
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(MouseHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<span class="text-h6 text-bold">История версий:</span>
|
||||
<br><br>
|
||||
|
||||
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
|
||||
<span v-for="(item, index) in versionHeader" :key="index" class="clickable" @click="showRelease(item)">
|
||||
<p>
|
||||
{{ item }}
|
||||
{{ item }}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
@@ -20,13 +20,11 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import {versionHistory} from '../../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class VersionHistoryPage extends Vue {
|
||||
class VersionHistoryPage {
|
||||
versionHeader = [];
|
||||
versionContent = [];
|
||||
|
||||
@@ -35,14 +33,15 @@ class VersionHistoryPage extends Vue {
|
||||
|
||||
mounted() {
|
||||
let vh = [];
|
||||
for (const version of versionHistory) {
|
||||
vh.push(version.header);
|
||||
for (const v of versionHistory) {
|
||||
vh.push(`${v.version} (${v.releaseDate})`);
|
||||
}
|
||||
this.versionHeader = vh;
|
||||
|
||||
let vc = [];
|
||||
for (const version of versionHistory) {
|
||||
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
||||
for (const v of versionHistory) {
|
||||
let header = `${v.version} (${v.releaseDate})`;
|
||||
vc.push({key: header, content: 'Версия ' + header + v.content});
|
||||
}
|
||||
this.versionContent = vc;
|
||||
}
|
||||
@@ -54,6 +53,8 @@ class VersionHistoryPage extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(VersionHistoryPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
//import rstore from '../../../store/modules/reader';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window
|
||||
},
|
||||
@@ -20,8 +20,10 @@ export default @Component({
|
||||
this.sendLibs();
|
||||
},
|
||||
}
|
||||
})
|
||||
class LibsPage extends Vue {
|
||||
};
|
||||
class LibsPage {
|
||||
_options = componentOptions;
|
||||
|
||||
created() {
|
||||
this.popupWindow = null;
|
||||
this.commit = this.$store.commit;
|
||||
@@ -113,13 +115,15 @@ class LibsPage extends Vue {
|
||||
}
|
||||
|
||||
sendLibs() {
|
||||
this.sendMessage({type: 'libs', data: this.libs});
|
||||
this.sendMessage({type: 'libs', data: _.cloneDeep(this.libs)});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('libs-close');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(LibsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div ref="main" class="column no-wrap" style="min-height: 500px">
|
||||
<div v-if="mode != 'liberama.top'" class="relative-position">
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" corner-color="#1B695F" git-color="#EBE2C9"></GithubCorner>
|
||||
</div>
|
||||
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
|
||||
<span class="greeting"><b>{{ title }}</b></span>
|
||||
@@ -12,21 +12,31 @@
|
||||
</div>
|
||||
|
||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
|
||||
<template v-slot:append>
|
||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
|
||||
<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>
|
||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
||||
<input
|
||||
id="file" ref="file" type="file"
|
||||
style="display: none;"
|
||||
:accept="acceptFileExt"
|
||||
@change="loadFile"
|
||||
/>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<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>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<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>
|
||||
|
||||
@@ -45,36 +55,55 @@
|
||||
</div>
|
||||
|
||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="findBook">Найти книгу</span>
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||
<!--span class="bottom-span clickable" @click="openDonate">Помочь проекту</span-->
|
||||
|
||||
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||
</div>
|
||||
|
||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||
|
||||
<Dialog ref="dialog1" v-model="findBookVisible">
|
||||
<template #header>
|
||||
Подсказка ;-)
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Если вы хотите найти определенную книгу, добро пожаловать в
|
||||
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте читалки
|
||||
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import GithubCorner from './GithubCorner/GithubCorner.vue';
|
||||
|
||||
import Dialog from '../../share/Dialog.vue';
|
||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
GithubCorner,
|
||||
Dialog,
|
||||
PasteTextPage,
|
||||
},
|
||||
})
|
||||
class LoaderPage extends Vue {
|
||||
};
|
||||
class LoaderPage {
|
||||
_options = componentOptions;
|
||||
|
||||
bookUrl = null;
|
||||
loadPercent = 0;
|
||||
pasteTextActive = false;
|
||||
findBookVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -107,14 +136,16 @@ class LoaderPage extends Vue {
|
||||
return this.$store.state.config.version;
|
||||
}
|
||||
|
||||
get acceptFileExt() {
|
||||
return this.$store.state.config.acceptFileExt;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
@@ -136,7 +167,7 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.pasteTextToggle();
|
||||
this.showPasteText();
|
||||
}
|
||||
|
||||
loadBuffer(opts) {
|
||||
@@ -146,6 +177,10 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
showPasteText() {
|
||||
this.pasteTextActive = true;
|
||||
}
|
||||
|
||||
pasteTextToggle() {
|
||||
this.pasteTextActive = !this.pasteTextActive;
|
||||
}
|
||||
@@ -158,6 +193,10 @@ class LoaderPage extends Vue {
|
||||
this.$emit('do-action', {action: 'donate'});
|
||||
}
|
||||
|
||||
findBook() {
|
||||
this.findBookVisible = true;
|
||||
}
|
||||
|
||||
openComments() {
|
||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||
}
|
||||
@@ -166,30 +205,30 @@ class LoaderPage extends Vue {
|
||||
window.open('http://old.omnireader.ru', '_blank');
|
||||
}
|
||||
|
||||
async onInputKeydown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
await utils.sleep(100);
|
||||
this.submitUrl();
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.$refs.dialog1.active)
|
||||
return true;
|
||||
|
||||
if (this.pasteTextActive) {
|
||||
return this.$refs.pasteTextPage.keyHook(event);
|
||||
}
|
||||
|
||||
//недостатки сторонних ui
|
||||
const input = this.$refs.input.$refs.input;
|
||||
if (document.activeElement === input && event.type == 'keydown' && event.key == 'Enter') {
|
||||
this.submitUrl();
|
||||
const input = this.$refs.input.getNativeElement();
|
||||
if (event.type == 'keydown' && (document.activeElement === input || event.code == 'Enter') && event.code != 'Escape')
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && document.activeElement !== input) {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
switch (action) {
|
||||
case 'help':
|
||||
this.openHelp(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(LoaderPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<span style="position: relative; top: -3px">
|
||||
Вставьте текст и нажмите
|
||||
<span class="clickable text-primary" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
@@ -8,27 +8,28 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
|
||||
<hr/>
|
||||
<q-input v-model="bookTitle" class="q-px-sm" dense borderless placeholder="Введите название текста" />
|
||||
<hr />
|
||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import Window from '../../../share/Window.vue';
|
||||
import _ from 'lodash';
|
||||
import * as utils from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
})
|
||||
class PasteTextPage extends Vue {
|
||||
};
|
||||
class PasteTextPage {
|
||||
_options = componentOptions;
|
||||
|
||||
bookTitle = '';
|
||||
|
||||
created() {
|
||||
@@ -59,15 +60,19 @@ class PasteTextPage extends Vue {
|
||||
|
||||
calcTitle(event) {
|
||||
if (this.bookTitle == '') {
|
||||
let text = event.clipboardData.getData('text');
|
||||
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}: ` + _.compact([
|
||||
this.getNonEmptyLine3words(text, 1),
|
||||
this.getNonEmptyLine3words(text, 2)
|
||||
]).join(' - ');
|
||||
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}`;
|
||||
if (event) {
|
||||
let text = event.clipboardData.getData('text');
|
||||
this.bookTitle += ': ' + _.compact([
|
||||
this.getNonEmptyLine3words(text, 1),
|
||||
this.getNonEmptyLine3words(text, 2)
|
||||
]).join(' - ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadBuffer() {
|
||||
this.calcTitle();
|
||||
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
|
||||
this.close();
|
||||
}
|
||||
@@ -90,6 +95,8 @@ class PasteTextPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(PasteTextPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
||||
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
|
||||
<div class="column justify-start items-center" style="height: 250px">
|
||||
<q-circular-progress
|
||||
show-value
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div>
|
||||
<span class="text-yellow">{{ text }}</span>
|
||||
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
|
||||
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,8 +25,8 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ruMessage = {
|
||||
@@ -42,9 +42,7 @@ const ruMessage = {
|
||||
'upload': 'отправка',
|
||||
};
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class ProgressPage extends Vue {
|
||||
class ProgressPage {
|
||||
text = '';
|
||||
totalSteps = 1;
|
||||
step = 1;
|
||||
@@ -96,5 +94,7 @@ class ProgressPage extends Vue {
|
||||
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ProgressPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,75 +1,131 @@
|
||||
<template>
|
||||
<div class="column no-wrap">
|
||||
<div ref="header" class="header" v-show="toolBarActive">
|
||||
<div v-show="toolBarActive" ref="header" class="header">
|
||||
<div ref="buttons" class="row justify-between no-wrap">
|
||||
<div>
|
||||
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
|
||||
<q-icon name="la la-arrow-left" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
|
||||
<div class="row no-wrap">
|
||||
<button ref="loader" v-ripple class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')">
|
||||
<q-icon name="la la-arrow-left" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['loader'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['loadFile']" ref="loadFile" v-ripple class="tool-button" :class="buttonActiveClass('loadFile')" @click="buttonClick('loadFile')">
|
||||
<q-icon name="la la-caret-square-up" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['loadFile'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['loadBuffer']" ref="loadBuffer" v-ripple class="tool-button" :class="buttonActiveClass('loadBuffer')" @click="buttonClick('loadBuffer')">
|
||||
<q-icon name="la la-comment" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['loadBuffer'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button v-show="showToolButton['help']" ref="help" v-ripple class="tool-button" :class="buttonActiveClass('help')" @click="buttonClick('help')">
|
||||
<q-icon name="la la-question" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['help'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
|
||||
<q-icon name="la la-angle-left" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['undoAction'] }}</q-tooltip>
|
||||
<div class="row no-wrap">
|
||||
<div class="space"></div>
|
||||
<button v-show="showToolButton['undoAction']" ref="undoAction" v-ripple class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')">
|
||||
<q-icon name="la la-angle-left" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['undoAction'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
|
||||
<q-icon name="la la-angle-right" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['redoAction']" ref="redoAction" v-ripple class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')">
|
||||
<q-icon name="la la-angle-right" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['redoAction'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['fullScreen'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['fullScreen']" ref="fullScreen" v-ripple class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')">
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['fullScreen'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
|
||||
<q-icon name="la la-film" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['scrolling'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['scrolling']" ref="scrolling" v-ripple class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')">
|
||||
<q-icon name="la la-film" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['scrolling'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
|
||||
<q-icon name="la la-angle-double-right" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['setPosition'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['setPosition']" ref="setPosition" v-ripple class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')">
|
||||
<q-icon name="la la-angle-double-right" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['setPosition'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
|
||||
<q-icon name="la la-search" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['search'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['search']" ref="search" v-ripple class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')">
|
||||
<q-icon name="la la-search" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['search'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
|
||||
<q-icon name="la la-copy" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['copyText']" ref="copyText" v-ripple class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')">
|
||||
<q-icon name="la la-copy" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['copyText'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
|
||||
<q-icon name="la la-magic" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['convOptions']" ref="convOptions" v-ripple class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')">
|
||||
<q-icon name="la la-magic" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['convOptions'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
||||
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['refresh']" ref="refresh" v-ripple class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
|
||||
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['refresh'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<button ref="contents" v-show="showToolButton['contents']" class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')" v-ripple>
|
||||
<q-icon name="la la-list" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['contents'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['contents']" ref="contents" v-ripple class="tool-button" :class="buttonActiveClass('contents')" @click="buttonClick('contents')">
|
||||
<q-icon name="la la-list" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['contents'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="libs" v-show="mode == 'liberama.top' && showToolButton['libs']" class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')" v-ripple>
|
||||
<q-icon name="la la-sitemap" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['libs'] }}</q-tooltip>
|
||||
<button v-show="mode == 'liberama.top' && showToolButton['libs']" ref="libs" v-ripple class="tool-button" :class="buttonActiveClass('libs')" @click="buttonClick('libs')">
|
||||
<q-icon name="la la-sitemap" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['libs'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
|
||||
<q-icon name="la la-book-open" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['recentBooks']" ref="recentBooks" v-ripple class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')">
|
||||
<q-icon name="la la-book-open" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['recentBooks'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
|
||||
<q-icon name="la la-unlink" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
|
||||
<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 ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
|
||||
<q-icon name="la la-cog" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
|
||||
<button v-show="showToolButton['offlineMode']" ref="offlineMode" v-ripple class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')">
|
||||
<q-icon name="la la-unlink" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['offlineMode'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
<button ref="settings" v-ripple class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')">
|
||||
<q-icon name="la la-cog" size="32px" />
|
||||
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">
|
||||
{{ rstore.readerActions['settings'] }}
|
||||
</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,39 +133,45 @@
|
||||
|
||||
<div class="main col row relative-position">
|
||||
<keep-alive>
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
<component
|
||||
:is="activePage"
|
||||
ref="page"
|
||||
class="col"
|
||||
@load-book="loadBook"
|
||||
@load-file="loadFile"
|
||||
@book-pos-changed="bookPosChanged"
|
||||
@do-action="doAction"
|
||||
@hide-tool-bar="hideToolBar"
|
||||
></component>
|
||||
</keep-alive>
|
||||
|
||||
<SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
|
||||
<SearchPage v-show="searchActive" ref="searchPage"
|
||||
<SearchPage
|
||||
v-show="searchActive"
|
||||
ref="searchPage"
|
||||
@do-action="doAction"
|
||||
@book-pos-changed="bookPosChanged"
|
||||
@start-text-search="startTextSearch"
|
||||
@stop-text-search="stopTextSearch">
|
||||
</SearchPage>
|
||||
@stop-text-search="stopTextSearch"
|
||||
></SearchPage>
|
||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
||||
<LibsPage v-show="hidden" ref="libsPage" @load-book="loadBook" @libs-close="libsClose" @do-action="doAction"></LibsPage>
|
||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
||||
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||
|
||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle" @load-buffer-toggle="loadBufferToggle"></ReaderDialogs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
import {Buffer} from 'safe-buffer';
|
||||
|
||||
@@ -125,13 +187,14 @@ import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
|
||||
import SettingsPage from './SettingsPage/SettingsPage.vue';
|
||||
import HelpPage from './HelpPage/HelpPage.vue';
|
||||
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
||||
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
||||
import ContentsPage from './ContentsPage/ContentsPage.vue';
|
||||
|
||||
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
||||
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
||||
|
||||
import bookManager from './share/bookManager';
|
||||
import wallpaperStorage from './share/wallpaperStorage';
|
||||
import coversStorage from './share/coversStorage';
|
||||
import dynamicCss from '../../share/dynamicCss';
|
||||
|
||||
import rstore from '../../store/modules/reader';
|
||||
@@ -140,8 +203,9 @@ import miscApi from '../../api/misc';
|
||||
|
||||
import {versionHistory} from './versionHistory';
|
||||
import * as utils from '../../share/utils';
|
||||
import LockQueue from '../../share/LockQueue';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
LoaderPage,
|
||||
TextPage,
|
||||
@@ -155,9 +219,9 @@ export default @Component({
|
||||
SettingsPage,
|
||||
HelpPage,
|
||||
ClickMapPage,
|
||||
ServerStorage,
|
||||
ContentsPage,
|
||||
|
||||
ServerStorage,
|
||||
ReaderDialogs,
|
||||
},
|
||||
watch: {
|
||||
@@ -202,24 +266,33 @@ export default @Component({
|
||||
this.stopScrolling();
|
||||
},
|
||||
},
|
||||
})
|
||||
class Reader extends Vue {
|
||||
rstore = {};
|
||||
loaderActive = false;
|
||||
offlineModeActive = false;
|
||||
progressActive = false;
|
||||
fullScreenActive = false;
|
||||
};
|
||||
|
||||
scrollingActive = false;
|
||||
class Reader {
|
||||
_options = componentOptions;
|
||||
|
||||
rstore = {};
|
||||
|
||||
loaderActive = false;
|
||||
loadFileActive = false;
|
||||
loadBufferActive = false;
|
||||
fullScreenActive = false;
|
||||
setPositionActive = false;
|
||||
searchActive = false;
|
||||
copyTextActive = false;
|
||||
convOptionsActive = false;
|
||||
refreshActive = false;
|
||||
contentsActive = false;
|
||||
libsActive = false;
|
||||
recentBooksActive = false;
|
||||
clickControlActive = false;
|
||||
offlineModeActive = false;
|
||||
settingsActive = false;
|
||||
helpActive = false;
|
||||
|
||||
clickMapActive = false;
|
||||
contentsActive = false;
|
||||
helpActive = false;
|
||||
scrollingActive = false;
|
||||
progressActive = false;
|
||||
|
||||
bookPos = null;
|
||||
allowUrlParamBookPos = false;
|
||||
@@ -243,10 +316,21 @@ class Reader extends Vue {
|
||||
this.reader = this.$store.state.reader;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
this.lock = new LockQueue(100);
|
||||
|
||||
this.$root.addEventHook('key', this.keyHook);
|
||||
|
||||
this.lastActivePage = false;
|
||||
|
||||
this.$watch(
|
||||
() => this.$route.path,
|
||||
(newValue) => {
|
||||
if (newValue == '/reader') {
|
||||
this.updateRoute();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.debouncedSetRecentBook = _.debounce(async(newValue) => {
|
||||
const recent = this.mostRecentBook();
|
||||
if (recent && (recent.bookPos != newValue || recent.bookPosSeen !== this.bookPosSeen)) {
|
||||
@@ -260,12 +344,19 @@ class Reader extends Vue {
|
||||
await this.$nextTick();
|
||||
this.paramPosIgnore = false;
|
||||
}
|
||||
}, 500, {maxWait: 5000});
|
||||
}, 250, {maxWait: 5000});
|
||||
|
||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
}, 15000, {maxWait: 20000});
|
||||
|
||||
this.debouncedHideToolBar = _.debounce((event) => {
|
||||
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
|
||||
this.commit('reader/setToolBarActive', !!event.show);
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
}, 200);
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||
});
|
||||
@@ -274,14 +365,14 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.updateHeaderMinWidth();
|
||||
|
||||
(async() => {
|
||||
await wallpaperStorage.init();
|
||||
await coversStorage.init();
|
||||
|
||||
await bookManager.init(this.settings);
|
||||
bookManager.addEventListener(this.bookManagerEvent);
|
||||
|
||||
if (this.$root.rootRoute() == '/reader') {
|
||||
if (this.$root.getRootRoute() == '/reader') {
|
||||
if (this.routeParamUrl) {
|
||||
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
|
||||
} else {
|
||||
@@ -322,8 +413,10 @@ class Reader extends Vue {
|
||||
this.copyFullText = settings.copyFullText;
|
||||
this.showClickMapPage = settings.showClickMapPage;
|
||||
this.clickControl = settings.clickControl;
|
||||
this.clickControlActive = this.clickControl;
|
||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||
this.splitToPara = settings.splitToPara;
|
||||
@@ -338,29 +431,69 @@ class Reader extends Vue {
|
||||
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
||||
}
|
||||
|
||||
this.updateHeaderMinWidth();
|
||||
|
||||
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
|
||||
async loadWallpapers() {
|
||||
const wallpaperDataLength = await wallpaperStorage.getLength();
|
||||
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
|
||||
this.wallpaperDataLength = wallpaperDataLength;
|
||||
if (!_.isEqual(this.userWallpapers, this.prevUserWallpapers)) {//оптимизация
|
||||
this.prevUserWallpapers = _.cloneDeep(this.userWallpapers);
|
||||
|
||||
let newCss = '';
|
||||
let updated = false;
|
||||
const wallpaperExists = new Set();
|
||||
for (const wp of this.userWallpapers) {
|
||||
const data = await wallpaperStorage.getData(wp.cssClass);
|
||||
wallpaperExists.add(wp.cssClass);
|
||||
|
||||
let data = await wallpaperStorage.getData(wp.cssClass);
|
||||
if (!data) {
|
||||
//здесь будем восстанавливать данные с сервера
|
||||
const url = `disk://${wp.cssClass.replace('user-paper', '')}`;
|
||||
try {
|
||||
data = await readerApi.getUploadedFileBuf(url);
|
||||
await wallpaperStorage.setData(wp.cssClass, data);
|
||||
updated = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
||||
}
|
||||
}
|
||||
|
||||
//почистим wallpaperStorage
|
||||
for (const key of await wallpaperStorage.getKeys()) {
|
||||
if (!wallpaperExists.has(key)) {
|
||||
await wallpaperStorage.removeData(key);
|
||||
}
|
||||
}
|
||||
|
||||
//обновим settings, если загружали обои из /upload/
|
||||
if (updated) {
|
||||
const newSettings = _.cloneDeep(this.settings);
|
||||
newSettings.needUpdateSettingsView = (newSettings.needUpdateSettingsView < 10 ? newSettings.needUpdateSettingsView + 1 : 0);
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
|
||||
dynamicCss.replace('wallpapers', newCss);
|
||||
}
|
||||
}
|
||||
@@ -389,17 +522,6 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
const q = this.$route.query;
|
||||
|
||||
@@ -475,9 +597,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
get routeParamUrl() {
|
||||
@@ -535,7 +655,20 @@ class Reader extends Vue {
|
||||
//сохранение в serverStorage
|
||||
if (value) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -566,7 +699,11 @@ class Reader extends Vue {
|
||||
|
||||
toolBarToggle() {
|
||||
this.commit('reader/setToolBarActive', !this.toolBarActive);
|
||||
this.$root.$emit('resize');
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
|
||||
hideToolBar(event) {
|
||||
this.debouncedHideToolBar(event);
|
||||
}
|
||||
|
||||
fullScreenToggle() {
|
||||
@@ -596,6 +733,28 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
this.setPositionActive = !this.setPositionActive;
|
||||
const page = this.$refs.page;
|
||||
@@ -723,6 +882,12 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
clickControlToggle() {
|
||||
const newSettings = _.cloneDeep(this.settings);
|
||||
newSettings.clickControl = !this.clickControl;
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
}
|
||||
|
||||
offlineModeToggle() {
|
||||
this.offlineModeActive = !this.offlineModeActive;
|
||||
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
||||
@@ -776,7 +941,7 @@ class Reader extends Vue {
|
||||
|
||||
refreshBook() {
|
||||
const mrb = this.mostRecentBook();
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||
this.loadBook(Object.assign({}, mrb, {force: true}));
|
||||
}
|
||||
|
||||
undoAction() {
|
||||
@@ -811,6 +976,9 @@ class Reader extends Vue {
|
||||
|
||||
switch (action) {
|
||||
case 'loader':
|
||||
case 'loadFile':
|
||||
case 'loadBuffer':
|
||||
case 'help':
|
||||
case 'fullScreen':
|
||||
case 'setPosition':
|
||||
case 'search':
|
||||
@@ -820,6 +988,7 @@ class Reader extends Vue {
|
||||
case 'contents':
|
||||
case 'libs':
|
||||
case 'recentBooks':
|
||||
case 'clickControl':
|
||||
case 'offlineMode':
|
||||
case 'settings':
|
||||
if (this.progressActive) {
|
||||
@@ -857,7 +1026,6 @@ class Reader extends Vue {
|
||||
classResult = classDisabled;
|
||||
break;
|
||||
case 'refresh':
|
||||
case 'recentBooks':
|
||||
if (!this.mostRecentBookReactive)
|
||||
classResult = classDisabled;
|
||||
break;
|
||||
@@ -876,7 +1044,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
let result = '';
|
||||
let result = undefined;
|
||||
|
||||
if (this.progressActive)
|
||||
result = 'ProgressPage';
|
||||
@@ -891,7 +1059,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
if (result != 'TextPage') {
|
||||
this.$root.$emit('set-app-title');
|
||||
this.$root.setAppTitle();
|
||||
}
|
||||
|
||||
// на LoaderPage всегда показываем toolBar
|
||||
@@ -906,7 +1074,7 @@ class Reader extends Vue {
|
||||
const isParsed = await bookManager.hasBookParsed(last);
|
||||
|
||||
if (!isParsed) {
|
||||
this.$root.$emit('set-app-title');
|
||||
this.$root.setAppTitle();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -926,7 +1094,7 @@ class Reader extends Vue {
|
||||
return result;
|
||||
}
|
||||
|
||||
async loadBook(opts) {
|
||||
async _loadBook(opts) {
|
||||
if (!opts || !opts.url) {
|
||||
this.mostRecentBook();
|
||||
return;
|
||||
@@ -936,10 +1104,6 @@ class Reader extends Vue {
|
||||
|
||||
let url = encodeURI(decodeURI(opts.url));
|
||||
|
||||
//TODO: убрать конвертирование 'file://' после 06.2021
|
||||
if (url.length == 71 && url.indexOf('file://') == 0)
|
||||
url = url.replace(/^file/, 'disk');
|
||||
|
||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||
(url.indexOf('disk://') != 0))
|
||||
url = 'http://' + url;
|
||||
@@ -966,33 +1130,37 @@ class Reader extends Vue {
|
||||
progress.show();
|
||||
progress.setState({state: 'parse'});
|
||||
|
||||
// есть ли среди недавних
|
||||
const key = bookManager.keyFromUrl(url);
|
||||
let wasOpened = await bookManager.getRecentBook({key});
|
||||
wasOpened = (wasOpened ? wasOpened : {});
|
||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
||||
// есть ли среди загруженных
|
||||
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
|
||||
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
|
||||
|
||||
wasOpened = Object.assign(wasOpened, {
|
||||
url: (opts.url !== undefined ? opts.url : wasOpened.url),
|
||||
path: (opts.path !== undefined ? opts.path : wasOpened.path),
|
||||
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
|
||||
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
|
||||
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
|
||||
});
|
||||
|
||||
let book = null;
|
||||
|
||||
if (!opts.force) {
|
||||
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
||||
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
|
||||
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
|
||||
progress.setState({progress: prog});
|
||||
});
|
||||
|
||||
// если есть в локальном кэше
|
||||
if (bookParsed) {
|
||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
|
||||
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
|
||||
this.mostRecentBook();
|
||||
this.addAction(bookPos);
|
||||
this.addAction(wasOpened.bookPos);
|
||||
this.loaderActive = false;
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.blinkCachedLoadMessage();
|
||||
|
||||
this.checkBookPosPercent();
|
||||
await this.activateClickMapPage();
|
||||
this.activateClickMapPage();//no await
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1006,7 +1174,7 @@ class Reader extends Vue {
|
||||
});
|
||||
book = Object.assign({}, wasOpened, {data: resp.data});
|
||||
} catch (e) {
|
||||
//молчим
|
||||
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1017,7 +1185,7 @@ class Reader extends Vue {
|
||||
if (!book) {
|
||||
book = await readerApi.loadBook({
|
||||
url,
|
||||
uploadFileName,
|
||||
uploadFileName: wasOpened.uploadFileName,
|
||||
enableSitesFilter: this.enableSitesFilter,
|
||||
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||
isText: (this.splitToPara ? true : false),
|
||||
@@ -1034,14 +1202,44 @@ class Reader extends Vue {
|
||||
|
||||
// добавляем в bookManager
|
||||
progress.setState({state: 'parse', step: 5});
|
||||
|
||||
const addedBook = await bookManager.addBook(book, (prog) => {
|
||||
progress.setState({progress: prog});
|
||||
});
|
||||
|
||||
// sameBookKey
|
||||
if (url.indexOf('disk://') == 0) {
|
||||
//ищем такой файл в загруженных
|
||||
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
|
||||
found = (found ? _.cloneDeep(found) : found);
|
||||
|
||||
if (found) {
|
||||
if (wasOpened.sameBookKey != found.sameBookKey) {
|
||||
//спрашиваем, надо ли объединить файлы
|
||||
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
|
||||
await this.$root.stdDialog.askYesNo(`
|
||||
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
|
||||
<br>Объединить позицию?`, 'Найдена похожая книга');
|
||||
if (askResult) {
|
||||
wasOpened.bookPos = found.bookPos;
|
||||
wasOpened.bookPosSeen = found.bookPosSeen;
|
||||
wasOpened.sameBookKey = found.sameBookKey;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wasOpened.sameBookKey = wasOpened.uploadFileName;
|
||||
}
|
||||
} else {
|
||||
wasOpened.sameBookKey = addedBook.url;
|
||||
}
|
||||
|
||||
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
|
||||
delete wasOpened.loadTime;
|
||||
|
||||
// добавляем в историю
|
||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
|
||||
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
|
||||
this.mostRecentBook();
|
||||
this.addAction(bookPos);
|
||||
this.addAction(wasOpened.bookPos);
|
||||
this.updateRoute(true);
|
||||
|
||||
this.loaderActive = false;
|
||||
@@ -1052,17 +1250,28 @@ class Reader extends Vue {
|
||||
this.stopBlink = true;
|
||||
|
||||
this.checkBookPosPercent();
|
||||
await this.activateClickMapPage();
|
||||
this.activateClickMapPage();//no await
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
if (!this.showHelpOnErrorIfNeeded(url)) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
} finally {
|
||||
this.checkNewVersionAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
async loadFile(opts) {
|
||||
async loadBook(opts) {
|
||||
await this.lock.get();
|
||||
try {
|
||||
await this._loadBook(opts);
|
||||
} finally {
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadFile(opts) {
|
||||
this.progressActive = true;
|
||||
|
||||
await this.$nextTick();
|
||||
@@ -1078,7 +1287,7 @@ class Reader extends Vue {
|
||||
|
||||
progress.hide(); this.progressActive = false;
|
||||
|
||||
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||
await this._loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
@@ -1086,6 +1295,15 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async loadFile(opts) {
|
||||
await this.lock.get();
|
||||
try {
|
||||
await this._loadFile(opts);
|
||||
} finally {
|
||||
this.lock.ret();
|
||||
}
|
||||
}
|
||||
|
||||
blinkCachedLoadMessage() {
|
||||
if (!this.blinkCachedLoad)
|
||||
return;
|
||||
@@ -1122,6 +1340,12 @@ class Reader extends Vue {
|
||||
case 'loader':
|
||||
this.loaderToggle();
|
||||
break;
|
||||
case 'loadFile':
|
||||
this.loadFileToggle();
|
||||
break;
|
||||
case 'loadBuffer':
|
||||
this.loadBufferToggle();
|
||||
break;
|
||||
case 'help':
|
||||
this.helpToggle();
|
||||
break;
|
||||
@@ -1164,6 +1388,9 @@ class Reader extends Vue {
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'clickControl':
|
||||
this.clickControlToggle();
|
||||
break;
|
||||
case 'offlineMode':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
@@ -1232,7 +1459,7 @@ class Reader extends Vue {
|
||||
|
||||
keyHook(event) {
|
||||
let result = false;
|
||||
if (this.$root.rootRoute() == '/reader') {
|
||||
if (this.$root.getRootRoute() == '/reader') {
|
||||
if (this.$root.stdDialog.active)
|
||||
return result;
|
||||
|
||||
@@ -1266,29 +1493,53 @@ class Reader extends Vue {
|
||||
if (!result && event.type == 'keydown') {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
|
||||
if (action == 'loader') {
|
||||
/*if (action == 'loader') {
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
|
||||
if (!result && this.activePage == 'TextPage') {
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
}*/
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Reader);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
background-color: #1B695F;
|
||||
color: #000;
|
||||
overflow: hidden;
|
||||
height: 50px;
|
||||
overflow-x: auto;
|
||||
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 {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Что нового:
|
||||
</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 slot="footer">
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
<span class="clickable" style="font-size: 13px" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||
|
||||
<template #footer>
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">
|
||||
Больше не показывать
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog2" v-model="donationVisible">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Здравствуйте, уважаемые читатели!
|
||||
</template>
|
||||
|
||||
@@ -35,7 +40,9 @@
|
||||
Автор также обращается с просьбой о помощи в распространении
|
||||
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
||||
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
||||
@@ -47,29 +54,53 @@
|
||||
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
<!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
|
||||
Помочь проекту
|
||||
</q-btn-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<template #footer>
|
||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||
<br>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||
</span>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">
|
||||
Напомнить позже
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Dialog from '../../share/Dialog.vue';
|
||||
import * as utils from '../../../share/utils';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
@@ -78,11 +109,14 @@ export default @Component({
|
||||
this.loadSettings();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ReaderDialogs extends Vue {
|
||||
};
|
||||
class ReaderDialogs {
|
||||
_options = componentOptions;
|
||||
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
urlHelpVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -107,9 +141,9 @@ class ReaderDialogs extends Vue {
|
||||
const whatsNew = versionHistory[0];
|
||||
if (this.showWhatsNewDialog &&
|
||||
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||
whatsNew.header != this.whatsNewContentHash) {
|
||||
this.whatsNewHeader != this.whatsNewContentHash) {
|
||||
await utils.sleep(2000);
|
||||
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
||||
this.whatsNewContent = 'Версия ' + this.whatsNewHeader + whatsNew.content;
|
||||
this.whatsNewVisible = true;
|
||||
}
|
||||
}
|
||||
@@ -123,6 +157,15 @@ class ReaderDialogs extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async showUrlHelp() {
|
||||
this.urlHelpVisible = true;
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.$emit('load-buffer-toggle');
|
||||
this.urlHelpVisible = false;
|
||||
}
|
||||
|
||||
donationDialogDisable() {
|
||||
this.donationVisible = false;
|
||||
if (this.showDonationDialog2020) {
|
||||
@@ -155,8 +198,11 @@ class ReaderDialogs extends Vue {
|
||||
|
||||
whatsNewDisable() {
|
||||
this.whatsNewVisible = false;
|
||||
const whatsNew = versionHistory[0];
|
||||
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
||||
this.commit('reader/setWhatsNewContentHash', this.whatsNewHeader);
|
||||
}
|
||||
|
||||
get whatsNewHeader() {
|
||||
return `${versionHistory[0].version} (${versionHistory[0].releaseDate})`;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
@@ -176,11 +222,13 @@ class ReaderDialogs extends Vue {
|
||||
}
|
||||
|
||||
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 false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ReaderDialogs);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
@@ -8,18 +8,23 @@
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
|
||||
<div v-show="!initStep" class="input">
|
||||
<!--input ref="input"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/-->
|
||||
<q-input ref="input" class="col" outlined dense
|
||||
placeholder="что ищем"
|
||||
v-model="needle" @keydown="inputKeyDown"
|
||||
<q-input
|
||||
ref="input" v-model="needle"
|
||||
class="col" outlined dense
|
||||
placeholder="Найти"
|
||||
@keydown="inputKeyDown"
|
||||
/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
|
||||
{{ foundText }}
|
||||
</div>
|
||||
</div>
|
||||
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
|
||||
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
|
||||
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
|
||||
<q-btn class="button" dense stretch @click="showNext">
|
||||
<q-icon style="top: -6px" name="la la-angle-down" dense size="22px" />
|
||||
</q-btn>
|
||||
<q-btn class="button" dense stretch @click="showPrev">
|
||||
<q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px" />
|
||||
</q-btn>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
</Window>
|
||||
@@ -27,13 +32,12 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
@@ -49,8 +53,10 @@ export default @Component({
|
||||
el.style.paddingRight = newValue.length*12 + 'px';
|
||||
},
|
||||
},
|
||||
})
|
||||
class SearchPage extends Vue {
|
||||
};
|
||||
class SearchPage {
|
||||
_options = componentOptions;
|
||||
|
||||
header = null;
|
||||
initStep = null;
|
||||
initPercentage = 0;
|
||||
@@ -100,7 +106,7 @@ class SearchPage extends Vue {
|
||||
this.parsed = parsed;
|
||||
}
|
||||
|
||||
this.header = 'Найти';
|
||||
this.header = 'Поиск в тексте';
|
||||
await this.$nextTick();
|
||||
this.$refs.input.focus();
|
||||
this.$refs.input.select();
|
||||
@@ -180,6 +186,8 @@ class SearchPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SearchPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import bookManager from '../share/bookManager';
|
||||
@@ -18,7 +18,7 @@ const ssCacheStore = localForage.createInstance({
|
||||
name: 'ssCacheStore'
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
serverSyncEnabled: function() {
|
||||
this.serverSyncEnabledChanged();
|
||||
@@ -39,14 +39,16 @@ export default @Component({
|
||||
this.debouncedSaveLibs();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ServerStorage extends Vue {
|
||||
};
|
||||
class ServerStorage {
|
||||
_options = componentOptions;
|
||||
|
||||
created() {
|
||||
this.inited = false;
|
||||
this.keyInited = false;
|
||||
this.commit = this.$store.commit;
|
||||
this.prevServerStorageKey = null;
|
||||
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
|
||||
this.$root.generateNewServerStorageKey = () => {this.generateNewServerStorageKey()};
|
||||
|
||||
this.debouncedSaveSettings = _.debounce(() => {
|
||||
this.saveSettings();
|
||||
@@ -574,7 +576,7 @@ class ServerStorage extends Vue {
|
||||
newRecentPatch.rev++;
|
||||
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])
|
||||
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod, {isAddChanged: true});
|
||||
|
||||
@@ -625,7 +627,7 @@ class ServerStorage extends Vue {
|
||||
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||
if (!recurse && itemKey) {
|
||||
this.savingRecent = false;
|
||||
this.saveRecent(itemKey, true);
|
||||
await this.saveRecent(itemKey, true);
|
||||
return;
|
||||
}
|
||||
} else if (result.state == 'success') {
|
||||
@@ -726,13 +728,15 @@ class ServerStorage extends Vue {
|
||||
const ids = id.split('.');
|
||||
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||
items[utils.fromBase58(ids[1])] = decoded;
|
||||
items[utils.fromBase58(ids[1]).toString()] = decoded;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.items = items;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ServerStorage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
<template>
|
||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template #header>
|
||||
Установить позицию
|
||||
</template>
|
||||
|
||||
<div id="set-position-slider" class="slider q-px-md">
|
||||
<q-slider
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
v-model="sliderValue"
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
<div class="col column justify-center">
|
||||
<div id="set-position-slider" class="slider q-px-md column justify-center">
|
||||
<q-slider
|
||||
v-model="sliderValue"
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
@@ -34,8 +36,10 @@ export default @Component({
|
||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||
},
|
||||
},
|
||||
})
|
||||
class SetPositionPage extends Vue {
|
||||
};
|
||||
class SetPositionPage {
|
||||
_options = componentOptions;
|
||||
|
||||
sliderValue = null;
|
||||
sliderMax = null;
|
||||
|
||||
@@ -67,12 +71,15 @@ class SetPositionPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SetPositionPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slider {
|
||||
margin: 20px;
|
||||
margin: 0 20px 0 20px;
|
||||
height: 35px;
|
||||
background-color: #efefef;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<!--div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||
Показывать "Оплатим хостинг вместе"
|
||||
@@ -60,7 +60,7 @@
|
||||
Показывать уведомление "Оплатим хостинг вместе"
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
@@ -1,15 +1,18 @@
|
||||
<template lang="includer">
|
||||
<Window ref="window" height="95%" width="600px" @close="close">
|
||||
<template slot="header">
|
||||
<template>
|
||||
<Window ref="window" width="600px" @close="close">
|
||||
<template #header>
|
||||
Настройки
|
||||
</template>
|
||||
|
||||
<div class="col row">
|
||||
<a ref="download" style="display: none;" target="_blank"></a>
|
||||
|
||||
<div class="full-height">
|
||||
<q-tabs
|
||||
ref="tabs"
|
||||
class="bg-grey-3 text-black"
|
||||
v-model="selectedTab"
|
||||
class="bg-grey-3 text-black"
|
||||
|
||||
left-icon="la la-caret-up"
|
||||
right-icon="la la-caret-down"
|
||||
active-color="white"
|
||||
@@ -20,62 +23,100 @@
|
||||
stretch
|
||||
inline-label
|
||||
>
|
||||
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
||||
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
|
||||
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
|
||||
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
||||
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" label="Панель" />
|
||||
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
||||
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
||||
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
||||
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
||||
<div v-show="tabsScrollable" class="q-pt-lg" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<div class="col fit">
|
||||
<!-- Профили --------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'profiles'" class="fit tab-panel">
|
||||
@@include('./include/ProfilesTab.inc');
|
||||
@@include('./ProfilesTab.inc');
|
||||
</div>
|
||||
<!-- Вид ------------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'view'" class="fit column">
|
||||
@@include('./include/ViewTab.inc');
|
||||
<q-tabs
|
||||
v-model="selectedViewTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="mode" label="Режим" />
|
||||
<q-tab name="color" label="Цвет" />
|
||||
<q-tab name="font" label="Шрифт" />
|
||||
<q-tab name="text" label="Текст" />
|
||||
<q-tab name="status" label="Строка статуса" />
|
||||
</q-tabs>
|
||||
|
||||
<div class="q-mb-sm" />
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedViewTab == 'mode'">
|
||||
@@include('./ViewTab/Mode.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'color'">
|
||||
@@include('./ViewTab/Color.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'font'">
|
||||
@@include('./ViewTab/Font.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'text'">
|
||||
@@include('./ViewTab/Text.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'status'">
|
||||
@@include('./ViewTab/Status.inc');
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Кнопки ---------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
|
||||
@@include('./include/ButtonsTab.inc');
|
||||
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
|
||||
@@include('./ToolBarTab.inc');
|
||||
</div>
|
||||
<!-- Управление ------------------------------------------------------------------>
|
||||
<div v-if="selectedTab == 'keys'" class="fit column">
|
||||
@@include('./include/KeysTab.inc');
|
||||
@@include('./KeysTab.inc');
|
||||
</div>
|
||||
<!-- Листание -------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
||||
@@include('./include/PageMoveTab.inc');
|
||||
@@include('./PageMoveTab.inc');
|
||||
</div>
|
||||
<!-- Конвертирование ------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
||||
@@include('./include/ConvertTab.inc');
|
||||
@@include('./ConvertTab.inc');
|
||||
</div>
|
||||
<!-- Прочее ---------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
||||
@@include('./include/OthersTab.inc');
|
||||
@@include('./OthersTab.inc');
|
||||
</div>
|
||||
<!-- Сброс ----------------------------------------------------------------------->
|
||||
<div v-if="selectedTab == 'reset'" class="fit tab-panel">
|
||||
@@include('./include/ResetTab.inc');
|
||||
@@include('./ResetTab.inc');
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import { ref, watch } from 'vue';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
@@ -85,12 +126,13 @@ import NumInput from '../../share/NumInput.vue';
|
||||
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
||||
import wallpaperStorage from '../share/wallpaperStorage';
|
||||
|
||||
import readerApi from '../../../api/reader';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
import defPalette from './defPalette';
|
||||
|
||||
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
NumInput,
|
||||
@@ -104,8 +146,9 @@ export default @Component({
|
||||
this.settingsChanged();
|
||||
},
|
||||
form: function(newValue) {
|
||||
if (this.inited)
|
||||
this.commit('reader/setSettings', newValue);
|
||||
if (this.inited) {
|
||||
this.commit('reader/setSettings', _.cloneDeep(newValue));
|
||||
}
|
||||
},
|
||||
fontBold: function(newValue) {
|
||||
this.fontWeight = (newValue ? 'bold' : '');
|
||||
@@ -165,12 +208,13 @@ export default @Component({
|
||||
this.statusBarColor = newValue;
|
||||
},
|
||||
},
|
||||
})
|
||||
class SettingsPage extends Vue {
|
||||
};
|
||||
class SettingsPage {
|
||||
_options = componentOptions;
|
||||
|
||||
selectedTab = 'profiles';
|
||||
selectedViewTab = 'mode';
|
||||
selectedKeysTab = 'mouse';
|
||||
form = {};
|
||||
fontBold = false;
|
||||
fontItalic = false;
|
||||
vertShift = 0;
|
||||
@@ -186,6 +230,19 @@ class SettingsPage extends Vue {
|
||||
toolButtons = [];
|
||||
rstore = {};
|
||||
|
||||
setup() {
|
||||
const settingsProps = { form: ref({}) };
|
||||
|
||||
for (let prop in rstore.settingDefaults) {
|
||||
settingsProps[prop] = ref(_.cloneDeep(rstore.settingDefaults[prop]));
|
||||
watch(settingsProps[prop], (newValue) => {
|
||||
settingsProps.form.value = Object.assign({}, settingsProps.form.value, {[prop]: newValue});
|
||||
}, {deep: true});
|
||||
}
|
||||
|
||||
return settingsProps;
|
||||
}
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.reader = this.$store.state.reader;
|
||||
@@ -200,7 +257,7 @@ class SettingsPage extends Vue {
|
||||
this.$watch(
|
||||
'$refs.tabs.scrollable',
|
||||
(newValue) => {
|
||||
this.tabsScrollable = newValue && !this.$isMobileDevice;
|
||||
this.tabsScrollable = newValue && !this.$root.isMobileDevice;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -215,18 +272,8 @@ class SettingsPage extends Vue {
|
||||
return;
|
||||
|
||||
this.form = Object.assign({}, this.settings);
|
||||
if (!this.unwatch)
|
||||
this.unwatch = {};
|
||||
|
||||
for (let prop in rstore.settingDefaults) {
|
||||
if (this.unwatch && this.unwatch[prop])
|
||||
this.unwatch[prop]();
|
||||
|
||||
this[prop] = this.form[prop];
|
||||
|
||||
this.unwatch[prop] = this.$watch(prop, (newValue) => {
|
||||
this.form = Object.assign({}, this.form, {[prop]: newValue});
|
||||
});
|
||||
for (const prop in rstore.settingDefaults) {
|
||||
this[prop] = _.cloneDeep(this.form[prop]);
|
||||
}
|
||||
|
||||
this.fontBold = (this.fontWeight == 'bold');
|
||||
@@ -421,10 +468,6 @@ class SettingsPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
changeShowToolButton(buttonName) {
|
||||
this.showToolButton = Object.assign({}, this.showToolButton, {[buttonName]: !this.showToolButton[buttonName]});
|
||||
}
|
||||
|
||||
async addProfile() {
|
||||
try {
|
||||
if (Object.keys(this.profiles).length >= 100) {
|
||||
@@ -457,7 +500,7 @@ class SettingsPage extends Vue {
|
||||
return;
|
||||
|
||||
try {
|
||||
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$sanitize(this.currentProfile)}' необратимо.` +
|
||||
const result = await this.$root.stdDialog.prompt(`<b>Предупреждение!</b> Удаление профиля '${this.$root.sanitize(this.currentProfile)}' необратимо.` +
|
||||
`<br>Все настройки профиля будут потеряны, однако список читаемых книг сохранится.` +
|
||||
`<br><br>Введите 'да' для подтверждения удаления:`, ' ', {
|
||||
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||
@@ -550,7 +593,8 @@ class SettingsPage extends Vue {
|
||||
});
|
||||
|
||||
if (result && result.value && result.value.toLowerCase() == 'да') {
|
||||
this.$root.$emit('generateNewServerStorageKey');
|
||||
if (this.$root.generateNewServerStorageKey)
|
||||
this.$root.generateNewServerStorageKey();
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
@@ -595,8 +639,17 @@ class SettingsPage extends Vue {
|
||||
|
||||
if (index < 0)
|
||||
newUserWallpapers.push({label, cssClass});
|
||||
if (!wallpaperStorage.keyExists(cssClass))
|
||||
if (!wallpaperStorage.keyExists(cssClass)) {
|
||||
await wallpaperStorage.setData(cssClass, data);
|
||||
//отправим data на сервер в файл `/upload/${key}`
|
||||
try {
|
||||
//const res =
|
||||
await readerApi.uploadFileBuf(data);
|
||||
//console.log(res);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.userWallpapers = newUserWallpapers;
|
||||
this.wallpaper = cssClass;
|
||||
@@ -623,6 +676,27 @@ class SettingsPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadWallpaper() {
|
||||
if (this.wallpaper.indexOf('user-paper') != 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
const d = this.$refs.download;
|
||||
|
||||
const dataUrl = await wallpaperStorage.getData(this.wallpaper);
|
||||
|
||||
if (!dataUrl)
|
||||
throw new Error('Файл обоев не найден');
|
||||
|
||||
d.href = dataUrl;
|
||||
d.download = `wallpaper-#${this.wallpaper.replace('user-paper', '').substring(0, 4)}`;
|
||||
|
||||
d.click();
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
@@ -630,6 +704,8 @@ class SettingsPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SettingsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -659,11 +735,11 @@ class SettingsPage extends Vue {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label-1, .label-7 {
|
||||
.label-1, .label-3, .label-7 {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.label-2, .label-3, .label-4, .label-5 {
|
||||
.label-2, .label-4, .label-5 {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
|
||||
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
18
client/components/Reader/SettingsPage/ToolBarTab.inc
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="part-header">Отображение</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-3"></div>
|
||||
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Скрывать/показывть панель при прокрутке текста вперед/назад
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="part-header">Показывать кнопки</div>
|
||||
|
||||
<div class="item row no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||
<div class="label-3"></div>
|
||||
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||
/>
|
||||
</div>
|
||||
@@ -2,14 +2,20 @@
|
||||
<div class="table col column no-wrap">
|
||||
<!-- header -->
|
||||
<div class="table-row row">
|
||||
<div class="desc q-pa-sm bg-blue-2">Команда</div>
|
||||
<div class="desc q-pa-sm bg-blue-2">
|
||||
Команда
|
||||
</div>
|
||||
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
|
||||
<div style="width: 80px">Сочетание клавиш</div>
|
||||
<q-input ref="input" class="q-ml-sm col"
|
||||
outlined dense rounded
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
<div style="width: 80px">
|
||||
Сочетание клавиш
|
||||
</div>
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="search"
|
||||
class="q-ml-sm col"
|
||||
outlined dense
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
@click.stop
|
||||
/>
|
||||
<div v-show="!readonly" class="q-ml-sm column justify-center">
|
||||
@@ -23,35 +29,38 @@
|
||||
</div>
|
||||
|
||||
<!-- body -->
|
||||
<div class="table-row row" v-for="(action, index) in tableData" :key="index">
|
||||
<div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div>
|
||||
<div v-for="(action, index) in tableData" :key="index" class="table-row row">
|
||||
<div class="desc q-pa-sm">
|
||||
{{ rstore.readerActions[action] }}
|
||||
</div>
|
||||
<div class="hotKeys col q-pa-sm">
|
||||
<q-chip
|
||||
v-for="(code, index2) in modelValue[action]" :key="index2"
|
||||
:color="collisions[code] ? 'red' : 'grey-7'"
|
||||
:removable="!readonly" :clickable="collisions[code] ? true : false"
|
||||
text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)"
|
||||
text-color="white" @remove="removeCode(action, code)"
|
||||
@click="collisionWarning(code)"
|
||||
>
|
||||
>
|
||||
{{ code }}
|
||||
</q-chip>
|
||||
</div>
|
||||
<div v-show="!readonly" class="column q-pa-xs">
|
||||
<q-icon
|
||||
v-ripple
|
||||
:disabled="(modelValue[action].length >= maxCodesLength) || null"
|
||||
name="la la-plus-circle"
|
||||
class="button bg-green-8 text-white"
|
||||
@click="addHotKey(action)"
|
||||
v-ripple
|
||||
:disabled="value[action].length >= maxCodesLength"
|
||||
@click="addHotKey(action)"
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавить сочетание клавиш
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
v-ripple
|
||||
name="la la-broom"
|
||||
class="button text-grey-5"
|
||||
@click="defaultHotKey(action)"
|
||||
v-ripple
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По умолчанию
|
||||
@@ -64,31 +73,29 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../../vueComponent.js';
|
||||
|
||||
import rstore from '../../../../store/modules/reader';
|
||||
//import * as utils from '../../share/utils';
|
||||
|
||||
const UserHotKeysProps = Vue.extend({
|
||||
props: {
|
||||
value: Object,
|
||||
readonly: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
search: function() {
|
||||
this.updateTableData();
|
||||
},
|
||||
value: function() {
|
||||
modelValue: function() {
|
||||
this.checkCollisions();
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
})
|
||||
class UserHotKeys extends UserHotKeysProps {
|
||||
};
|
||||
class UserHotKeys {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
modelValue: Object,
|
||||
readonly: Boolean,
|
||||
};
|
||||
|
||||
search = '';
|
||||
rstore = {};
|
||||
tableData = [];
|
||||
@@ -113,7 +120,7 @@ class UserHotKeys extends UserHotKeysProps {
|
||||
|
||||
const search = this.search.toLowerCase();
|
||||
const codesIncludeSearch = (action) => {
|
||||
for (const code of this.value[action]) {
|
||||
for (const code of this.modelValue[action]) {
|
||||
if (code.toLowerCase().includes(search))
|
||||
return true;
|
||||
}
|
||||
@@ -131,7 +138,7 @@ class UserHotKeys extends UserHotKeysProps {
|
||||
|
||||
checkCollisions() {
|
||||
const cols = {};
|
||||
for (const [action, codes] of Object.entries(this.value)) {
|
||||
for (const [action, codes] of Object.entries(this.modelValue)) {
|
||||
codes.forEach(code => {
|
||||
if (!cols[code])
|
||||
cols[code] = [];
|
||||
@@ -158,26 +165,26 @@ class UserHotKeys extends UserHotKeysProps {
|
||||
}
|
||||
|
||||
removeCode(action, code) {
|
||||
let codes = Array.from(this.value[action]);
|
||||
let codes = Array.from(this.modelValue[action]);
|
||||
const index = codes.indexOf(code);
|
||||
if (index >= 0) {
|
||||
codes.splice(index, 1);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
const newValue = Object.assign({}, this.modelValue, {[action]: codes});
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
}
|
||||
|
||||
async addHotKey(action) {
|
||||
if (this.value[action].length >= this.maxCodesLength)
|
||||
if (this.modelValue[action].length >= this.maxCodesLength)
|
||||
return;
|
||||
try {
|
||||
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
|
||||
if (result) {
|
||||
let codes = Array.from(this.value[action]);
|
||||
let codes = Array.from(this.modelValue[action]);
|
||||
if (codes.indexOf(result) < 0) {
|
||||
codes.push(result);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
const newValue = Object.assign({}, this.modelValue, {[action]: codes});
|
||||
this.$emit('update:modelValue', newValue);
|
||||
this.$nextTick(() => {
|
||||
this.collisionWarning(result);
|
||||
});
|
||||
@@ -192,8 +199,8 @@ class UserHotKeys extends UserHotKeysProps {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
|
||||
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
const newValue = Object.assign({}, this.modelValue, {[action]: codes});
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
@@ -204,13 +211,15 @@ class UserHotKeys extends UserHotKeysProps {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
|
||||
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
|
||||
this.$emit('input', newValue);
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(UserHotKeys);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
121
client/components/Reader/SettingsPage/ViewTab/Color.inc
Normal file
121
client/components/Reader/SettingsPage/ViewTab/Color.inc
Normal file
@@ -0,0 +1,121 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">
|
||||
Цвет
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Текст
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-input
|
||||
v-model="textColorFiltered"
|
||||
class="col-left no-mp"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color
|
||||
v-model="textColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" />
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Фон
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-input
|
||||
v-model="bgColorFiltered"
|
||||
class="col-left no-mp"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors" />
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" />
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Обои
|
||||
</div>
|
||||
<div class="col row items-center">
|
||||
<q-select
|
||||
v-model="wallpaper"
|
||||
class="col-left no-mp"
|
||||
:options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div>
|
||||
{{ scope.opt.label }}
|
||||
</div>
|
||||
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item
|
||||
v-bind="scope.itemProps"
|
||||
>
|
||||
<q-item-section style="min-width: 50px;">
|
||||
<q-item-label v-html="scope.opt.label" />
|
||||
</q-item-section>
|
||||
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="q-px-xs" />
|
||||
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Добавить файл обоев
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Удалить выбранные обои
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-file-download" @click.stop="downloadWallpaper">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Скачать выбранные обои
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-sm" />
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row items-center">
|
||||
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="part-header">Показывать кнопки панели</div>
|
||||
|
||||
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
|
||||
<div class="label-3"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" @input="changeShowToolButton(item.name)"
|
||||
:value="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,39 +0,0 @@
|
||||
<q-tabs
|
||||
v-model="selectedViewTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="mode" label="Режим" />
|
||||
<q-tab name="color" label="Цвет" />
|
||||
<q-tab name="font" label="Шрифт" />
|
||||
<q-tab name="text" label="Текст" />
|
||||
<q-tab name="status" label="Строка статуса" />
|
||||
</q-tabs>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedViewTab == 'mode'">
|
||||
@@include('./ViewTab/Mode.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'color'">
|
||||
@@include('./ViewTab/Color.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'font'">
|
||||
@@include('./ViewTab/Font.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'text'">
|
||||
@@include('./ViewTab/Text.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'status'">
|
||||
@@include('./ViewTab/Status.inc');
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,95 +0,0 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Цвет</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Текст</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="textColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="textColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md"/>
|
||||
<div class="item row">
|
||||
<div class="label-2">Фон</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="bgColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md"/>
|
||||
<div class="item row">
|
||||
<div class="label-2">Обои</div>
|
||||
<div class="col row items-center">
|
||||
<q-select class="col-left no-mp" v-model="wallpaper" :options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<template v-slot:selected-item="scope">
|
||||
<div >{{ scope.opt.label }}</div>
|
||||
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
|
||||
</template>
|
||||
|
||||
<template v-slot:option="scope">
|
||||
<q-item
|
||||
v-bind="scope.itemProps"
|
||||
v-on="scope.itemEvents"
|
||||
>
|
||||
<q-item-section style="min-width: 50px;">
|
||||
<q-item-label v-html="scope.opt.label" />
|
||||
</q-item-section>
|
||||
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;"/>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="q-px-xs"/>
|
||||
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить файл обоев</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить выбранные обои</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-sm"/>
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row items-center">
|
||||
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" ref="file" @change="loadWallpaperFile" style='display: none;'/>
|
||||
@@ -6,26 +6,36 @@
|
||||
</div>
|
||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<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 ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<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 v-show="showStatusBar" ref="statusBar" class="layout">
|
||||
<div v-html="statusBar"></div>
|
||||
</div>
|
||||
<div v-show="clickControl" ref="layoutEvents" class="layout events" @mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||
<div
|
||||
v-show="clickControl" ref="layoutEvents" class="layout events"
|
||||
oncontextmenu="return false;"
|
||||
@mousedown.prevent.stop="onMouseDown" @mouseup.prevent.stop="onMouseUp"
|
||||
@wheel.prevent.stop="onMouseWheel"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||
oncontextmenu="return false;">
|
||||
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||
>
|
||||
<div
|
||||
v-show="showStatusBar && statusBarClickOpen" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"
|
||||
v-html="statusBarClickable"
|
||||
></div>
|
||||
</div>
|
||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick">
|
||||
<div
|
||||
v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout"
|
||||
@mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"
|
||||
v-html="statusBarClickable"
|
||||
>
|
||||
</div>
|
||||
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
|
||||
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||
@@ -35,10 +45,11 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import {loadCSS} from 'fg-loadcss';
|
||||
import _ from 'lodash';
|
||||
import he from 'he';
|
||||
|
||||
import './TextPage.css';
|
||||
|
||||
@@ -51,11 +62,18 @@ import {clickMap} from '../share/clickMap';
|
||||
|
||||
const minLayoutWidth = 100;
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
bookPos: function() {
|
||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||
|
||||
this.draw();
|
||||
|
||||
if (this.userBookPosChange) {
|
||||
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
|
||||
this.prevBookPos = this.bookPos;
|
||||
this.userBookPosChange = false;
|
||||
}
|
||||
},
|
||||
bookPosSeen: function() {
|
||||
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
|
||||
@@ -70,8 +88,10 @@ export default @Component({
|
||||
this.updateLayout();
|
||||
},
|
||||
},
|
||||
})
|
||||
class TextPage extends Vue {
|
||||
};
|
||||
class TextPage {
|
||||
_options = componentOptions;
|
||||
|
||||
toggleLayout = false;
|
||||
showStatusBar = false;
|
||||
clickControl = true;
|
||||
@@ -86,6 +106,8 @@ class TextPage extends Vue {
|
||||
lastBook = null;
|
||||
bookPos = 0;
|
||||
bookPosSeen = null;
|
||||
prevBookPos = 0;
|
||||
userBookPosChange = false;
|
||||
|
||||
fontStyle = null;
|
||||
fontSize = null;
|
||||
@@ -140,9 +162,9 @@ class TextPage extends Vue {
|
||||
await this.doPageAnimation();
|
||||
}, 10);
|
||||
|
||||
this.$root.$on('resize', async() => {
|
||||
this.$root.addEventHook('resize', async() => {
|
||||
this.$nextTick(this.onResize);
|
||||
await utils.sleep(500);
|
||||
await utils.sleep(200);
|
||||
this.$nextTick(this.onResize);
|
||||
});
|
||||
}
|
||||
@@ -436,7 +458,7 @@ class TextPage extends Vue {
|
||||
|
||||
this.title = bt.fullTitle;
|
||||
|
||||
this.$root.$emit('set-app-title', this.title);
|
||||
this.$root.setAppTitle(this.title);
|
||||
|
||||
this.parsed = this.book.parsed;
|
||||
|
||||
@@ -486,12 +508,25 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
async onResize() {
|
||||
if (this.resizing)
|
||||
return;
|
||||
|
||||
this.resizing = true;
|
||||
try {
|
||||
const scrolled = this.doingScrolling;
|
||||
if (scrolled)
|
||||
await this.stopTextScrolling();
|
||||
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
|
||||
if (scrolled)
|
||||
this.startTextScrolling();
|
||||
} catch (e) {
|
||||
//
|
||||
} finally {
|
||||
this.resizing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,7 +674,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
|
||||
this.doEnd(true);
|
||||
this.doEnd(true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -662,7 +697,7 @@ class TextPage extends Vue {
|
||||
this.debouncedDrawPageDividerAndOrnament();
|
||||
|
||||
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
||||
this.doEnd(true);
|
||||
this.doEnd(true, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -898,12 +933,14 @@ class TextPage extends Vue {
|
||||
|
||||
doDown() {
|
||||
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesDown[1].begin;
|
||||
}
|
||||
}
|
||||
|
||||
doUp() {
|
||||
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesUp[1].begin;
|
||||
}
|
||||
}
|
||||
@@ -916,6 +953,7 @@ class TextPage extends Vue {
|
||||
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = true;
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesDown[i].begin;
|
||||
} else
|
||||
this.doEnd();
|
||||
@@ -931,6 +969,7 @@ class TextPage extends Vue {
|
||||
if (i >= 0 && this.linesUp.length > i) {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = false;
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = this.linesUp[i].begin;
|
||||
}
|
||||
}
|
||||
@@ -939,10 +978,11 @@ class TextPage extends Vue {
|
||||
doHome() {
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = false;
|
||||
this.userBookPosChange = true;
|
||||
this.bookPos = 0;
|
||||
}
|
||||
|
||||
doEnd(noAni) {
|
||||
doEnd(noAni, isUser = true) {
|
||||
if (this.parsed.para.length && this.pageLineCount > 0) {
|
||||
let i = this.parsed.para.length - 1;
|
||||
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
|
||||
@@ -953,6 +993,7 @@ class TextPage extends Vue {
|
||||
if (!noAni)
|
||||
this.currentAnimation = this.pageChangeAnimation;
|
||||
this.pageChangeDirectionDown = true;
|
||||
this.userBookPosChange = isUser;
|
||||
this.bookPos = lines[i].begin;
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1077,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
@@ -1064,7 +1105,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchEnd(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
@@ -1100,13 +1141,13 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchCancel() {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onMouseDown(event) {
|
||||
if (this.$isMobileDevice)
|
||||
if (this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
if (event.button == 0) {
|
||||
@@ -1123,13 +1164,13 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.$isMobileDevice)
|
||||
if (this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onMouseWheel(event) {
|
||||
if (this.$isMobileDevice)
|
||||
if (this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.deltaY > 0) {
|
||||
this.doDown();
|
||||
@@ -1192,9 +1233,57 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
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);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -3,6 +3,8 @@ import sax from '../../../../server/core/sax';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
const maxParaLength = 10000;
|
||||
const maxParaTextLength = 10000;
|
||||
|
||||
// defaults
|
||||
const defaultSettings = {
|
||||
@@ -83,6 +85,7 @@ export default class BookParser {
|
||||
let binaryId = '';
|
||||
let binaryType = '';
|
||||
let dimPromises = [];
|
||||
this.coverPageId = '';
|
||||
|
||||
//оглавление
|
||||
this.contents = [];
|
||||
@@ -159,7 +162,24 @@ export default class BookParser {
|
||||
const prevParaIndex = paraIndex;
|
||||
let p = para[paraIndex];
|
||||
paraOffset -= p.length;
|
||||
//добавление пустых (addEmptyParagraphs) параграфов перед текущим непустым
|
||||
|
||||
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
|
||||
let newParaText = p.text.trim();
|
||||
newParaText = (newParaText.length ? newParaText : ' ');
|
||||
const ldiff = p.text.length - newParaText.length;
|
||||
if (ldiff != 0) {
|
||||
p.text = newParaText;
|
||||
p.length -= ldiff;
|
||||
}
|
||||
|
||||
//удаление параграфов, которые содержат только разметку, такого не должно быть
|
||||
if (!p.length) {
|
||||
delete para[paraIndex];
|
||||
paraIndex--;
|
||||
return;
|
||||
}
|
||||
|
||||
//добавление пустых (не)видимых (addEmptyParagraphs) параграфов перед текущим непустым
|
||||
if (p.text.trim() != '') {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
para[paraIndex] = {
|
||||
@@ -179,15 +199,6 @@ export default class BookParser {
|
||||
curSubtitle.paraIndex = paraIndex;
|
||||
}
|
||||
|
||||
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
|
||||
let newParaText = p.text.trim();
|
||||
newParaText = (newParaText.length ? newParaText : ' ');
|
||||
const ldiff = p.text.length - newParaText.length;
|
||||
if (ldiff != 0) {
|
||||
p.text = newParaText;
|
||||
p.length -= ldiff;
|
||||
}
|
||||
|
||||
p.index = paraIndex;
|
||||
p.offset = paraOffset;
|
||||
para[paraIndex] = p;
|
||||
@@ -203,7 +214,7 @@ export default class BookParser {
|
||||
let p = {
|
||||
index: paraIndex,
|
||||
offset: paraOffset,
|
||||
length: len,
|
||||
length: len,//длина текста внутри параграфа без учета длины разметки
|
||||
text: text,
|
||||
addIndex: 0,
|
||||
};
|
||||
@@ -218,13 +229,26 @@ export default class BookParser {
|
||||
paraOffset += len;
|
||||
};
|
||||
|
||||
const growParagraph = (text, len) => {
|
||||
const growParagraph = (text, len, textRaw) => {
|
||||
//начальный параграф
|
||||
if (paraIndex < 0) {
|
||||
newParagraph();
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
//ограничение на размер куска текста в параграфе
|
||||
if (textRaw && textRaw.length > maxParaTextLength) {
|
||||
while (textRaw.length > 0) {
|
||||
const textPart = textRaw.substring(0, maxParaTextLength);
|
||||
textRaw = textRaw.substring(maxParaTextLength);
|
||||
|
||||
newParagraph();
|
||||
growParagraph(textPart, textPart.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += text;
|
||||
} else if (inTitle) {
|
||||
@@ -232,6 +256,14 @@ export default class BookParser {
|
||||
}
|
||||
|
||||
const p = para[paraIndex];
|
||||
|
||||
//ограничение на размер параграфа
|
||||
if (p.length > maxParaLength) {
|
||||
newParagraph();
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
p.length += len;
|
||||
p.text += text;
|
||||
paraOffset += len;
|
||||
@@ -258,7 +290,7 @@ export default class BookParser {
|
||||
const href = attrs.href.value;
|
||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||
const {id, local} = this.imageHrefToId(href);
|
||||
if (href[0] == '#') {//local
|
||||
if (local) {//local
|
||||
imageNum++;
|
||||
|
||||
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||
@@ -270,6 +302,11 @@ export default class BookParser {
|
||||
|
||||
if (inPara && this.sets.showInlineImagesInCenter)
|
||||
newParagraph();
|
||||
|
||||
//coverpage
|
||||
if (path == '/fictionbook/description/title-info/coverpage/image') {
|
||||
this.coverPageId = id;
|
||||
}
|
||||
} else {//external
|
||||
imageNum++;
|
||||
|
||||
@@ -528,7 +565,7 @@ export default class BookParser {
|
||||
tClose += (center ? '</center>' : '');
|
||||
|
||||
if (text != ' ')
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length, text);
|
||||
else
|
||||
growParagraph(' ', 1);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import localForage from 'localforage';
|
||||
import path from 'path-browserify';
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
import BookParser from './BookParser';
|
||||
import readerApi from '../../../api/reader';
|
||||
import coversStorage from './coversStorage';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||
const maxRecentLength = 5000;
|
||||
|
||||
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||
const bmMetaStore = localForage.createInstance({
|
||||
@@ -17,9 +21,6 @@ const bmDataStore = localForage.createInstance({
|
||||
});
|
||||
|
||||
//список недавно открытых книг
|
||||
const bmRecentStoreOld = localForage.createInstance({
|
||||
name: 'bmRecentStore'
|
||||
});
|
||||
const bmRecentStoreNew = localForage.createInstance({
|
||||
name: 'bmRecentStoreNew'
|
||||
});
|
||||
@@ -39,7 +40,7 @@ class BookManager {
|
||||
|
||||
this.saveRecentItem = _.debounce(() => {
|
||||
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
||||
this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
|
||||
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||
}, 200, {maxWait: 300});
|
||||
|
||||
@@ -54,6 +55,9 @@ class BookManager {
|
||||
if (this.recentItem)
|
||||
this.recent[this.recentItem.key] = this.recentItem;
|
||||
|
||||
//конвертируем в новые ключи
|
||||
await this.convertRecent();
|
||||
|
||||
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||
if (this.recentLastKey) {
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||
@@ -63,48 +67,6 @@ class BookManager {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -112,6 +74,40 @@ class BookManager {
|
||||
this.loadStored();//no await
|
||||
}
|
||||
|
||||
//TODO: убрать в 2025г
|
||||
async convertRecent() {
|
||||
const converted = await bmRecentStoreNew.getItem('recent-converted');
|
||||
|
||||
if (converted)
|
||||
return;
|
||||
|
||||
const newRecent = {};
|
||||
for (const book of Object.values(this.recent)) {
|
||||
|
||||
if (!book.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newKey = this.keyFromPath(book.path);
|
||||
|
||||
newRecent[newKey] = _.cloneDeep(book);
|
||||
newRecent[newKey].key = newKey;
|
||||
if (!newRecent[newKey].loadTime)
|
||||
newRecent[newKey].loadTime = newRecent[newKey].addTime;
|
||||
}
|
||||
|
||||
this.recent = newRecent;
|
||||
|
||||
//console.log(converted);
|
||||
(async() => {
|
||||
await utils.sleep(3000);
|
||||
this.saveRecent();
|
||||
this.emit('recent-changed');
|
||||
this.emit('set-recent');
|
||||
await bmRecentStoreNew.setItem('recent-converted', true);
|
||||
})();
|
||||
}
|
||||
|
||||
//Ленивая асинхронная загрузка bmMetaStore
|
||||
async loadStored() {
|
||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||
@@ -238,8 +234,8 @@ class BookManager {
|
||||
|
||||
async addBook(newBook, callback) {
|
||||
let meta = {url: newBook.url, path: newBook.path};
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.addTime = Date.now();
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
meta.addTime = Date.now();//время добавления в кеш
|
||||
|
||||
const cb = (perc) => {
|
||||
const p = Math.round(30*perc/100);
|
||||
@@ -274,10 +270,10 @@ class BookManager {
|
||||
async hasBookParsed(meta) {
|
||||
if (!this.books)
|
||||
return false;
|
||||
if (!meta.url)
|
||||
if (!meta.path)
|
||||
return false;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
|
||||
let book = this.books[meta.key];
|
||||
|
||||
@@ -292,8 +288,12 @@ class BookManager {
|
||||
|
||||
async getBook(meta, callback) {
|
||||
let result = undefined;
|
||||
|
||||
if (!meta.path)
|
||||
return;
|
||||
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
meta.key = this.keyFromPath(meta.path);
|
||||
|
||||
result = this.books[meta.key];
|
||||
|
||||
@@ -303,11 +303,6 @@ class BookManager {
|
||||
this.books[meta.key] = result;
|
||||
}
|
||||
|
||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
||||
if (meta.path && result && meta.path != result.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && !result.parsed) {
|
||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||
callback(5);
|
||||
@@ -352,9 +347,38 @@ class BookManager {
|
||||
const parsed = new BookParser(this.settings);
|
||||
|
||||
const parsedMeta = await parsed.parse(data, callback);
|
||||
|
||||
//cover page
|
||||
let coverPageUrl = '';
|
||||
if (parsed.coverPageId && parsed.binary[parsed.coverPageId]) {
|
||||
const bin = parsed.binary[parsed.coverPageId];
|
||||
let dataUrl = `data:${bin.type};base64,${bin.data}`;
|
||||
try {
|
||||
dataUrl = await utils.resizeImage(dataUrl, 160, 160, 0.94);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
coverPageUrl = readerApi.makeUrlFromBuf(dataUrl);
|
||||
|
||||
//далее асинхронно
|
||||
(async() => {
|
||||
//отправим dataUrl на сервер в /upload
|
||||
try {
|
||||
await readerApi.uploadFileBuf(dataUrl, coverPageUrl);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
//сохраним в storage
|
||||
await coversStorage.setData(coverPageUrl, dataUrl);
|
||||
})();
|
||||
}
|
||||
|
||||
const result = Object.assign({}, meta, parsedMeta, {
|
||||
length: data.length,
|
||||
textLength: parsed.textLength,
|
||||
coverPageUrl,
|
||||
parsed
|
||||
});
|
||||
|
||||
@@ -367,14 +391,24 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
keyFromUrl(url) {
|
||||
/*keyFromUrl(url) {
|
||||
return utils.stringToHex(url);
|
||||
}*/
|
||||
|
||||
keyFromPath(bookPath) {
|
||||
return path.basename(bookPath);
|
||||
}
|
||||
|
||||
keysEqual(bookPath1, bookPath2) {
|
||||
if (bookPath1 === undefined || bookPath2 === undefined)
|
||||
return false;
|
||||
|
||||
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
|
||||
}
|
||||
//-- recent --------------------------------------------------------------
|
||||
async recentSetItem(item = null, skipCheck = false) {
|
||||
const rev = await bmRecentStoreNew.getItem('rev');
|
||||
if (rev != this.recentRev && !skipCheck) {
|
||||
if (rev != this.recentRev && !skipCheck) {//если изменение произошло в другой вкладке барузера
|
||||
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||
Object.assign(this.recent, newRecent);
|
||||
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||
@@ -411,7 +445,10 @@ class BookManager {
|
||||
|
||||
async setRecentBook(value) {
|
||||
let result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();
|
||||
result.touchTime = Date.now();//время последнего чтения
|
||||
if (!result.loadTime)
|
||||
result.loadTime = Date.now();//время загрузки файла
|
||||
|
||||
result.deleted = 0;
|
||||
|
||||
if (this.recent[result.key]) {
|
||||
@@ -427,9 +464,9 @@ class BookManager {
|
||||
return this.recent[value.key];
|
||||
}
|
||||
|
||||
async delRecentBook(value) {
|
||||
async delRecentBook(value, delFlag = 1) {
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 1;
|
||||
item.deleted = delFlag;
|
||||
|
||||
if (this.recentLastKey == value.key) {
|
||||
await this.recentSetLastKey(null);
|
||||
@@ -439,11 +476,18 @@ class BookManager {
|
||||
this.emit('recent-deleted', value.key);
|
||||
}
|
||||
|
||||
async restoreRecentBook(value) {
|
||||
const item = this.recent[value.key];
|
||||
item.deleted = 0;
|
||||
|
||||
await this.recentSetItem(item);
|
||||
}
|
||||
|
||||
async cleanRecentBooks() {
|
||||
const sorted = this.getSortedRecent();
|
||||
|
||||
let isDel = false;
|
||||
for (let i = 1000; i < sorted.length; i++) {
|
||||
for (let i = maxRecentLength; i < sorted.length; i++) {
|
||||
delete this.recent[sorted[i].key];
|
||||
isDel = true;
|
||||
}
|
||||
@@ -455,33 +499,6 @@ class BookManager {
|
||||
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() {
|
||||
if (this.recentLastKey) {
|
||||
return this.recent[this.recentLastKey];
|
||||
@@ -490,7 +507,7 @@ class BookManager {
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
for (let key in this.recent) {
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.touchTime > max) {
|
||||
max = book.touchTime;
|
||||
@@ -521,6 +538,43 @@ class BookManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
findRecentByUrlAndPath(url, bookPath) {
|
||||
if (bookPath) {
|
||||
const key = this.keyFromPath(bookPath);
|
||||
const book = this.recent[key];
|
||||
if (book && !book.deleted)
|
||||
return book;
|
||||
}
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.url == url && book.loadTime > max) {
|
||||
max = book.loadTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
findRecentBySameBookKey(sameKey) {
|
||||
let max = 0;
|
||||
let result = null;
|
||||
|
||||
for (const key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
|
||||
max = book.loadTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async setRecent(value) {
|
||||
const mergedRecent = _.cloneDeep(this.recent);
|
||||
|
||||
|
||||
61
client/components/Reader/share/coversStorage.js
Normal file
61
client/components/Reader/share/coversStorage.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import localForage from 'localforage';
|
||||
//import _ from 'lodash';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const maxDataSize = 100*1024*1024;
|
||||
|
||||
const coversStore = localForage.createInstance({
|
||||
name: 'coversStorage'
|
||||
});
|
||||
|
||||
class CoversStorage {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.cleanCovers(); //no await
|
||||
}
|
||||
|
||||
async setData(key, data) {
|
||||
await coversStore.setItem(key, {addTime: Date.now(), data});
|
||||
}
|
||||
|
||||
async getData(key) {
|
||||
const item = await coversStore.getItem(key);
|
||||
return (item ? item.data : undefined);
|
||||
}
|
||||
|
||||
async removeData(key) {
|
||||
await coversStore.removeItem(key);
|
||||
}
|
||||
|
||||
async cleanCovers() {
|
||||
await utils.sleep(10000);
|
||||
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
let size = 0;
|
||||
let min = Date.now();
|
||||
let toDel = null;
|
||||
for (const key of (await coversStore.keys())) {
|
||||
const item = await coversStore.getItem(key);
|
||||
|
||||
size += item.data.length;
|
||||
|
||||
if (item.addTime < min) {
|
||||
toDel = key;
|
||||
min = item.addTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (size > maxDataSize && toDel) {
|
||||
await this.removeData(toDel);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new CoversStorage();
|
||||
@@ -32,6 +32,10 @@ class WallpaperStorage {
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
async getKeys() {
|
||||
return await wpStore.keys();
|
||||
}
|
||||
|
||||
keyExists(key) {//не асинхронная
|
||||
return this.cachedKeys.includes(key);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,137 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
version: '0.11.8',
|
||||
releaseDate: '2022-07-14',
|
||||
showUntil: '2022-07-13',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено отображение и синхронизация обложек в окне загруженных книг</li>
|
||||
<li>добавлена синхронизация обоев</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.11.7',
|
||||
releaseDate: '2022-07-12',
|
||||
showUntil: '2022-07-19',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
|
||||
<li>изменения в окне загруженных книг:</li>
|
||||
<ul>
|
||||
<li>добавлена группировка по версиям файла одной и той же книги</li>
|
||||
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
|
||||
<li>добавлены различные методы сортировки списка загруженных книг</li>
|
||||
<li>нумерация всегда осуществляется по времени загрузки</li>
|
||||
</ul>
|
||||
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
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',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на Vue 3</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.3',
|
||||
releaseDate: '2021-10-24',
|
||||
showUntil: '2021-10-23',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.2',
|
||||
releaseDate: '2021-10-19',
|
||||
showUntil: '2021-10-18',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>актуализация версий пакетов и стека используемых технологий</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.1',
|
||||
releaseDate: '2021-10-10',
|
||||
showUntil: '2021-10-09',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.10.0',
|
||||
releaseDate: '2021-02-09',
|
||||
showUntil: '2021-02-16',
|
||||
header: '0.10.0 (2021-02-09)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -10,12 +140,14 @@ export const versionHistory = [
|
||||
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||
<li>немного улучшен парсинг fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.12',
|
||||
releaseDate: '2020-12-18',
|
||||
showUntil: '2020-12-17',
|
||||
header: '0.9.12 (2020-12-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -24,23 +156,27 @@ export const versionHistory = [
|
||||
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||
<li>улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.11',
|
||||
releaseDate: '2020-12-09',
|
||||
showUntil: '2020-12-08',
|
||||
header: '0.9.11 (2020-12-09)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.10',
|
||||
releaseDate: '2020-12-03',
|
||||
showUntil: '2020-12-10',
|
||||
header: '0.9.10 (2020-12-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -48,69 +184,81 @@ export const versionHistory = [
|
||||
<li>добавлена поддержка Rar-архивов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.9',
|
||||
releaseDate: '2020-11-21',
|
||||
showUntil: '2020-11-20',
|
||||
header: '0.9.9 (2020-11-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.8',
|
||||
releaseDate: '2020-11-13',
|
||||
showUntil: '2020-11-12',
|
||||
header: '0.9.8 (2020-11-13)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено окно "Оглавление/закладки"</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.7',
|
||||
releaseDate: '2020-11-12',
|
||||
showUntil: '2020-11-11',
|
||||
header: '0.9.7 (2020-11-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.6',
|
||||
releaseDate: '2020-11-06',
|
||||
showUntil: '2020-11-05',
|
||||
header: '0.9.6 (2020-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>завершена работа над новым окном "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.5',
|
||||
releaseDate: '2020-11-01',
|
||||
showUntil: '2020-10-31',
|
||||
header: '0.9.5 (2020-11-01)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>на панель инструментов добавлена новая кнопка "Обновить с разбиением на параграфы"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.4',
|
||||
releaseDate: '2020-10-29',
|
||||
showUntil: '2020-10-28',
|
||||
header: '0.9.4 (2020-10-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -118,23 +266,27 @@ export const versionHistory = [
|
||||
<li>для liberama.top добавлено новое окно: "Библиотека"</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.3',
|
||||
releaseDate: '2020-05-21',
|
||||
showUntil: '2020-05-20',
|
||||
header: '0.9.3 (2020-05-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.2',
|
||||
releaseDate: '2020-03-15',
|
||||
showUntil: '2020-04-25',
|
||||
header: '0.9.2 (2020-03-15)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -142,119 +294,139 @@ export const versionHistory = [
|
||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.1',
|
||||
releaseDate: '2020-03-03',
|
||||
showUntil: '2020-03-02',
|
||||
header: '0.9.1 (2020-03-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение работы серверной части</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.0',
|
||||
releaseDate: '2020-02-26',
|
||||
showUntil: '2020-02-25',
|
||||
header: '0.9.0 (2020-02-26)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на UI-фреймфорк Quasar</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.4',
|
||||
releaseDate: '2020-02-06',
|
||||
showUntil: '2020-02-05',
|
||||
header: '0.8.4 (2020-02-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен paypal-адрес для пожертвований</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.3',
|
||||
releaseDate: '2020-01-28',
|
||||
showUntil: '2020-01-27',
|
||||
header: '0.8.3 (2020-01-28)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.2',
|
||||
releaseDate: '2020-01-20',
|
||||
showUntil: '2020-01-19',
|
||||
header: '0.8.2 (2020-01-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>внутренние оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.1',
|
||||
releaseDate: '2020-01-07',
|
||||
showUntil: '2020-01-06',
|
||||
header: '0.8.1 (2020-01-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена частичная поддержка формата FB3</li>
|
||||
<li>исправлен баг "Request path contains unescaped characters"</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.8.0',
|
||||
releaseDate: '2020-01-02',
|
||||
showUntil: '2020-01-05',
|
||||
header: '0.8.0 (2020-01-02)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>окончательный переход на https</li>
|
||||
<li>код проекта теперь Open Source: <a href="https://github.com/bookpauk/liberama" target="_blank">https://github.com/bookpauk/liberama</a></li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.9',
|
||||
releaseDate: '2019-11-27',
|
||||
showUntil: '2019-11-26',
|
||||
header: '0.7.9 (2019-11-27)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен неубираемый баннер для http-версии о переходе на httpS</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.8',
|
||||
releaseDate: '2019-11-25',
|
||||
showUntil: '2019-11-24',
|
||||
header: '0.7.8 (2019-11-25)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение html-фильтров для сайтов</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.7',
|
||||
releaseDate: '2019-11-06',
|
||||
showUntil: '2019-11-10',
|
||||
header: '0.7.7 (2019-11-06)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -266,34 +438,40 @@ export const versionHistory = [
|
||||
<li style="list-style-type: square">от центра влево: уменьшить скорость скроллинга</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.6',
|
||||
releaseDate: '2019-10-30',
|
||||
showUntil: '2019-10-29',
|
||||
header: '0.7.6 (2019-10-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.5',
|
||||
releaseDate: '2019-10-22',
|
||||
showUntil: '2019-10-21',
|
||||
header: '0.7.5 (2019-10-22)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.3',
|
||||
releaseDate: '2019-10-18',
|
||||
showUntil: '2019-10-17',
|
||||
header: '0.7.3 (2019-10-18)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -302,12 +480,14 @@ export const versionHistory = [
|
||||
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.1',
|
||||
releaseDate: '2019-09-20',
|
||||
showUntil: '2019-09-19',
|
||||
header: '0.7.1 (2019-09-20)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -315,12 +495,14 @@ export const versionHistory = [
|
||||
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||
<li>актуализирована справка</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.7.0',
|
||||
releaseDate: '2019-09-07',
|
||||
showUntil: '2019-10-01',
|
||||
header: '0.7.0 (2019-09-07)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -331,23 +513,27 @@ export const versionHistory = [
|
||||
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.10',
|
||||
releaseDate: '2019-07-21',
|
||||
showUntil: '2019-07-20',
|
||||
header: '0.6.10 (2019-07-21)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.9',
|
||||
releaseDate: '2019-06-23',
|
||||
showUntil: '2019-06-22',
|
||||
header: '0.6.9 (2019-06-23)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -358,12 +544,14 @@ export const versionHistory = [
|
||||
<li>улучшены прогрессбары</li>
|
||||
<li>исправления недочетов, небольшие оптимизации</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.7',
|
||||
releaseDate: '2019-05-30',
|
||||
showUntil: '2019-06-05',
|
||||
header: '0.6.7 (2019-05-30)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -376,36 +564,42 @@ export const versionHistory = [
|
||||
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||
<li>исправления багов и недочетов</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.6',
|
||||
releaseDate: '2019-03-29',
|
||||
showUntil: '2019-03-29',
|
||||
header: '0.6.6 (2019-03-29)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.6.4',
|
||||
releaseDate: '2019-03-24',
|
||||
showUntil: '2019-03-24',
|
||||
header: '0.6.4 (2019-03-24)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>исправления багов, оптимизации</li>
|
||||
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.5.4',
|
||||
releaseDate: '2019-03-04',
|
||||
showUntil: '2019-03-04',
|
||||
header: '0.5.4 (2019-03-04)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -414,12 +608,14 @@ export const versionHistory = [
|
||||
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.3.0',
|
||||
releaseDate: '2019-02-17',
|
||||
showUntil: '2019-02-17',
|
||||
header: '0.3.0 (2019-02-17)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -427,12 +623,14 @@ export const versionHistory = [
|
||||
<li>улучшено распознавание текста</li>
|
||||
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.1.7',
|
||||
releaseDate: '2019-02-14',
|
||||
showUntil: '2019-02-14',
|
||||
header: '0.1.7 (2019-02-14)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
@@ -442,17 +640,20 @@ export const versionHistory = [
|
||||
<li>добавлена возможность сброса настроек</li>
|
||||
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.1.0',
|
||||
releaseDate: '2019-02-12',
|
||||
showUntil: '2019-02-12',
|
||||
header: '0.1.0 (2019-02-12)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Settings extends Vue {
|
||||
class Settings {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Settings);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Sources extends Vue {
|
||||
class Sources {
|
||||
created() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(Sources);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<q-dialog v-model="active" no-route-dismiss>
|
||||
<q-dialog v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
|
||||
<div class="column bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -25,26 +25,42 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const DialogProps = Vue.extend({
|
||||
props: {
|
||||
value: Boolean,
|
||||
}
|
||||
});
|
||||
class Dialog {
|
||||
_props = {
|
||||
modelValue: Boolean,
|
||||
};
|
||||
|
||||
shown = false;
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Dialog extends DialogProps {
|
||||
get active() {
|
||||
return this.value;
|
||||
return this.modelValue;
|
||||
}
|
||||
|
||||
set active(value) {
|
||||
this.$emit('input', value);
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.shown = true;
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.shown = false;
|
||||
}
|
||||
|
||||
async waitShown() {
|
||||
let i = 100;
|
||||
while (!this.shown && i > 0) {
|
||||
await utils.sleep(10);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Dialog);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Notify extends Vue {
|
||||
class Notify {
|
||||
notify(opts) {
|
||||
let {
|
||||
caption = null,
|
||||
@@ -55,5 +52,7 @@ class Notify extends Vue {
|
||||
this.notify(Object.assign({color: 'info', icon: 'la la-bell', message, caption}, options));
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Notify);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<template>
|
||||
<q-input outlined dense
|
||||
<q-input
|
||||
v-model="filteredValue"
|
||||
outlined dense
|
||||
input-style="text-align: center"
|
||||
class="no-mp"
|
||||
:class="(error ? 'error' : '')"
|
||||
:disable="disable"
|
||||
>
|
||||
<slot></slot>
|
||||
<template v-slot:prepend>
|
||||
<q-icon :class="(validate(value - step) ? '' : 'disable')"
|
||||
<template #prepend>
|
||||
<q-icon
|
||||
v-ripple="validate(modelValue - step)"
|
||||
:class="(validate(modelValue - step) ? '' : 'disable')"
|
||||
name="la la-minus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value - step)"
|
||||
@click="minus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@@ -21,11 +23,12 @@
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon :class="(validate(value + step) ? '' : 'disable')"
|
||||
<template #append>
|
||||
<q-icon
|
||||
v-ripple="validate(modelValue + step)"
|
||||
:class="(validate(modelValue + step) ? '' : 'disable')"
|
||||
name="la la-plus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value + step)"
|
||||
@click="plus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@@ -40,43 +43,41 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const NumInputProps = Vue.extend({
|
||||
props: {
|
||||
value: Number,
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
filteredValue: function(newValue) {
|
||||
if (this.validate(newValue)) {
|
||||
this.error = false;
|
||||
this.$emit('update:modelValue', this.string2number(newValue));
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
modelValue: function(newValue) {
|
||||
this.filteredValue = newValue;
|
||||
},
|
||||
}
|
||||
};
|
||||
class NumInput {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
modelValue: Number,
|
||||
min: { type: Number, default: -Number.MAX_VALUE },
|
||||
max: { type: Number, default: Number.MAX_VALUE },
|
||||
step: { type: Number, default: 1 },
|
||||
digits: { type: Number, default: 0 },
|
||||
disable: Boolean
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
filteredValue: function(newValue) {
|
||||
if (this.validate(newValue)) {
|
||||
this.error = false;
|
||||
this.$emit('input', this.string2number(newValue));
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
value: function(newValue) {
|
||||
this.filteredValue = newValue;
|
||||
},
|
||||
}
|
||||
})
|
||||
class NumInput extends NumInputProps {
|
||||
filteredValue = 0;
|
||||
error = false;
|
||||
|
||||
created() {
|
||||
this.filteredValue = this.value;
|
||||
this.filteredValue = this.modelValue;
|
||||
}
|
||||
|
||||
string2number(value) {
|
||||
@@ -95,13 +96,13 @@ class NumInput extends NumInputProps {
|
||||
}
|
||||
|
||||
plus() {
|
||||
const newValue = this.value + this.step;
|
||||
const newValue = this.modelValue + this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
minus() {
|
||||
const newValue = this.value - this.step;
|
||||
const newValue = this.modelValue - this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
@@ -136,7 +137,7 @@ class NumInput extends NumInputProps {
|
||||
}
|
||||
|
||||
onTouchStart(event, way) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
this.inTouch = true;
|
||||
@@ -145,12 +146,14 @@ class NumInput extends NumInputProps {
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.inTouch = false;
|
||||
this.onMouseUp();
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(NumInput);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
|
||||
<q-dialog ref="dialog" v-model="active" no-route-dismiss @show="onShow" @hide="onHide">
|
||||
<slot></slot>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
@@ -10,7 +10,7 @@
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -21,7 +21,9 @@
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
|
||||
<q-btn class="q-px-md" dense no-caps @click="okClick">
|
||||
OK
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +35,7 @@
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -44,8 +46,40 @@
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||
OK
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Нет
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||
Да
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +91,7 @@
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -65,13 +99,19 @@
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
|
||||
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
|
||||
<q-input ref="input" v-model="inputValue" class="q-mt-xs" outlined dense />
|
||||
<div class="error">
|
||||
<span v-show="error != ''">{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||
OK
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +123,7 @@
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-btn v-close-popup flat round dense>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
@@ -92,14 +132,20 @@
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<div class="q-my-md text-center">
|
||||
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
|
||||
<div v-show="hotKeyCode == ''" class="text-grey-5">
|
||||
Нет
|
||||
</div>
|
||||
<div>{{ hotKeyCode }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
|
||||
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
|
||||
Отмена
|
||||
</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps :disabled="hotKeyCode == ''" @click="okClick">
|
||||
OK
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
@@ -107,19 +153,18 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import vueComponent from '../vueComponent.js';
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
watch: {
|
||||
inputValue: function(newValue) {
|
||||
this.validate(newValue);
|
||||
},
|
||||
}
|
||||
})
|
||||
class StdDialog extends Vue {
|
||||
};
|
||||
class StdDialog {
|
||||
_options = componentOptions;
|
||||
caption = '';
|
||||
message = '';
|
||||
active = false;
|
||||
@@ -131,8 +176,8 @@ class StdDialog extends Vue {
|
||||
hotKeyCode = '';
|
||||
|
||||
created() {
|
||||
if (this.$root.addKeyHook) {
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
if (this.$root.addEventHook) {
|
||||
this.$root.addEventHook('key', this.keyHook);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +290,23 @@ class StdDialog extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
askYesNo(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'askYesNo';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
prompt(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.enableValidator = false;
|
||||
@@ -313,6 +375,8 @@ class StdDialog extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(StdDialog);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
|
||||
<div ref="window" class="window flexfit column no-wrap">
|
||||
<div ref="header" class="header row justify-end" @mousedown.prevent.stop="onMouseDown"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
||||
<div
|
||||
ref="header"
|
||||
class="header row justify-end"
|
||||
@mousedown.prevent.stop="onMouseDown"
|
||||
@touchstart.stop="onTouchStart"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchmove.stop="onTouchMove"
|
||||
>
|
||||
<span class="header-text col"><slot name="header"></slot></span>
|
||||
<slot name="buttons"></slot>
|
||||
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
|
||||
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px" /></span>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
@@ -17,19 +23,17 @@
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import vueComponent from '../vueComponent.js';
|
||||
|
||||
export default @Component({
|
||||
props: {
|
||||
class Window {
|
||||
_props = {
|
||||
height: { type: String, default: '100%' },
|
||||
width: { type: String, default: '100%' },
|
||||
maxWidth: { type: String, default: '' },
|
||||
topShift: { type: Number, default: 0 },
|
||||
margin: '',
|
||||
}
|
||||
})
|
||||
class Window extends Vue {
|
||||
};
|
||||
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.main.style.top = 0;
|
||||
@@ -51,7 +55,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onMouseDown(event) {
|
||||
if (this.$isMobileDevice)
|
||||
if (this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.button == 0) {
|
||||
this.$refs.header.style.cursor = 'move';
|
||||
@@ -81,7 +85,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
const touch = event.touches[0];
|
||||
@@ -93,7 +97,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onTouchMove(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1 && this.moving) {
|
||||
const touch = event.touches[0];
|
||||
@@ -108,7 +112,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.$refs.header.style.cursor = 'default';
|
||||
this.moving = false;
|
||||
@@ -120,6 +124,8 @@ class Window extends Vue {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(Window);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -147,7 +153,7 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(to bottom right, green, #59B04F);
|
||||
background: linear-gradient(to bottom right, #007000, #59B04F);
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
}
|
||||
@@ -155,8 +161,8 @@ class Window extends Vue {
|
||||
.header-text {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: yellow;
|
||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
||||
color: #FFFFA0;
|
||||
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -168,7 +174,8 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #69C05F;
|
||||
color: white;
|
||||
background-color: #FF3030;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
52
client/components/vueComponent.js
Normal file
52
client/components/vueComponent.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function(componentClass) {
|
||||
const comp = {};
|
||||
const obj = new componentClass();
|
||||
|
||||
//data, options, props
|
||||
const data = {};
|
||||
for (const prop of Object.getOwnPropertyNames(obj)) {
|
||||
if (['_options', '_props'].includes(prop)) {//meta props
|
||||
if (prop === '_options') {
|
||||
const options = obj[prop];
|
||||
for (const optName of ['components', 'watch', 'emits']) {
|
||||
if (options[optName]) {
|
||||
comp[optName] = options[optName];
|
||||
}
|
||||
}
|
||||
} else if (prop === '_props') {
|
||||
comp['props'] = obj[prop];
|
||||
}
|
||||
} else {//usual prop
|
||||
data[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
comp.data = () => _.cloneDeep(data);
|
||||
|
||||
//methods
|
||||
const classProto = Object.getPrototypeOf(obj);
|
||||
const classMethods = Object.getOwnPropertyNames(classProto);
|
||||
const methods = {};
|
||||
const computed = {};
|
||||
for (const method of classMethods) {
|
||||
const desc = Object.getOwnPropertyDescriptor(classProto, method);
|
||||
if (desc.get) {//has getter, computed
|
||||
computed[method] = {get: desc.get};
|
||||
if (desc.set)
|
||||
computed[method].set = desc.set;
|
||||
} else if ( ['beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated',//life cycle hooks
|
||||
'deactivated', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered',//life cycle hooks
|
||||
'setup'].includes(method) ) {
|
||||
comp[method] = obj[method];
|
||||
} else if (method !== 'constructor') {//usual
|
||||
methods[method] = obj[method];
|
||||
}
|
||||
}
|
||||
comp.methods = methods;
|
||||
comp.computed = computed;
|
||||
|
||||
//console.log(comp);
|
||||
return defineComponent(comp);
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
import Vue from 'vue';
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './quasar';
|
||||
|
||||
import vueSanitize from 'vue-sanitize';
|
||||
Vue.use(vueSanitize);
|
||||
import q from './quasar';
|
||||
|
||||
import App from './components/App.vue';
|
||||
//Vue.config.productionTip = false;
|
||||
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App),
|
||||
}).$mount('#app');
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.use(q.quasar, q.options);
|
||||
q.init();
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import 'quasar/dist/quasar.css';
|
||||
import Quasar from 'quasar/src/vue-plugin.js'
|
||||
//import Quasar from 'quasar/dist/quasar.umd.prod.js';
|
||||
|
||||
import Quasar from 'quasar/src/vue-plugin.js';
|
||||
//config
|
||||
const config = {};
|
||||
|
||||
@@ -33,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||
import {QDialog} from 'quasar/src/components/dialog';
|
||||
import {QChip} from 'quasar/src/components/chip';
|
||||
import {QTree} from 'quasar/src/components/tree';
|
||||
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
|
||||
|
||||
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
|
||||
|
||||
const components = {
|
||||
@@ -63,6 +64,7 @@ const components = {
|
||||
QChip,
|
||||
QTree,
|
||||
//QExpansionItem,
|
||||
QVirtualScroll,
|
||||
};
|
||||
|
||||
//directives
|
||||
@@ -80,16 +82,17 @@ const plugins = {
|
||||
Notify,
|
||||
};
|
||||
|
||||
//use
|
||||
Vue.use(Quasar, { config, components, directives, plugins });
|
||||
|
||||
//icons
|
||||
//import '@quasar/extras/material-icons/material-icons.css';
|
||||
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
|
||||
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
|
||||
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||
|
||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||
|
||||
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||
Quasar.iconSet.set(lineAwesome);
|
||||
|
||||
export default {
|
||||
quasar: Quasar,
|
||||
options: { config, components, directives, plugins },
|
||||
init: () => {
|
||||
Quasar.iconSet.set(lineAwesome);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import _ from 'lodash';
|
||||
|
||||
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
||||
@@ -36,7 +35,7 @@ const myRoutes = [
|
||||
['/settings', Settings],
|
||||
['/help', Help],
|
||||
['/404', NotFound404],
|
||||
['*', null, null, '/cardindex'],
|
||||
['/:pathMatch(.*)*', null, null, '/cardindex'],
|
||||
];
|
||||
|
||||
let routes = {};
|
||||
@@ -63,8 +62,7 @@ for (let route of myRoutes) {
|
||||
}
|
||||
routes = routes.children;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
export default new VueRouter({
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
53
client/share/LockQueue.js
Normal file
53
client/share/LockQueue.js
Normal file
@@ -0,0 +1,53 @@
|
||||
class LockQueue {
|
||||
constructor(queueSize) {
|
||||
this.queueSize = queueSize;
|
||||
this.freed = true;
|
||||
this.waitingQueue = [];
|
||||
}
|
||||
|
||||
//async
|
||||
get(take = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.freed) {
|
||||
if (take)
|
||||
this.freed = false;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.waitingQueue.length < this.queueSize) {
|
||||
this.waitingQueue.push({resolve, reject});
|
||||
} else {
|
||||
reject(new Error('Lock queue is too long'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ret() {
|
||||
if (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().resolve();
|
||||
} else {
|
||||
this.freed = true;
|
||||
}
|
||||
}
|
||||
|
||||
//async
|
||||
wait() {
|
||||
return this.get(false);
|
||||
}
|
||||
|
||||
retAll() {
|
||||
while (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().resolve();
|
||||
}
|
||||
}
|
||||
|
||||
errAll(error = 'rejected') {
|
||||
while (this.waitingQueue.length) {
|
||||
this.waitingQueue.shift().reject(new Error(error));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default LockQueue;
|
||||
@@ -90,7 +90,7 @@ export function toBase58(data) {
|
||||
}
|
||||
|
||||
export function fromBase58(data) {
|
||||
return bs58.decode(data);
|
||||
return Buffer.from(bs58.decode(data));
|
||||
}
|
||||
|
||||
//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 = {}) {
|
||||
const {
|
||||
exclude = [],
|
||||
@@ -126,7 +130,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
for (const key of Object.keys(oldObj)) {
|
||||
const kp = `${keyPath}${key}`;
|
||||
|
||||
if (newObj.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||||
if (ex.has(kp))
|
||||
continue;
|
||||
|
||||
@@ -149,7 +153,7 @@ export function getObjDiff(oldObj, newObj, opts = {}) {
|
||||
if (exAdd.has(kp))
|
||||
continue;
|
||||
|
||||
if (!oldObj.hasOwnProperty(key)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(oldObj, key)) {
|
||||
result.add[key] = _.cloneDeep(newObj[key]);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +217,7 @@ export function applyObjDiff(obj, diff, opts = {}) {
|
||||
|
||||
const change = diff.change;
|
||||
for (const key of Object.keys(change)) {
|
||||
if (result.hasOwnProperty(key)) {
|
||||
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
||||
if (_.isObject(change[key])) {
|
||||
result[key] = applyObjDiff(result[key], change[key], opts);
|
||||
} else {
|
||||
@@ -359,4 +363,50 @@ export function getBookTitle(fb2) {
|
||||
]).join(' - ');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function resizeImage(dataUrl, toWidth, toHeight, quality = 0.9) {
|
||||
return new Promise ((resolve, reject) => { (async() => {
|
||||
const img = new Image();
|
||||
|
||||
let resolved = false;
|
||||
img.onload = () => {
|
||||
try {
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > height) {
|
||||
if (width > toWidth) {
|
||||
height = height * (toWidth / width);
|
||||
width = toWidth;
|
||||
}
|
||||
} else {
|
||||
if (height > toHeight) {
|
||||
width = width * (toHeight / height);
|
||||
height = toHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
const result = canvas.toDataURL('image/jpeg', quality);
|
||||
resolved = true;
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = reject;
|
||||
|
||||
img.src = dataUrl;
|
||||
|
||||
await sleep(1000);
|
||||
if (!resolved)
|
||||
reject('Не удалось изменить размер');
|
||||
})().catch(reject); });
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { createStore } from 'vuex';
|
||||
import createPersistedState from 'vuex-persistedstate';
|
||||
|
||||
import root from './root.js';
|
||||
@@ -7,11 +6,9 @@ import uistate from './modules/uistate';
|
||||
import config from './modules/config';
|
||||
import reader from './modules/reader';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export default new Vuex.Store(Object.assign({}, root, {
|
||||
export default createStore(Object.assign({}, root, {
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
|
||||
@@ -2,8 +2,10 @@ import * as utils from '../../share/utils';
|
||||
import googleFonts from './fonts/fonts.json';
|
||||
|
||||
const readerActions = {
|
||||
'help': 'Вызвать cправку',
|
||||
'loader': 'На страницу загрузки',
|
||||
'loadFile': 'Загрузить файл с диска',
|
||||
'loadBuffer': 'Загрузить из буфера обмена',
|
||||
'help': 'Вызвать cправку',
|
||||
'settings': 'Настроить',
|
||||
'undoAction': 'Действие назад',
|
||||
'redoAction': 'Действие вперед',
|
||||
@@ -15,10 +17,11 @@ const readerActions = {
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'convOptions': 'Настроить конвертирование',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'clickControl': 'Управление кликом',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Сетевая библиотека',
|
||||
'recentBooks': 'Открыть недавние',
|
||||
'recentBooks': 'Показать загруженные',
|
||||
'switchToolbar': 'Показать/скрыть панель управления',
|
||||
'donate': '',
|
||||
'bookBegin': 'В начало книги',
|
||||
@@ -35,6 +38,9 @@ const readerActions = {
|
||||
|
||||
//readerActions[name]
|
||||
const toolButtons = [
|
||||
{name: 'loadFile', show: true},
|
||||
{name: 'loadBuffer', show: true},
|
||||
{name: 'help', show: true},
|
||||
{name: 'undoAction', show: true},
|
||||
{name: 'redoAction', show: true},
|
||||
{name: 'fullScreen', show: true},
|
||||
@@ -47,13 +53,16 @@ const toolButtons = [
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
{name: 'recentBooks', show: true},
|
||||
{name: 'clickControl', show: false},
|
||||
{name: 'offlineMode', show: false},
|
||||
];
|
||||
|
||||
//readerActions[name]
|
||||
const hotKeys = [
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{name: 'loader', codes: ['Escape']},
|
||||
{name: 'loadFile', codes: ['F3']},
|
||||
{name: 'loadBuffer', codes: ['F4']},
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{name: 'settings', codes: ['S']},
|
||||
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||
@@ -61,12 +70,13 @@ const hotKeys = [
|
||||
{name: 'scrolling', codes: ['Z']},
|
||||
{name: 'setPosition', codes: ['P']},
|
||||
{name: 'search', codes: ['Ctrl+F']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'copyText', codes: ['Ctrl+Space']},
|
||||
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||
{name: 'refresh', codes: ['R']},
|
||||
{name: 'contents', codes: ['C']},
|
||||
{name: 'libs', codes: ['L']},
|
||||
{name: 'recentBooks', codes: ['X']},
|
||||
{name: 'clickControl', codes: ['Ctrl+B']},
|
||||
{name: 'offlineMode', codes: ['O']},
|
||||
|
||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||
@@ -175,8 +185,14 @@ const settingDefaults = {
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
toolBarHideOnScroll: true,
|
||||
userHotKeys: {},
|
||||
userWallpapers: [],
|
||||
|
||||
recentShowSameBook: false,
|
||||
recentSortMethod: '',
|
||||
|
||||
needUpdateSettingsView: 0,
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
@@ -212,9 +228,6 @@ const libsDefaults = {
|
||||
{r: 'http://flibusta.is', s: 'http://flibusta.is', list: [
|
||||
{l: 'http://flibusta.is', c: 'Флибуста | Книжное братство'},
|
||||
]},
|
||||
{r: 'https://flibs.in', s: 'https://flibs.in', list: [
|
||||
{l: 'https://flibs.in', c: 'Flibs'},
|
||||
]},
|
||||
{r: 'http://fantasy-worlds.org', s: 'http://fantasy-worlds.org', list: [
|
||||
{l: 'http://fantasy-worlds.org', c: 'Миры Фэнтези'},
|
||||
]},
|
||||
|
||||
@@ -6,6 +6,7 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
server_name beta.liberama.top;
|
||||
set $liberama http://127.0.0.1:34082;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -15,15 +16,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -32,6 +38,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -50,6 +61,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name b.beta.liberama.top;
|
||||
set $liberama http://127.0.0.1:34082;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -59,15 +71,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34082;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -76,6 +93,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
|
||||
@@ -6,6 +6,7 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
server_name beta.omnireader.ru;
|
||||
set $liberama http://127.0.0.1:34081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -15,15 +16,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -32,6 +38,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
|
||||
54
docs/beta/beta.omnireader_http
Normal file
54
docs/beta/beta.omnireader_http
Normal file
@@ -0,0 +1,54 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name beta.omnireader.ru;
|
||||
set $liberama http://127.0.0.1:34081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /home/beta.liberama/public;
|
||||
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
expires -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name beta.omnireader.ru;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -17,8 +17,9 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
server_name liberama.top;
|
||||
set $liberama http://127.0.0.1:55081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
client_max_body_size 100m;
|
||||
proxy_read_timeout 1h;
|
||||
|
||||
gzip on;
|
||||
@@ -26,15 +27,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -43,6 +49,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -61,8 +72,9 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
server_name b.liberama.top;
|
||||
set $liberama http://127.0.0.1:55081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
client_max_body_size 100m;
|
||||
proxy_read_timeout 1h;
|
||||
|
||||
gzip on;
|
||||
@@ -70,15 +82,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:55081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -87,6 +104,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -139,5 +161,6 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_pass http://fantasy-worlds.org;
|
||||
proxy_hide_header x-frame-options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
## Разворачивание сервера OmniReader в Ubuntu:
|
||||
## Разворачивание сервера OmniReader в Ubuntu 20.04:
|
||||
|
||||
### git, clone
|
||||
```
|
||||
cd ~
|
||||
sudo apt install ssh git
|
||||
git clone https://github.com/bookpauk/liberama
|
||||
```
|
||||
@@ -9,7 +10,7 @@ git clone https://github.com/bookpauk/liberama
|
||||
### node.js
|
||||
```
|
||||
sudo apt install -y curl
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
```
|
||||
|
||||
@@ -25,29 +26,25 @@ sudo mkdir /home/liberama
|
||||
sudo chown www-data.www-data /home/liberama
|
||||
```
|
||||
|
||||
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
||||
### external converter `calibre`
|
||||
#### download from https://download.calibre-ebook.com/
|
||||
```
|
||||
wget "https://download.calibre-ebook.com/5.5.0/calibre-5.5.0-x86_64.txz"
|
||||
wget "https://download.calibre-ebook.com/5.29.0/calibre-5.29.0-x86_64.txz"
|
||||
sudo -u www-data mkdir -p /home/liberama/data/calibre
|
||||
sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
|
||||
sudo -u www-data tar xvf calibre-5.29.0-x86_64.txz -C /home/liberama/data/calibre
|
||||
```
|
||||
|
||||
### external converters
|
||||
```
|
||||
sudo apt install rar
|
||||
sudo apt install libreoffice
|
||||
sudo apt install poppler-utils
|
||||
sudo apt install djvulibre-bin
|
||||
sudo apt install libtiff-tools
|
||||
sudo apt install graphicsmagick-imagemagick-compat
|
||||
sudo apt install rar libreoffice poppler-utils djvulibre-bin libtiff-tools graphicsmagick-imagemagick-compat
|
||||
```
|
||||
|
||||
### nginx, server config
|
||||
Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader.
|
||||
Можно также настроить сервер для HTTP, без SSL.
|
||||
#### Для своего домена необходимо будет подправить docs/omnireader.ru/omnireader и docs/omnireader.ru/omnireader_http
|
||||
Сначала настроим для HTTP:
|
||||
```
|
||||
sudo apt install nginx
|
||||
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
|
||||
sudo cp docs/omnireader.ru/omnireader_http /etc/nginx/sites-available/omnireader
|
||||
sudo ln -s /etc/nginx/sites-available/omnireader /etc/nginx/sites-enabled/omnireader
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
sudo service nginx reload
|
||||
@@ -55,11 +52,19 @@ sudo chown -R www-data.www-data /var/www
|
||||
```
|
||||
|
||||
### certbot
|
||||
Следовать инструкции установки certbot https://certbot.eff.org/lets-encrypt/ubuntubionic-nginx
|
||||
### old.omnireader
|
||||
#### Следовать инструкции установки certbot https://certbot.eff.org/instructions?ws=nginx&os=ubuntu-20
|
||||
После установки сертификата, можно использовать конфиг для nginx c ssl:
|
||||
```
|
||||
sudo apt install php7.2 php7.2-curl php7.2-mbstring php7.2-fpm
|
||||
sudo service php7.2-fpm restart
|
||||
sudo cp docs/omnireader.ru/omnireader /etc/nginx/sites-available/omnireader
|
||||
sudo service nginx reload
|
||||
|
||||
```
|
||||
|
||||
### old.omnireader
|
||||
#### Старая версия omnireader на базе PHP, можно не устанавливать
|
||||
```
|
||||
sudo apt install php7.4 php7.4-curl php7.4-mbstring php7.4-fpm
|
||||
sudo service php7.4-fpm restart
|
||||
|
||||
sudo mkdir /home/oldreader
|
||||
sudo chown www-data.www-data /home/oldreader
|
||||
@@ -68,7 +73,7 @@ sudo -u www-data cp -r docs/omnireader.ru/old/* /home/oldreader
|
||||
|
||||
## Запуск по крону
|
||||
```
|
||||
* * * * * /root/liberama/docs/omnireader.ru/cron_server.sh
|
||||
* * * * * ~/liberama/docs/omnireader.ru/cron_server.sh >>~/liberama_cron.log 2>&1
|
||||
```
|
||||
|
||||
## Деплой и запуск
|
||||
@@ -78,8 +83,8 @@ cd docs/omnireader.ru
|
||||
./deploy.sh
|
||||
./start_server.sh
|
||||
```
|
||||
|
||||
После первого запуска будет создан конфигурационный файл `/home/liberama/data/config.json`.
|
||||
|
||||
Необходимо переключить приложение в режим `omnireader`, отредактировав опцию `servers`:
|
||||
```
|
||||
"servers": [
|
||||
@@ -91,4 +96,8 @@ cd docs/omnireader.ru
|
||||
}
|
||||
]
|
||||
```
|
||||
и перезапустить сервер
|
||||
Для использования установленных внешних конвертеров можно также поправить:
|
||||
```
|
||||
"useExternalBookConverter": true,
|
||||
```
|
||||
и перезапустить сервер.
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if ! pgrep -x "liberama" > /dev/null ; then
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null"
|
||||
else
|
||||
echo "Process 'liberama' already running"
|
||||
fi
|
||||
|
||||
@@ -6,8 +6,9 @@ server {
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
server_name omnireader.ru;
|
||||
set $liberama http://127.0.0.1:44081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
client_max_body_size 100m;
|
||||
proxy_read_timeout 1h;
|
||||
|
||||
gzip on;
|
||||
@@ -15,15 +16,20 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -32,6 +38,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -51,7 +62,7 @@ server {
|
||||
listen 80;
|
||||
server_name old.omnireader.ru;
|
||||
|
||||
client_max_body_size 50m;
|
||||
client_max_body_size 100m;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1024;
|
||||
@@ -66,6 +77,6 @@ server {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
include /etc/nginx/fastcgi.conf;
|
||||
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
|
||||
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name omnireader.ru;
|
||||
set $liberama http://127.0.0.1:44081;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
@@ -10,12 +11,16 @@ server {
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location @liberama {
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:44081;
|
||||
proxy_pass $liberama;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
@@ -27,6 +32,11 @@ server {
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
try_files $uri @liberama;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
@@ -54,6 +64,6 @@ server {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
include /etc/nginx/fastcgi.conf;
|
||||
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
|
||||
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
|
||||
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama >/dev/null & disown"
|
||||
sudo service cron start
|
||||
|
||||
29003
package-lock.json
generated
29003
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
123
package.json
123
package.json
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.10.1",
|
||||
"version": "0.11.8",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=14.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||
"build:linux": "npm run build:client && node build/linux && pkg -t node12-linux-x64 -o dist/linux/liberama .",
|
||||
"build:win": "npm run build:client && node build/win && pkg -t node12-win-x64 -o dist/win/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 -C GZip -o dist/win/liberama .",
|
||||
"lint": "eslint --ext=.js,.vue client server",
|
||||
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
||||
"postinstall": "npm run build:client-dev && node build/linux"
|
||||
@@ -21,72 +21,67 @@
|
||||
"scripts": "server/config/*.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"clean-webpack-plugin": "^1.0.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^1.0.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-html": "^5.0.5",
|
||||
"eslint-plugin-node": "^8.0.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"file-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"pkg": "^4.4.4",
|
||||
"terser-webpack-plugin": "^1.4.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"vue-class-component": "^6.3.2",
|
||||
"vue-loader": "^15.9.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-middleware": "^3.7.2",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^4.2.2",
|
||||
"workbox-webpack-plugin": "^5.1.3"
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
"@babel/eslint-plugin": "^7.14.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.16.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@vue/compiler-sfc": "^3.2.22",
|
||||
"babel-loader": "^8.2.3",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-vue": "^9.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"mini-css-extract-plugin": "^2.4.4",
|
||||
"pkg": "^5.5.1",
|
||||
"terser-webpack-plugin": "^5.2.5",
|
||||
"vue-eslint-parser": "^9.0.3",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"webpack": "^5.64.1",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-middleware": "^5.2.1",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"workbox-webpack-plugin": "^6.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.5.2",
|
||||
"axios": "^0.18.1",
|
||||
"base-x": "^3.0.8",
|
||||
"chardet": "^0.7.0",
|
||||
"@quasar/extras": "^1.12.0",
|
||||
"@vue/compat": "^3.2.21",
|
||||
"axios": "^0.27.2",
|
||||
"base-x": "^4.0.0",
|
||||
"chardet": "^1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.17.1",
|
||||
"fg-loadcss": "^2.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"got": "^9.6.0",
|
||||
"fg-loadcss": "^3.1.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"he": "^1.2.0",
|
||||
"iconv-lite": "^0.4.24",
|
||||
"localforage": "^1.7.3",
|
||||
"lodash": "^4.17.15",
|
||||
"minimist": "^1.2.0",
|
||||
"multer": "^1.4.2",
|
||||
"pako": "^1.0.11",
|
||||
"path-browserify": "^1.0.0",
|
||||
"pidusage": "^2.0.21",
|
||||
"quasar": "^1.14.3",
|
||||
"safe-buffer": "^5.2.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"jembadb": "^3.0.8",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pako": "^2.0.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pidusage": "^3.0.0",
|
||||
"quasar": "^2.7.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"sanitize-html": "^2.5.3",
|
||||
"sjcl": "^1.0.8",
|
||||
"sql-template-strings": "^2.2.2",
|
||||
"sqlite": "^3.0.3",
|
||||
"tar-fs": "^2.0.0",
|
||||
"unbzip2-stream": "^1.3.3",
|
||||
"vue": "github:bookpauk/vue",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-sanitize": "^0.2.1",
|
||||
"vuex": "^3.1.2",
|
||||
"vuex-persistedstate": "^2.7.1",
|
||||
"webdav": "^2.10.2",
|
||||
"ws": "^7.2.1",
|
||||
"zip-stream": "^2.1.3"
|
||||
"sqlite": "^4.0.23",
|
||||
"sqlite3": "^5.0.2",
|
||||
"tar-fs": "^2.1.1",
|
||||
"unbzip2-stream": "^1.4.3",
|
||||
"vue": "^3.2.37",
|
||||
"vue-router": "^4.1.1",
|
||||
"vuex": "^4.0.2",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
"webdav": "^4.7.0",
|
||||
"ws": "^8.2.3",
|
||||
"zip-stream": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ module.exports = {
|
||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||
|
||||
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: [
|
||||
{
|
||||
@@ -37,17 +38,25 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
|
||||
jembaDb: [
|
||||
{
|
||||
dbName: 'reader-storage',
|
||||
thread: true,
|
||||
openAll: true,
|
||||
}
|
||||
],
|
||||
|
||||
servers: [
|
||||
{
|
||||
serverName: '1',
|
||||
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader'
|
||||
mode: 'normal', //'none', 'normal', 'site', 'reader', 'omnireader', 'liberama.top', 'book_update_checker'
|
||||
ip: '0.0.0.0',
|
||||
port: '33080',
|
||||
},
|
||||
],
|
||||
|
||||
remoteWebDavStorage: false,
|
||||
/*
|
||||
remoteWebDavStorage: false,
|
||||
remoteWebDavStorage: {
|
||||
url: '127.0.0.1:1900',
|
||||
username: '',
|
||||
@@ -55,5 +64,12 @@ module.exports = {
|
||||
},
|
||||
*/
|
||||
|
||||
remoteStorage: false,
|
||||
/*
|
||||
remoteStorage: {
|
||||
url: 'https://127.0.0.1:11900',
|
||||
accessToken: '',
|
||||
},
|
||||
*/
|
||||
};
|
||||
|
||||
|
||||
95
server/controllers/BookUpdateCheckerController.js
Normal file
95
server/controllers/BookUpdateCheckerController.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const WebSocket = require ('ws');
|
||||
//const _ = require('lodash');
|
||||
|
||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||
//const utils = require('../core/utils');
|
||||
|
||||
const cleanPeriod = 1*60*1000;//1 минута
|
||||
const closeSocketOnIdle = 5*60*1000;//5 минут
|
||||
|
||||
class BookUpdateCheckerController {
|
||||
constructor(wss, config) {
|
||||
this.config = config;
|
||||
this.isDevelopment = (config.branch == 'development');
|
||||
|
||||
//this.readerStorage = new JembaReaderStorage();
|
||||
|
||||
this.wss = wss;
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', (message) => {
|
||||
this.onMessage(ws, message.toString());
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
log(LM_ERR, err);
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
}
|
||||
|
||||
periodicClean() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
this.wss.clients.forEach((ws) => {
|
||||
if (!ws.lastActivity || now - ws.lastActivity > closeSocketOnIdle - 50) {
|
||||
ws.terminate();
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
async onMessage(ws, message) {
|
||||
let req = {};
|
||||
try {
|
||||
if (this.isDevelopment) {
|
||||
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||
}
|
||||
|
||||
req = JSON.parse(message);
|
||||
|
||||
ws.lastActivity = Date.now();
|
||||
|
||||
//pong for WebSocketConnection
|
||||
this.send({_rok: 1}, req, ws);
|
||||
|
||||
switch (req.action) {
|
||||
case 'test':
|
||||
await this.test(req, ws); break;
|
||||
|
||||
default:
|
||||
throw new Error(`Action not found: ${req.action}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.send({error: e.message}, req, ws);
|
||||
}
|
||||
}
|
||||
|
||||
send(res, req, ws) {
|
||||
if (ws.readyState == WebSocket.OPEN) {
|
||||
ws.lastActivity = Date.now();
|
||||
let r = res;
|
||||
if (req.requestId)
|
||||
r = Object.assign({requestId: req.requestId}, r);
|
||||
|
||||
const message = JSON.stringify(r);
|
||||
ws.send(message);
|
||||
|
||||
if (this.isDevelopment) {
|
||||
log(`WebSocket-OUT: ${message.substr(0, 4000)}`);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//Actions ------------------------------------------------------------------
|
||||
async test(req, ws) {
|
||||
this.send({message: 'Liberama project is awesome'}, req, ws);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = BookUpdateCheckerController;
|
||||
@@ -1,12 +1,12 @@
|
||||
const BaseController = require('./BaseController');
|
||||
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
|
||||
|
||||
class ReaderController extends BaseController {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
this.readerStorage = new ReaderStorage();
|
||||
this.readerStorage = new JembaReaderStorage();
|
||||
this.readerWorker = new ReaderWorker(config);
|
||||
this.workerState = new WorkerState();
|
||||
}
|
||||
@@ -68,24 +68,6 @@ class ReaderController extends BaseController {
|
||||
res.status(400).send({error});
|
||||
return false;
|
||||
}
|
||||
|
||||
async restoreCachedFile(req, res) {
|
||||
const request = req.body;
|
||||
let error = '';
|
||||
try {
|
||||
if (!request.path)
|
||||
throw new Error(`key 'path' is empty`);
|
||||
|
||||
const workerId = this.readerWorker.restoreCachedFile(request.path);
|
||||
const state = this.workerState.getState(workerId);
|
||||
return (state ? state : {});
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
//bad request
|
||||
res.status(400).send({error});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ReaderController;
|
||||
|
||||
@@ -2,7 +2,7 @@ const WebSocket = require ('ws');
|
||||
const _ = require('lodash');
|
||||
|
||||
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 log = new (require('../core/AppLogger'))().log;//singleton
|
||||
const utils = require('../core/utils');
|
||||
@@ -15,7 +15,7 @@ class WebSocketController {
|
||||
this.config = config;
|
||||
this.isDevelopment = (config.branch == 'development');
|
||||
|
||||
this.readerStorage = new ReaderStorage();
|
||||
this.readerStorage = new JembaReaderStorage();
|
||||
this.readerWorker = new ReaderWorker(config);
|
||||
this.workerState = new WorkerState();
|
||||
|
||||
@@ -23,7 +23,11 @@ class WebSocketController {
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', (message) => {
|
||||
this.onMessage(ws, message);
|
||||
this.onMessage(ws, message.toString());
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
log(LM_ERR, err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,8 +59,7 @@ class WebSocketController {
|
||||
ws.lastActivity = Date.now();
|
||||
|
||||
//pong for WebSocketConnection
|
||||
if (req._rpo === 1)
|
||||
this.send({_rok: 1}, req, ws);
|
||||
this.send({_rok: 1}, req, ws);
|
||||
|
||||
switch (req.action) {
|
||||
case 'test':
|
||||
@@ -67,10 +70,12 @@ class WebSocketController {
|
||||
await this.workerGetState(req, ws); break;
|
||||
case 'worker-get-state-finish':
|
||||
await this.workerGetStateFinish(req, ws); break;
|
||||
case 'reader-restore-cached-file':
|
||||
await this.readerRestoreCachedFile(req, ws); break;
|
||||
case 'reader-storage':
|
||||
await this.readerStorageDo(req, ws); break;
|
||||
case 'upload-file-buf':
|
||||
await this.uploadFileBuf(req, ws); break;
|
||||
case 'upload-file-touch':
|
||||
await this.uploadFileTouch(req, ws); break;
|
||||
|
||||
default:
|
||||
throw new Error(`Action not found: ${req.action}`);
|
||||
@@ -150,15 +155,6 @@ class WebSocketController {
|
||||
}
|
||||
}
|
||||
|
||||
async readerRestoreCachedFile(req, ws) {
|
||||
if (!req.path)
|
||||
throw new Error(`key 'path' is empty`);
|
||||
|
||||
const workerId = this.readerWorker.restoreCachedFile(req.path);
|
||||
const state = this.workerState.getState(workerId);
|
||||
this.send((state ? state : {}), req, ws);
|
||||
}
|
||||
|
||||
async readerStorageDo(req, ws) {
|
||||
if (!req.body)
|
||||
throw new Error(`key 'body' is empty`);
|
||||
@@ -169,6 +165,20 @@ class WebSocketController {
|
||||
|
||||
this.send(await this.readerStorage.doAction(req.body), req, ws);
|
||||
}
|
||||
|
||||
async uploadFileBuf(req, ws) {
|
||||
if (!req.buf)
|
||||
throw new Error(`key 'buf' is empty`);
|
||||
|
||||
this.send({url: await this.readerWorker.saveFileBuf(req.buf)}, req, ws);
|
||||
}
|
||||
|
||||
async uploadFileTouch(req, ws) {
|
||||
if (!req.url)
|
||||
throw new Error(`key 'url' is empty`);
|
||||
|
||||
this.send({url: await this.readerWorker.uploadFileTouch(req.url)}, req, ws);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebSocketController;
|
||||
|
||||
@@ -25,60 +25,7 @@ class WorkerController extends BaseController {
|
||||
res.status(400).send({error});
|
||||
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;
|
||||
|
||||
@@ -3,4 +3,5 @@ module.exports = {
|
||||
ReaderController: require('./ReaderController'),
|
||||
WorkerController: require('./WorkerController'),
|
||||
WebSocketController: require('./WebSocketController'),
|
||||
BookUpdateCheckerController: require('./BookUpdateCheckerController'),
|
||||
}
|
||||
@@ -7,10 +7,14 @@ let instance = null;
|
||||
class AppLogger {
|
||||
constructor() {
|
||||
if (!instance) {
|
||||
this.inited = false;
|
||||
this.logFileName = '';
|
||||
this.errLogFileName = '';
|
||||
this.fatalLogFileName = '';
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
this.inited = false;
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -22,11 +26,16 @@ class AppLogger {
|
||||
|
||||
if (config.loggingEnabled) {
|
||||
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 = [
|
||||
{log: 'ConsoleLog'},
|
||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.log`},
|
||||
{log: 'FileLog', fileName: `${config.logDir}/${config.name}.err.log`, 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.logFileName},
|
||||
{log: 'FileLog', fileName: this.errLogFileName, exclude: [LM_OK, LM_INFO, LM_TOTAL]},
|
||||
{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;
|
||||
0
server/core/BookUpdateChecker/BUCClient.js
Normal file
0
server/core/BookUpdateChecker/BUCClient.js
Normal file
24
server/core/BookUpdateChecker/BUCServer.js
Normal file
24
server/core/BookUpdateChecker/BUCServer.js
Normal file
@@ -0,0 +1,24 @@
|
||||
let instance = null;
|
||||
|
||||
//singleton
|
||||
class BUCServer {
|
||||
constructor(config) {
|
||||
if (!instance) {
|
||||
this.config = Object.assign({}, config);
|
||||
|
||||
this.config.tempDownloadDir = `${config.tempDir}/download`;
|
||||
fs.ensureDirSync(this.config.tempDownloadDir);
|
||||
|
||||
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||
|
||||
instance = this;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async main() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BUCServer;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user