Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c6e534b9db | ||
|
|
032ab6a85d | ||
|
|
830c066ebf | ||
|
|
c432388515 | ||
|
|
476deba93a | ||
|
|
ffb4f2386d | ||
|
|
21716163cb | ||
|
|
ca924148a5 | ||
|
|
37aa9b84ae | ||
|
|
c7bd7d4d7d | ||
|
|
d81a50e696 | ||
|
|
dda9943dbe | ||
|
|
2b4b9f24a1 | ||
|
|
2af77f22d6 | ||
|
|
f142e5812d | ||
|
|
ed901fc181 | ||
|
|
87a068899a | ||
|
|
115f683128 | ||
|
|
111568fc2e | ||
|
|
825136b5ff | ||
|
|
eae34b1121 | ||
|
|
b9d7a6a3bb | ||
|
|
1e5375f8f9 | ||
|
|
f597c603bf | ||
|
|
b93dd0a59e | ||
|
|
a5740e4349 | ||
|
|
dacbd05911 | ||
|
|
65c66e0feb | ||
|
|
52f9131f99 | ||
|
|
cfc946ad12 | ||
|
|
a207a0554c | ||
|
|
675e898163 | ||
|
|
d2167d8605 | ||
|
|
de849d3447 | ||
|
|
6c20b0b83e | ||
|
|
a09b70a991 | ||
|
|
2427a3e08b | ||
|
|
1104f9b850 | ||
|
|
dc48700e9e | ||
|
|
f0b0c39328 | ||
|
|
aad74cf682 | ||
|
|
d449478204 | ||
|
|
d4f6536caa | ||
|
|
1eac00f71c | ||
|
|
ca1170a9f0 | ||
|
|
79dda03bac | ||
|
|
6c8e0b8573 | ||
|
|
17c14722fd | ||
|
|
48612ee118 | ||
|
|
205c676999 | ||
|
|
54e0dd0478 | ||
|
|
2de8d7515e | ||
|
|
a251d16432 | ||
|
|
599caba912 | ||
|
|
3477c43465 | ||
|
|
200dac7946 | ||
|
|
e60829946d | ||
|
|
ef12a84285 | ||
|
|
6a18ae3f27 | ||
|
|
a250e95950 | ||
|
|
b174ae452b | ||
|
|
0b63bce357 | ||
|
|
de0d10e792 | ||
|
|
b358b340b4 | ||
|
|
455aba7f4f | ||
|
|
fde0437157 | ||
|
|
480c95bd63 | ||
|
|
972f957685 | ||
|
|
40ff04e5dc | ||
|
|
b3c028bd7a | ||
|
|
51ec6a54fa | ||
|
|
7a29b16ee8 | ||
|
|
7af6fd8248 | ||
|
|
e1c93169b5 | ||
|
|
f4716d5a1e | ||
|
|
f5c06ce420 | ||
|
|
9492f85d80 |
6
.babelrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"presets": [['@babel/preset-env', { "targets": { "esmodules": true } }]],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }]
|
||||
]
|
||||
}
|
||||
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
|
||||
|
||||
@@ -24,8 +24,8 @@ 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 не включает его в сборку
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -24,8 +24,8 @@ 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 не включает его в сборку
|
||||
|
||||
@@ -13,8 +13,7 @@ class Misc {
|
||||
]};
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||
if (config.error)
|
||||
throw new Error(config.error);
|
||||
return config;
|
||||
|
||||
@@ -19,8 +19,7 @@ class Reader {
|
||||
|
||||
let response = {};
|
||||
try {
|
||||
await wsc.open();
|
||||
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
||||
const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
|
||||
|
||||
let prevResponse = false;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
@@ -124,8 +123,7 @@ class Reader {
|
||||
let response = null
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
@@ -210,8 +208,7 @@ class Reader {
|
||||
async storage(request) {
|
||||
let response = null;
|
||||
try {
|
||||
await wsc.open();
|
||||
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
|
||||
response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//если с WebSocket проблема, работаем по http
|
||||
@@ -222,7 +219,7 @@ class Reader {
|
||||
const state = response.state;
|
||||
if (!state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (response.state == 'error') {
|
||||
if (state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,185 +1,3 @@
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
const cleanPeriod = 60*1000;//1 минута
|
||||
|
||||
class WebSocketConnection {
|
||||
//messageLifeTime в минутах (cleanPeriod)
|
||||
constructor(messageLifeTime = 5) {
|
||||
this.ws = null;
|
||||
this.timer = null;
|
||||
this.listeners = [];
|
||||
this.messageQueue = [];
|
||||
this.messageLifeTime = messageLifeTime;
|
||||
this.requestId = 0;
|
||||
|
||||
this.connecting = false;
|
||||
}
|
||||
|
||||
addListener(listener) {
|
||||
if (this.listeners.indexOf(listener) < 0)
|
||||
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
|
||||
}
|
||||
|
||||
//рассылаем сообщение и удаляем те обработчики, которые его получили
|
||||
emit(mes, isError) {
|
||||
const len = this.listeners.length;
|
||||
if (len > 0) {
|
||||
let newListeners = [];
|
||||
for (const listener of this.listeners) {
|
||||
let emitted = false;
|
||||
if (isError) {
|
||||
if (listener.onError)
|
||||
listener.onError(mes);
|
||||
emitted = true;
|
||||
} else {
|
||||
if (listener.onMessage) {
|
||||
if (listener.requestId) {
|
||||
if (listener.requestId === mes.requestId) {
|
||||
listener.onMessage(mes);
|
||||
emitted = true;
|
||||
}
|
||||
} else {
|
||||
listener.onMessage(mes);
|
||||
emitted = true;
|
||||
}
|
||||
} else {
|
||||
emitted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!emitted)
|
||||
newListeners.push(listener);
|
||||
}
|
||||
this.listeners = newListeners;
|
||||
}
|
||||
|
||||
return this.listeners.length != len;
|
||||
}
|
||||
|
||||
open(url) {
|
||||
return new Promise((resolve, reject) => { (async() => {
|
||||
//Ожидаем окончания процесса подключения, если open уже был вызван
|
||||
let i = 0;
|
||||
while (this.connecting && i < 200) {//10 сек
|
||||
await utils.sleep(50);
|
||||
i++;
|
||||
}
|
||||
if (i >= 200)
|
||||
this.connecting = false;
|
||||
|
||||
//проверим подключение, и если нет, то подключимся заново
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
resolve(this.ws);
|
||||
} else {
|
||||
this.connecting = true;
|
||||
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
|
||||
|
||||
url = url || `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
|
||||
this.ws.onopen = (e) => {
|
||||
this.connecting = false;
|
||||
resolve(e);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (e) => {
|
||||
try {
|
||||
const mes = JSON.parse(e.data);
|
||||
this.messageQueue.push({regTime: Date.now(), mes});
|
||||
|
||||
let newMessageQueue = [];
|
||||
for (const message of this.messageQueue) {
|
||||
if (!this.emit(message.mes)) {
|
||||
newMessageQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
this.messageQueue = newMessageQueue;
|
||||
} catch (e) {
|
||||
this.emit(e.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (e) => {
|
||||
this.emit(e.message, true);
|
||||
if (this.connecting) {
|
||||
this.connecting = false;
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
})() });
|
||||
}
|
||||
|
||||
//timeout в минутах (cleanPeriod)
|
||||
message(requestId, timeout = 2) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.addListener({
|
||||
requestId,
|
||||
timeout,
|
||||
onMessage: (mes) => {
|
||||
resolve(mes);
|
||||
},
|
||||
onError: (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
send(req) {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
const requestId = ++this.requestId;
|
||||
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
||||
return requestId;
|
||||
} else {
|
||||
throw new Error('WebSocket connection is not ready');
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
periodicClean() {
|
||||
try {
|
||||
this.timer = null;
|
||||
|
||||
const now = Date.now();
|
||||
//чистка listeners
|
||||
let newListeners = [];
|
||||
for (const listener of this.listeners) {
|
||||
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
|
||||
newListeners.push(listener);
|
||||
} else {
|
||||
if (listener.onError)
|
||||
listener.onError('Время ожидания ответа истекло');
|
||||
}
|
||||
}
|
||||
this.listeners = newListeners;
|
||||
|
||||
//чистка messageQueue
|
||||
let newMessageQueue = [];
|
||||
for (const message of this.messageQueue) {
|
||||
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
|
||||
newMessageQueue.push(message);
|
||||
}
|
||||
}
|
||||
this.messageQueue = newMessageQueue;
|
||||
} finally {
|
||||
if (this.ws.readyState == WebSocket.OPEN) {
|
||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import WebSocketConnection from '../../server/core/WebSocketConnection';
|
||||
|
||||
export default new WebSocketConnection();
|
||||
|
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>
|
||||
|
||||
@@ -270,6 +280,14 @@ body, html, #app {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
} to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.notify-button-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@@ -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" rounded 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">
|
||||
<template #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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
>
|
||||
<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>
|
||||
<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" @click.stop="bookmarkSettings" size="12px">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Настроить закладки</q-tooltip>
|
||||
<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 v-slot:selected>
|
||||
<div style="overflow: hidden; white-space: nowrap;">{{ rootLinkWithoutProtocol }}</div>
|
||||
<template #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"
|
||||
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
|
||||
<q-select
|
||||
ref="selectedLink"
|
||||
v-model="selectedLink"
|
||||
class="q-mr-sm"
|
||||
:options="selectedLinkOptions"
|
||||
style="width: 50px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
rounded 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-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Закладки
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
|
||||
<q-input class="col q-mr-sm" ref="input" rounded outlined dense bg-color="white" v-model="bookUrl" placeholder="Скопируйте сюда URL книги"
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="bookUrl"
|
||||
class="col q-mr-sm"
|
||||
rounded 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" rounded 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>
|
||||
|
||||
|
||||
@@ -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,124 +1,139 @@
|
||||
<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'" class="tab-panel">
|
||||
<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>
|
||||
<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-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="`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>
|
||||
</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 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'" class="tab-panel">
|
||||
<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>
|
||||
</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();
|
||||
}
|
||||
},
|
||||
})
|
||||
class ContentsPage extends ContentsPageProps {
|
||||
};
|
||||
class ContentsPage {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
bookPos: Number,
|
||||
isVisible: Boolean,
|
||||
};
|
||||
|
||||
selectedTab = 'contents';
|
||||
contents = [];
|
||||
images = [];
|
||||
@@ -145,25 +160,36 @@ class ContentsPage extends ContentsPageProps {
|
||||
await this.$nextTick();
|
||||
|
||||
const pc = parsed.contents;
|
||||
const newpc = [];
|
||||
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||
let curSubtitles = [];
|
||||
let prevBodyIndex = -1;
|
||||
for (let i = 0; i < pc.length; i++) {
|
||||
const cont = pc[i];
|
||||
if (prevBodyIndex != cont.bodyIndex)
|
||||
curSubtitles = [];
|
||||
const ims = parsed.images;
|
||||
const newpc = [];
|
||||
if (pc.length) {//если есть оглавление
|
||||
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||
let curSubtitles = [];
|
||||
let prevBodyIndex = -1;
|
||||
for (let i = 0; i < pc.length; i++) {
|
||||
const cont = pc[i];
|
||||
if (prevBodyIndex != cont.bodyIndex)
|
||||
curSubtitles = [];
|
||||
|
||||
prevBodyIndex = cont.bodyIndex;
|
||||
prevBodyIndex = cont.bodyIndex;
|
||||
|
||||
if (cont.bodyIndex > 1) {
|
||||
if (cont.inset < 1) {
|
||||
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||
if (cont.bodyIndex > 1) {
|
||||
if (cont.inset < 1) {
|
||||
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||
} else {
|
||||
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||
}
|
||||
} else {
|
||||
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||
newpc.push(cont);
|
||||
}
|
||||
}
|
||||
} else {//попробуем вытащить из images
|
||||
for (let i = 0; i < ims.length; i++) {
|
||||
const image = ims[i];
|
||||
|
||||
if (image.alt) {
|
||||
newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
|
||||
}
|
||||
} else {
|
||||
newpc.push(cont);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,19 +238,18 @@ class ContentsPage extends ContentsPageProps {
|
||||
|
||||
//формируем newImages
|
||||
const newImages = [];
|
||||
const ims = parsed.images;
|
||||
for (i = 0; i < ims.length; i++) {
|
||||
const image = ims[i];
|
||||
const bin = parsed.binary[image.id];
|
||||
const type = (bin ? bin.type : '');
|
||||
|
||||
const label = `Изображение ${image.num}`;
|
||||
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
|
||||
const indentStyle = getIndentStyle(1);
|
||||
const labelStyle = getLabelStyle(0);
|
||||
const labelStyle = getLabelStyle(1);
|
||||
|
||||
const p = parsed.para[image.paraIndex];
|
||||
newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
|
||||
indentStyle, labelStyle, type, id: image.id, local: image.local});
|
||||
indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
|
||||
}
|
||||
|
||||
this.images = newImages;
|
||||
@@ -244,9 +269,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);
|
||||
}
|
||||
@@ -270,17 +295,17 @@ class ContentsPage extends ContentsPageProps {
|
||||
|
||||
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||
subitem.isBookPos = true;
|
||||
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||
this.contents[i] = Object.assign(item, {list: item.list});
|
||||
} else if (subitem.isBookPos) {
|
||||
subitem.isBookPos = false;
|
||||
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||
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}));
|
||||
this.contents[i] = Object.assign(item, {isBookPos: true});
|
||||
} else if (item.isBookPos) {
|
||||
this.$set(this.contents, i, Object.assign(item, {isBookPos: false}));
|
||||
this.contents[i] = Object.assign(item, {isBookPos: false});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,9 +314,9 @@ 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] = Object.assign(img, {isBookPos: true});
|
||||
} else if (img.isBookPos) {
|
||||
this.$set(this.images, i, Object.assign(img, {isBookPos: false}));
|
||||
this.images[i] = Object.assign(img, {isBookPos: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,16 +326,16 @@ class ContentsPage extends ContentsPageProps {
|
||||
const expanded = !item.expanded;
|
||||
|
||||
if (!expanded) {
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
const subitems = this.$refs[`subitem${key}`];
|
||||
subitems.style.height = '0';
|
||||
await utils.sleep(200);
|
||||
}
|
||||
|
||||
this.$set(this.contents, key, Object.assign({}, item, {expanded}));
|
||||
this.contents[key] = Object.assign({}, item, {expanded});
|
||||
|
||||
if (expanded) {
|
||||
await this.$nextTick();
|
||||
const subitems = this.$refs[`subitem${key}`][0];
|
||||
const subitems = this.$refs[`subitem${key}`];
|
||||
subitems.style.height = subitems.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
@@ -331,6 +356,8 @@ class ContentsPage extends ContentsPageProps {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ContentsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -389,6 +416,10 @@ class ContentsPage extends ContentsPageProps {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.image-num {
|
||||
font-size: 120%;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.image-type {
|
||||
border: 1px solid black;
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -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/yandex.png">
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
|
||||
<div class="para">{{ yandexAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
<img class="logo" src="./assets/yoomoney.png">
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">
|
||||
Пожертвовать
|
||||
</q-btn><br>
|
||||
<div class="para">
|
||||
{{ yooAddress }}
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">
|
||||
Скопировать
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<!--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,14 +72,12 @@
|
||||
|
||||
<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 {
|
||||
yandexAddress = '410018702323056';
|
||||
class DonateHelpPage {
|
||||
yooAddress = '410018702323056';
|
||||
paypalAddress = 'bookpauk@gmail.com';
|
||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||
@@ -69,8 +86,8 @@ class DonateHelpPage extends Vue {
|
||||
created() {
|
||||
}
|
||||
|
||||
donateYandexMoney() {
|
||||
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank');
|
||||
donateYooMoney() {
|
||||
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
|
||||
}
|
||||
|
||||
async copyAddress(address, prefix) {
|
||||
@@ -81,6 +98,8 @@ class DonateHelpPage extends Vue {
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(DonateHelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Справка
|
||||
</template>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
<div class="separator"></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 +22,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 +44,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 +73,7 @@ class HelpPage extends Vue {
|
||||
}
|
||||
|
||||
activateDonateHelpPage() {
|
||||
this.selectedTab = 'DonateHelpPage';
|
||||
//this.selectedTab = 'DonateHelpPage';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
@@ -87,6 +87,8 @@ class HelpPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(HelpPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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,26 @@
|
||||
</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;" @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 +50,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;
|
||||
@@ -112,9 +136,7 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
@@ -136,7 +158,7 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
loadBufferClick() {
|
||||
this.pasteTextToggle();
|
||||
this.showPasteText();
|
||||
}
|
||||
|
||||
loadBuffer(opts) {
|
||||
@@ -146,6 +168,10 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
showPasteText() {
|
||||
this.pasteTextActive = true;
|
||||
}
|
||||
|
||||
pasteTextToggle() {
|
||||
this.pasteTextActive = !this.pasteTextActive;
|
||||
}
|
||||
@@ -158,6 +184,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 +196,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,16 +60,20 @@ 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.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
|
||||
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>
|
||||
|
||||
|
||||
@@ -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="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
|
||||
<q-icon name="la la-retweet" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</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,7 +133,10 @@
|
||||
|
||||
<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"
|
||||
@@ -86,30 +145,32 @@
|
||||
</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,12 +186,15 @@ 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 dynamicCss from '../../share/dynamicCss';
|
||||
|
||||
import rstore from '../../store/modules/reader';
|
||||
import readerApi from '../../api/reader';
|
||||
import miscApi from '../../api/misc';
|
||||
@@ -138,7 +202,7 @@ import miscApi from '../../api/misc';
|
||||
import {versionHistory} from './versionHistory';
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
LoaderPage,
|
||||
TextPage,
|
||||
@@ -152,9 +216,9 @@ export default @Component({
|
||||
SettingsPage,
|
||||
HelpPage,
|
||||
ClickMapPage,
|
||||
ServerStorage,
|
||||
ContentsPage,
|
||||
|
||||
ServerStorage,
|
||||
ReaderDialogs,
|
||||
},
|
||||
watch: {
|
||||
@@ -194,25 +258,38 @@ export default @Component({
|
||||
}
|
||||
})();
|
||||
},
|
||||
dualPageMode(newValue) {
|
||||
if (newValue)
|
||||
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;
|
||||
@@ -227,6 +304,7 @@ class Reader extends Vue {
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
dualPageMode = false;
|
||||
|
||||
created() {
|
||||
this.rstore = rstore;
|
||||
@@ -235,10 +313,19 @@ class Reader extends Vue {
|
||||
this.reader = this.$store.state.reader;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
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)) {
|
||||
@@ -252,7 +339,7 @@ class Reader extends Vue {
|
||||
await this.$nextTick();
|
||||
this.paramPosIgnore = false;
|
||||
}
|
||||
}, 500, {maxWait: 5000});
|
||||
}, 250, {maxWait: 5000});
|
||||
|
||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||
this.debouncedSetRecentBook(newValue);
|
||||
@@ -266,13 +353,12 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.updateHeaderMinWidth();
|
||||
|
||||
(async() => {
|
||||
await wallpaperStorage.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 {
|
||||
@@ -313,17 +399,63 @@ 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.enableSitesFilter = settings.enableSitesFilter;
|
||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||
this.splitToPara = settings.splitToPara;
|
||||
this.djvuQuality = settings.djvuQuality;
|
||||
this.pdfAsText = settings.pdfAsText;
|
||||
this.pdfQuality = settings.pdfQuality;
|
||||
this.dualPageMode = settings.dualPageMode;
|
||||
this.userWallpapers = settings.userWallpapers;
|
||||
|
||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||
this.$root.readerActionByKeyEvent = (event) => {
|
||||
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;
|
||||
|
||||
let newCss = '';
|
||||
for (const wp of this.userWallpapers) {
|
||||
const data = await wallpaperStorage.getData(wp.cssClass);
|
||||
|
||||
if (!data) {
|
||||
//здесь будем восстанавливать данные с сервера
|
||||
}
|
||||
|
||||
if (data) {
|
||||
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
||||
}
|
||||
}
|
||||
dynamicCss.replace('wallpapers', newCss);
|
||||
}
|
||||
}
|
||||
|
||||
async checkNewVersionAvailable() {
|
||||
@@ -336,7 +468,7 @@ class Reader extends Vue {
|
||||
|
||||
let againMes = '';
|
||||
if (this.isFirstNeedUpdateNotify) {
|
||||
againMes = ' ЕЩЕ один раз';
|
||||
againMes = ' еще один раз';
|
||||
}
|
||||
|
||||
if (this.version != this.clientVersion)
|
||||
@@ -345,20 +477,9 @@ class Reader extends Vue {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.checkingNewVersion = false;
|
||||
}
|
||||
}
|
||||
this.isFirstNeedUpdateNotify = false;
|
||||
}
|
||||
this.isFirstNeedUpdateNotify = false;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -436,9 +557,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
return versionHistory[0].version;
|
||||
}
|
||||
|
||||
get routeParamUrl() {
|
||||
@@ -496,7 +615,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,7 +659,7 @@ class Reader extends Vue {
|
||||
|
||||
toolBarToggle() {
|
||||
this.commit('reader/setToolBarActive', !this.toolBarActive);
|
||||
this.$root.$emit('resize');
|
||||
this.$root.eventHook('resize');
|
||||
}
|
||||
|
||||
fullScreenToggle() {
|
||||
@@ -557,6 +689,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;
|
||||
@@ -684,6 +838,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;
|
||||
@@ -703,6 +863,12 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
convOptionsToggle() {
|
||||
this.settingsToggle();
|
||||
if (this.settingsActive)
|
||||
this.$refs.settingsPage.selectedTab = 'convert';
|
||||
}
|
||||
|
||||
helpToggle() {
|
||||
this.helpActive = !this.helpActive;
|
||||
if (this.helpActive) {
|
||||
@@ -729,15 +895,9 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
refreshBook(mode) {
|
||||
refreshBook() {
|
||||
const mrb = this.mostRecentBook();
|
||||
if (mrb) {
|
||||
if (mode && mode == 'split') {
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, skipCheck: true, isText: true, force: true});
|
||||
} else {
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||
}
|
||||
}
|
||||
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||
}
|
||||
|
||||
undoAction() {
|
||||
@@ -772,16 +932,19 @@ class Reader extends Vue {
|
||||
|
||||
switch (action) {
|
||||
case 'loader':
|
||||
case 'loadFile':
|
||||
case 'loadBuffer':
|
||||
case 'help':
|
||||
case 'fullScreen':
|
||||
case 'setPosition':
|
||||
case 'scrolling':
|
||||
case 'search':
|
||||
case 'copyText':
|
||||
case 'splitToPara':
|
||||
case 'convOptions':
|
||||
case 'refresh':
|
||||
case 'contents':
|
||||
case 'libs':
|
||||
case 'recentBooks':
|
||||
case 'clickControl':
|
||||
case 'offlineMode':
|
||||
case 'settings':
|
||||
if (this.progressActive) {
|
||||
@@ -790,6 +953,13 @@ class Reader extends Vue {
|
||||
classResult = classActive;
|
||||
}
|
||||
break;
|
||||
case 'scrolling':
|
||||
if (this.progressActive || this.dualPageMode) {
|
||||
classResult = classDisabled;
|
||||
} else if (this[`${action}Active`]) {
|
||||
classResult = classActive;
|
||||
}
|
||||
break;
|
||||
case 'undoAction':
|
||||
if (this.actionCur <= 0)
|
||||
classResult = classDisabled;
|
||||
@@ -811,7 +981,6 @@ class Reader extends Vue {
|
||||
case 'contents':
|
||||
classResult = classDisabled;
|
||||
break;
|
||||
case 'splitToPara':
|
||||
case 'refresh':
|
||||
case 'recentBooks':
|
||||
if (!this.mostRecentBookReactive)
|
||||
@@ -832,7 +1001,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
let result = '';
|
||||
let result = undefined;
|
||||
|
||||
if (this.progressActive)
|
||||
result = 'ProgressPage';
|
||||
@@ -847,7 +1016,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
if (result != 'TextPage') {
|
||||
this.$root.$emit('set-app-title');
|
||||
this.$root.setAppTitle();
|
||||
}
|
||||
|
||||
// на LoaderPage всегда показываем toolBar
|
||||
@@ -862,7 +1031,7 @@ class Reader extends Vue {
|
||||
const isParsed = await bookManager.hasBookParsed(last);
|
||||
|
||||
if (!isParsed) {
|
||||
this.$root.$emit('set-app-title');
|
||||
this.$root.setAppTitle();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -973,10 +1142,13 @@ class Reader extends Vue {
|
||||
if (!book) {
|
||||
book = await readerApi.loadBook({
|
||||
url,
|
||||
skipCheck: (opts.skipCheck ? true : false),
|
||||
isText: (opts.isText ? true : false),
|
||||
uploadFileName,
|
||||
enableSitesFilter: this.enableSitesFilter,
|
||||
uploadFileName
|
||||
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||
isText: (this.splitToPara ? true : false),
|
||||
djvuQuality: this.djvuQuality,
|
||||
pdfAsText: this.pdfAsText,
|
||||
pdfQuality: this.pdfQuality,
|
||||
},
|
||||
(state) => {
|
||||
progress.setState(state);
|
||||
@@ -1009,7 +1181,9 @@ class Reader extends Vue {
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
if (!this.showHelpOnErrorIfNeeded(e.message)) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
} finally {
|
||||
this.checkNewVersionAvailable();
|
||||
}
|
||||
@@ -1075,6 +1249,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;
|
||||
@@ -1102,8 +1282,8 @@ class Reader extends Vue {
|
||||
case 'copyText':
|
||||
this.copyTextToggle();
|
||||
break;
|
||||
case 'splitToPara':
|
||||
this.refreshBook('split');
|
||||
case 'convOptions':
|
||||
this.convOptionsToggle();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
@@ -1117,6 +1297,9 @@ class Reader extends Vue {
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'clickControl':
|
||||
this.clickControlToggle();
|
||||
break;
|
||||
case 'offlineMode':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
@@ -1185,7 +1368,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;
|
||||
|
||||
@@ -1219,29 +1402,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,60 +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 rounded @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="liberamaTopVisible">
|
||||
<template slot="header">
|
||||
Здравствуйте, уважаемые читатели!
|
||||
<Dialog ref="dialog3" v-model="urlHelpVisible">
|
||||
<template #header>
|
||||
Обнаружена невалидная ссылка в поле "URL книги".
|
||||
<br>
|
||||
</template>
|
||||
|
||||
<div style="word-break: normal">
|
||||
Создан новый ресурс:<br><br>
|
||||
Если вы хотите найти определенную книгу и открыть в читалке, добро пожаловать в
|
||||
раздел "Сетевая библиотека" (кнопка <q-icon name="la la-sitemap" size="32px" />) на сайте
|
||||
<a href="https://liberama.top" target="_blank">liberama.top</a>
|
||||
|
||||
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
|
||||
<br><br>
|
||||
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
|
||||
|
||||
<ul>
|
||||
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
|
||||
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
|
||||
</ul>
|
||||
|
||||
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
|
||||
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
|
||||
Спасибо, что вы с нами!
|
||||
<br><br>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
</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>
|
||||
на странице загрузки.
|
||||
</div>
|
||||
|
||||
<span slot="footer">
|
||||
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
</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
|
||||
},
|
||||
@@ -109,12 +109,14 @@ export default @Component({
|
||||
this.loadSettings();
|
||||
},
|
||||
},
|
||||
})
|
||||
class ReaderDialogs extends Vue {
|
||||
};
|
||||
class ReaderDialogs {
|
||||
_options = componentOptions;
|
||||
|
||||
whatsNewVisible = false;
|
||||
whatsNewContent = '';
|
||||
donationVisible = false;
|
||||
liberamaTopVisible = false;
|
||||
urlHelpVisible = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -127,23 +129,21 @@ class ReaderDialogs extends Vue {
|
||||
async init() {
|
||||
await this.showWhatsNew();
|
||||
await this.showDonation();
|
||||
await this.showLiberamaTop();
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const settings = this.settings;
|
||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
|
||||
}
|
||||
|
||||
async showWhatsNew() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -157,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) {
|
||||
@@ -171,7 +180,6 @@ class ReaderDialogs extends Vue {
|
||||
|
||||
openDonate() {
|
||||
this.donationVisible = false;
|
||||
this.liberamaTopVisible = false;
|
||||
this.$emit('donate-toggle');
|
||||
}
|
||||
|
||||
@@ -190,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() {
|
||||
@@ -210,28 +221,14 @@ class ReaderDialogs extends Vue {
|
||||
return this.$store.state.reader.donationRemindDate;
|
||||
}
|
||||
|
||||
async showLiberamaTop() {
|
||||
const today = utils.formatDate(new Date(), 'coDate');
|
||||
|
||||
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
|
||||
await utils.sleep(3000);
|
||||
this.liberamaTopVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
liberamaTopDialogDisable() {
|
||||
this.liberamaTopVisible = false;
|
||||
if (this.showLiberamaTopDialog2020) {
|
||||
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
|
||||
}
|
||||
}
|
||||
|
||||
keyHook() {
|
||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ReaderDialogs);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
<template>
|
||||
<Window width="600px" ref="window" @close="close">
|
||||
<template slot="header">
|
||||
<Window ref="window" width="600px" @close="close">
|
||||
<template #header>
|
||||
<span v-show="!loading">{{ header }}</span>
|
||||
<span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
|
||||
<span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7" />
|
||||
Список загружается
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<a ref="download" style='display: none;' target="_blank"></a>
|
||||
<a ref="download" style="display: none;" target="_blank"></a>
|
||||
|
||||
<q-table
|
||||
class="recent-books-table col"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:rows="tableData"
|
||||
row-key="key"
|
||||
:pagination.sync="pagination"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
separator="cell"
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
dense
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<template #header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
|
||||
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
|
||||
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
|
||||
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
<q-th key="num" class="td-mp" style="width: 25px" :props="props">
|
||||
<span v-html="props.cols[0].label"></span>
|
||||
</q-th>
|
||||
<q-th key="date" class="td-mp break-word" style="width: 77px" :props="props">
|
||||
<span v-html="props.cols[1].label"></span>
|
||||
</q-th>
|
||||
<q-th key="desc" class="td-mp" style="width: 332px" :props="props" colspan="4">
|
||||
<q-input ref="input" v-model="search"
|
||||
outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
||||
placeholder="Найти"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
>
|
||||
<template #append>
|
||||
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
|
||||
</template>
|
||||
</q-input>
|
||||
<span v-html="props.cols[2].label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<template #body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="num" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 25px">
|
||||
@@ -42,17 +51,20 @@
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<q-td key="date" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
|
||||
<div class="break-word" style="width: 68px">
|
||||
{{ props.row.touchDate }}<br>
|
||||
{{ props.row.touchTime }}
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<q-td key="desc" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
|
||||
<div class="break-word" style="width: 332px; font-size: 90%">
|
||||
<div style="color: green">{{ props.row.desc.author }}</div>
|
||||
<div style="color: green">
|
||||
{{ props.row.desc.author }}
|
||||
</div>
|
||||
<div>{{ props.row.desc.title }}</div>
|
||||
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
@@ -68,8 +80,9 @@
|
||||
<q-btn
|
||||
dense
|
||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||
@click="handleDel(props.row.key)">
|
||||
<q-icon class="la la-times" size="14px" style="top: -6px"/>
|
||||
@click="handleDel(props.row.key)"
|
||||
>
|
||||
<q-icon class="la la-times" size="14px" />
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-td>
|
||||
@@ -78,15 +91,14 @@
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import path from 'path';
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import path from 'path-browserify';
|
||||
//import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
@@ -94,7 +106,7 @@ import Window from '../../share/Window.vue';
|
||||
import bookManager from '../share/bookManager';
|
||||
import readerApi from '../../../api/reader';
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
},
|
||||
@@ -103,10 +115,12 @@ export default @Component({
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
})
|
||||
class RecentBooksPage extends Vue {
|
||||
};
|
||||
class RecentBooksPage {
|
||||
_options = componentOptions;
|
||||
|
||||
loading = false;
|
||||
search = null;
|
||||
search = '';
|
||||
tableData = [];
|
||||
columns = [];
|
||||
pagination = {};
|
||||
@@ -167,13 +181,13 @@ class RecentBooksPage extends Vue {
|
||||
return;
|
||||
this.initing = true;
|
||||
|
||||
|
||||
if (this.firstInit) {//для отзывчивости
|
||||
await this.updateTableData(20);
|
||||
this.firstInit = false;
|
||||
}
|
||||
await utils.sleep(50);
|
||||
await this.updateTableData();
|
||||
|
||||
this.initing = false;
|
||||
})();
|
||||
}
|
||||
@@ -200,11 +214,13 @@ class RecentBooksPage extends Vue {
|
||||
d.setTime(book.touchTime);
|
||||
const t = utils.formatDate(d).split(' ');
|
||||
|
||||
let readPart = 0;
|
||||
let perc = '';
|
||||
let textLen = '';
|
||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
||||
if (book.textLength) {
|
||||
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
|
||||
readPart = p/book.textLength;
|
||||
perc = ` [${(readPart*100).toFixed(2)}%]`;
|
||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||
}
|
||||
|
||||
@@ -223,6 +239,7 @@ class RecentBooksPage extends Vue {
|
||||
author,
|
||||
title: `${title}${perc}${textLen}`,
|
||||
},
|
||||
readPart,
|
||||
descString: `${author}${title}${perc}${textLen}`,//для сортировки
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
@@ -244,6 +261,11 @@ class RecentBooksPage extends Vue {
|
||||
this.updating = false;
|
||||
}
|
||||
|
||||
resetSearch() {
|
||||
this.search = '';
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
|
||||
wordEnding(num) {
|
||||
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
||||
const deci = num % 100;
|
||||
@@ -312,6 +334,8 @@ class RecentBooksPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(RecentBooksPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -346,6 +370,10 @@ class RecentBooksPage extends Vue {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.read-bar {
|
||||
height: 3px;
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,15 +11,22 @@
|
||||
<!--input ref="input"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/-->
|
||||
<q-input ref="input" class="col" outlined dense
|
||||
<q-input ref="input" v-model="needle"
|
||||
class="col" outlined dense
|
||||
placeholder="что ищем"
|
||||
v-model="needle" @keydown="inputKeyDown"
|
||||
@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 +34,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 +55,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;
|
||||
@@ -180,6 +188,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') {
|
||||
@@ -734,5 +736,7 @@ class ServerStorage extends Vue {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(ServerStorage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<template>
|
||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
<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"
|
||||
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/this.sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
:label-value="(sliderMax ? (sliderValue/sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
@@ -19,12 +20,11 @@
|
||||
|
||||
<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 +34,10 @@ export default @Component({
|
||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||
},
|
||||
},
|
||||
})
|
||||
class SetPositionPage extends Vue {
|
||||
};
|
||||
class SetPositionPage {
|
||||
_options = componentOptions;
|
||||
|
||||
sliderValue = null;
|
||||
sliderMax = null;
|
||||
|
||||
@@ -67,6 +69,8 @@ class SetPositionPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SetPositionPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<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]"
|
||||
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
87
client/components/Reader/SettingsPage/ConvertTab.inc
Normal file
@@ -0,0 +1,87 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="q-mt-sm column items-center">
|
||||
<span>Настройки конвертирования применяются ко всем</span>
|
||||
<span>вновь загружаемым или обновляемым файлам</span>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">HTML, XML, TXT</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Текст</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Опция принудительно включает эвристику разбиения текста на<br>
|
||||
параграфы в случае, если формат файла определен как html,<br>
|
||||
xml или txt. Возможна нечитабельная разметка текста.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Сайты</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div v-if="isExternalConverter">
|
||||
<div class="part-header">PDF</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Формат</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
|
||||
Размер получаемого fb2-файла при этом относительно небольшой.<br>
|
||||
При отключении этой опции, pdf будет представлен как набор<br>
|
||||
изображений (аналогично ковертированию djvu).
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Качество</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
|
||||
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||
слишком большой файл, то попробуйте понизить качество.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div v-if="isExternalConverter">
|
||||
<div class="part-header">DJVU</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-7">Качество</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
|
||||
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||
слишком большой файл, то попробуйте понизить качество.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,27 +60,11 @@
|
||||
Показывать уведомление "Оплатим хостинг вместе"
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="includer">
|
||||
<template>
|
||||
<Window ref="window" height="95%" width="600px" @close="close">
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
Настройки
|
||||
</template>
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
<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,70 +21,115 @@
|
||||
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="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');
|
||||
@@include('./ButtonsTab.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('./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';
|
||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||
import Window from '../../share/Window.vue';
|
||||
import NumInput from '../../share/NumInput.vue';
|
||||
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
||||
import wallpaperStorage from '../share/wallpaperStorage';
|
||||
|
||||
import rstore from '../../../store/modules/reader';
|
||||
import defPalette from './defPalette';
|
||||
|
||||
const hex = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
|
||||
|
||||
export default @Component({
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Window,
|
||||
NumInput,
|
||||
@@ -97,8 +143,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' : '');
|
||||
@@ -108,7 +155,7 @@ export default @Component({
|
||||
},
|
||||
vertShift: function(newValue) {
|
||||
const font = (this.webFontName ? this.webFontName : this.fontName);
|
||||
if (this.fontShifts[font] != newValue) {
|
||||
if (this.fontShifts[font] != newValue || this.fontVertShift != newValue) {
|
||||
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
|
||||
this.fontVertShift = newValue;
|
||||
}
|
||||
@@ -125,6 +172,10 @@ export default @Component({
|
||||
if (newValue != '' && this.pageChangeAnimation == 'flip')
|
||||
this.pageChangeAnimation = '';
|
||||
},
|
||||
dualPageMode(newValue) {
|
||||
if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
|
||||
this.pageChangeAnimation = '';
|
||||
},
|
||||
textColor: function(newValue) {
|
||||
this.textColorFiltered = newValue;
|
||||
},
|
||||
@@ -139,19 +190,35 @@ export default @Component({
|
||||
if (hex.test(newValue))
|
||||
this.backgroundColor = newValue;
|
||||
},
|
||||
dualDivColor(newValue) {
|
||||
this.dualDivColorFiltered = newValue;
|
||||
},
|
||||
dualDivColorFiltered(newValue) {
|
||||
if (hex.test(newValue))
|
||||
this.dualDivColor = newValue;
|
||||
},
|
||||
statusBarColor(newValue) {
|
||||
this.statusBarColorFiltered = newValue;
|
||||
},
|
||||
statusBarColorFiltered(newValue) {
|
||||
if (hex.test(newValue))
|
||||
this.statusBarColor = newValue;
|
||||
},
|
||||
},
|
||||
})
|
||||
class SettingsPage extends Vue {
|
||||
};
|
||||
class SettingsPage {
|
||||
_options = componentOptions;
|
||||
|
||||
selectedTab = 'profiles';
|
||||
selectedViewTab = 'color';
|
||||
selectedViewTab = 'mode';
|
||||
selectedKeysTab = 'mouse';
|
||||
form = {};
|
||||
fontBold = false;
|
||||
fontItalic = false;
|
||||
vertShift = 0;
|
||||
tabsScrollable = false;
|
||||
textColorFiltered = '';
|
||||
bgColorFiltered = '';
|
||||
dualDivColorFiltered = '';
|
||||
|
||||
webFonts = [];
|
||||
fonts = [];
|
||||
@@ -160,6 +227,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;
|
||||
@@ -174,7 +254,7 @@ class SettingsPage extends Vue {
|
||||
this.$watch(
|
||||
'$refs.tabs.scrollable',
|
||||
(newValue) => {
|
||||
this.tabsScrollable = newValue && !this.$isMobileDevice;
|
||||
this.tabsScrollable = newValue && !this.$root.isMobileDevice;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -189,18 +269,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');
|
||||
@@ -212,12 +282,18 @@ class SettingsPage extends Vue {
|
||||
this.vertShift = this.fontShifts[font] || 0;
|
||||
this.textColorFiltered = this.textColor;
|
||||
this.bgColorFiltered = this.backgroundColor;
|
||||
this.dualDivColorFiltered = this.dualDivColor;
|
||||
this.statusBarColorFiltered = this.statusBarColor;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return this.$store.state.reader.settings;
|
||||
}
|
||||
@@ -247,9 +323,19 @@ class SettingsPage extends Vue {
|
||||
|
||||
get wallpaperOptions() {
|
||||
let result = [{label: 'Нет', value: ''}];
|
||||
for (let i = 1; i < 10; i++) {
|
||||
|
||||
const userWallpapers = _.cloneDeep(this.userWallpapers);
|
||||
userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
for (const wp of userWallpapers) {
|
||||
if (wallpaperStorage.keyExists(wp.cssClass))
|
||||
result.push({label: wp.label, value: wp.cssClass});
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 17; i++) {
|
||||
result.push({label: i, value: `paper${i}`});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -273,13 +359,15 @@ class SettingsPage extends Vue {
|
||||
let result = [
|
||||
{label: 'Нет', value: ''},
|
||||
{label: 'Вверх-вниз', value: 'downShift'},
|
||||
{label: 'Вправо-влево', value: 'rightShift'},
|
||||
(!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
|
||||
{label: 'Протаивание', value: 'thaw'},
|
||||
{label: 'Мерцание', value: 'blink'},
|
||||
{label: 'Вращение', value: 'rotate'},
|
||||
];
|
||||
if (this.wallpaper == '')
|
||||
result.push({label: 'Листание', value: 'flip'});
|
||||
(this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
|
||||
];
|
||||
|
||||
result = result.filter(v => v);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -342,6 +430,12 @@ class SettingsPage extends Vue {
|
||||
case 'bg':
|
||||
result += `background-color: ${this.backgroundColor};`
|
||||
break;
|
||||
case 'div':
|
||||
result += `background-color: ${this.dualDivColor};`
|
||||
break;
|
||||
case 'statusbar':
|
||||
result += `background-color: ${this.statusBarColor};`
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -371,10 +465,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) {
|
||||
@@ -407,7 +497,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 'Удаление не подтверждено'; },
|
||||
@@ -500,7 +590,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) {
|
||||
//
|
||||
@@ -508,6 +599,71 @@ class SettingsPage extends Vue {
|
||||
|
||||
}
|
||||
|
||||
loadWallpaperFileClick() {
|
||||
this.$refs.file.click();
|
||||
}
|
||||
|
||||
loadWallpaperFile() {
|
||||
const file = this.$refs.file.files[0];
|
||||
if (file.size > 10*1024*1024) {
|
||||
this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.type != 'image/png' && file.type != 'image/jpeg') {
|
||||
this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userWallpapers.length >= 100) {
|
||||
this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs.file.value = '';
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
(async() => {
|
||||
const data = e.target.result;
|
||||
const key = utils.toHex(cryptoUtils.sha256(data));
|
||||
const label = `#${key.substring(0, 4)}`;
|
||||
const cssClass = `user-paper${key}`;
|
||||
|
||||
const newUserWallpapers = _.cloneDeep(this.userWallpapers);
|
||||
const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
|
||||
|
||||
if (index < 0)
|
||||
newUserWallpapers.push({label, cssClass});
|
||||
if (!wallpaperStorage.keyExists(cssClass))
|
||||
await wallpaperStorage.setData(cssClass, data);
|
||||
|
||||
this.userWallpapers = newUserWallpapers;
|
||||
this.wallpaper = cssClass;
|
||||
})();
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
async delWallpaper() {
|
||||
if (this.wallpaper.indexOf('user-paper') == 0) {
|
||||
const newUserWallpapers = [];
|
||||
for (const wp of this.userWallpapers) {
|
||||
if (wp.cssClass != this.wallpaper) {
|
||||
newUserWallpapers.push(wp);
|
||||
}
|
||||
}
|
||||
|
||||
await wallpaperStorage.removeData(this.wallpaper);
|
||||
|
||||
this.userWallpapers = newUserWallpapers;
|
||||
this.wallpaper = '';
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||
this.close();
|
||||
@@ -515,6 +671,8 @@ class SettingsPage extends Vue {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SettingsPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
@@ -544,7 +702,7 @@ class SettingsPage extends Vue {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label-1 {
|
||||
.label-1, .label-7 {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
@@ -556,7 +714,7 @@ class SettingsPage extends Vue {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
|
||||
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
@@ -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"
|
||||
<div style="width: 80px">
|
||||
Сочетание клавиш
|
||||
</div>
|
||||
<q-input
|
||||
ref="input"
|
||||
v-model="search"
|
||||
class="q-ml-sm col"
|
||||
outlined dense rounded
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
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>
|
||||
|
||||
|
||||
116
client/components/Reader/SettingsPage/ViewTab/Color.inc
Normal file
@@ -0,0 +1,116 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">
|
||||
Цвет
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Текст
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-input
|
||||
v-model="textColorFiltered"
|
||||
class="col-left no-mp"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color
|
||||
v-model="textColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" />
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Фон
|
||||
</div>
|
||||
<div class="col row">
|
||||
<q-input
|
||||
v-model="bgColorFiltered"
|
||||
class="col-left no-mp"
|
||||
outlined dense
|
||||
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors" />
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" />
|
||||
<div class="item row">
|
||||
<div class="label-2">
|
||||
Обои
|
||||
</div>
|
||||
<div class="col row items-center">
|
||||
<q-select
|
||||
v-model="wallpaper"
|
||||
class="col-left no-mp"
|
||||
:options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div>
|
||||
{{ scope.opt.label }}
|
||||
</div>
|
||||
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item
|
||||
v-bind="scope.itemProps"
|
||||
>
|
||||
<q-item-section style="min-width: 50px;">
|
||||
<q-item-label v-html="scope.opt.label" />
|
||||
</q-item-section>
|
||||
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;" />
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<div class="q-px-xs" />
|
||||
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Добавить файл обоев
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
|
||||
Удалить выбранные обои
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-sm" />
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row items-center">
|
||||
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="file" type="file" style="display: none;" @change="loadWallpaperFile" />
|
||||
124
client/components/Reader/SettingsPage/ViewTab/Mode.inc
Normal file
@@ -0,0 +1,124 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Режим</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="dualPageMode" size="xs" label="Двухстраничный режим" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="part-header">Страницы</div>
|
||||
<div class="item row">
|
||||
<div class="label-2">Отступ границ</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Слева/справа от края экрана
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сверху/снизу от края экрана
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="dualPageMode" class="item row">
|
||||
<div class="label-2">Отступ внутри</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualIndentLR" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Слева/справа внутри страницы
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="dualPageMode">
|
||||
<div class="part-header">Разделитель</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-2">Цвет</div>
|
||||
<div class="col-left row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="dualDivColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="dualDivColorAsText"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('div')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="dualDivColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="q-px-xs"/>
|
||||
<q-checkbox v-model="dualDivColorAsText" size="xs" label="Как у текста" />
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Ширина (px)</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivWidth" :min="0" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Ширина разделителя
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Высота (%)</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivHeight" :min="0" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Высота разделителя
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Пунктир</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivStrokeFill" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Заполнение пунктира
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="dualDivStrokeGap" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Промежуток пунктира
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Ширина тени</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="dualDivShadowWidth" :min="0" :max="100"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
64
client/components/Reader/SettingsPage/ViewTab/Status.inc
Normal file
@@ -0,0 +1,64 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Строка статуса</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Статус</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||
<q-checkbox v-show="showStatusBar" class="q-ml-sm" v-model="statusBarTop" size="xs" label="Вверху/внизу" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" class="item row no-wrap">
|
||||
<div class="label-2">Цвет</div>
|
||||
<div class="col-left row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="statusBarColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="statusBarColorAsText"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('statusbar')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="statusBarColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="q-px-xs"/>
|
||||
<q-checkbox v-model="statusBarColorAsText" size="xs" label="Как у текста"/>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" class="item row">
|
||||
<div class="label-2">Высота</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="showStatusBar" class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По клику на автора-название в строке статуса<br>
|
||||
открывать оригинал произведения в новой вкладке
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,23 +15,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Отступ</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Слева/справа
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сверху/снизу
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
@@ -123,7 +106,7 @@
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
|
||||
<q-checkbox v-model="imageFitWidth" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,34 +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="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 == '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,58 +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>
|
||||
|
||||
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
|
||||
</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"
|
||||
:disable="wallpaper != ''"
|
||||
>
|
||||
<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 class="q-px-sm"/>
|
||||
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,36 +0,0 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Строка статуса</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Статус</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Высота</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По клику на автора-название в строке статуса<br>
|
||||
открывать оригинал произведения в новой вкладке
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,11 +2,11 @@ import {sleep} from '../../../share/utils';
|
||||
|
||||
export default class DrawHelper {
|
||||
fontBySize(size) {
|
||||
return `${size}px ${this.fontName}`;
|
||||
return `${size}px '${this.fontName}'`;
|
||||
}
|
||||
|
||||
fontByStyle(style) {
|
||||
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px ${this.fontName}`;
|
||||
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
|
||||
}
|
||||
|
||||
measureText(text, style) {// eslint-disable-line no-unused-vars
|
||||
@@ -19,6 +19,109 @@ export default class DrawHelper {
|
||||
return this.context.measureText(text).width;
|
||||
}
|
||||
|
||||
drawLine(line, lineIndex, baseLineIndex, sel, imageDrawn) {
|
||||
/* line:
|
||||
{
|
||||
begin: Number,
|
||||
end: Number,
|
||||
first: Boolean,
|
||||
last: Boolean,
|
||||
parts: array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
||||
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
||||
text: String,
|
||||
}
|
||||
}*/
|
||||
|
||||
let out = '<div>';
|
||||
|
||||
let lineText = '';
|
||||
let center = false;
|
||||
let space = 0;
|
||||
let j = 0;
|
||||
//формируем строку
|
||||
for (const part of line.parts) {
|
||||
let tOpen = '';
|
||||
tOpen += (part.style.bold ? '<b>' : '');
|
||||
tOpen += (part.style.italic ? '<i>' : '');
|
||||
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
|
||||
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
|
||||
let tClose = '';
|
||||
tClose += (part.style.sub ? '</span>' : '');
|
||||
tClose += (part.style.sup ? '</span>' : '');
|
||||
tClose += (part.style.italic ? '</i>' : '');
|
||||
tClose += (part.style.bold ? '</b>' : '');
|
||||
|
||||
let text = '';
|
||||
if (lineIndex == 0 && this.searching) {
|
||||
for (let k = 0; k < part.text.length; k++) {
|
||||
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
||||
j++;
|
||||
}
|
||||
} else
|
||||
text = part.text;
|
||||
|
||||
if (text && text.trim() == '')
|
||||
text = `<span style="white-space: pre">${text}</span>`;
|
||||
|
||||
lineText += `${tOpen}${text}${tClose}`;
|
||||
|
||||
center = center || part.style.center;
|
||||
space = (part.style.space > space ? part.style.space : space);
|
||||
|
||||
//избражения
|
||||
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||
const img = part.image;
|
||||
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > img.h) {
|
||||
resize = `height: ${img.h}px`;
|
||||
}
|
||||
|
||||
const left = (this.w - img.w)/2;
|
||||
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (lineIndex - baseLineIndex - img.imageLine)*this.lineHeight;
|
||||
if (img.local) {
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
} else {
|
||||
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
}
|
||||
}
|
||||
imageDrawn.add(img.paraIndex);
|
||||
}
|
||||
|
||||
if (img && img.id && img.inline) {
|
||||
if (img.local) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > this.fontSize) {
|
||||
resize = `height: ${this.fontSize - 3}px`;
|
||||
}
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
|
||||
}
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
|
||||
if ((line.first || space) && !center) {
|
||||
let p = (line.first ? this.p : 0);
|
||||
p = (space ? p + this.p*space : p);
|
||||
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
|
||||
}
|
||||
|
||||
if (line.last || center)
|
||||
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
||||
|
||||
out += lineText + '</div>';
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
drawPage(lines, isScrolling) {
|
||||
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
|
||||
return '';
|
||||
@@ -26,134 +129,65 @@ export default class DrawHelper {
|
||||
const font = this.fontByStyle({});
|
||||
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
|
||||
|
||||
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` +
|
||||
const boxH = this.h + (isScrolling ? this.lineHeight : 0);
|
||||
let out = `<div class="row no-wrap" style="width: ${this.boxW}px; height: ${boxH}px;` +
|
||||
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
||||
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
|
||||
|
||||
let imageDrawn = new Set();
|
||||
let imageDrawn1 = new Set();
|
||||
let imageDrawn2 = new Set();
|
||||
let len = lines.length;
|
||||
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
|
||||
len = (len > lineCount ? lineCount : len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const line = lines[i];
|
||||
/* line:
|
||||
{
|
||||
begin: Number,
|
||||
end: Number,
|
||||
first: Boolean,
|
||||
last: Boolean,
|
||||
parts: array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
||||
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
||||
text: String,
|
||||
}
|
||||
}*/
|
||||
let sel = new Set();
|
||||
//поиск
|
||||
if (i == 0 && this.searching) {
|
||||
let pureText = '';
|
||||
for (const part of line.parts) {
|
||||
pureText += part.text;
|
||||
}
|
||||
|
||||
pureText = pureText.toLowerCase();
|
||||
let j = 0;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
j = pureText.indexOf(this.needle, j);
|
||||
if (j >= 0) {
|
||||
for (let k = 0; k < this.needle.length; k++) {
|
||||
sel.add(j + k);
|
||||
}
|
||||
} else
|
||||
break;
|
||||
j++;
|
||||
}
|
||||
//поиск
|
||||
let sel = new Set();
|
||||
if (len > 0 && this.searching) {
|
||||
const line = lines[0];
|
||||
let pureText = '';
|
||||
for (const part of line.parts) {
|
||||
pureText += part.text;
|
||||
}
|
||||
|
||||
let lineText = '';
|
||||
let center = false;
|
||||
let space = 0;
|
||||
pureText = pureText.toLowerCase();
|
||||
let j = 0;
|
||||
//формируем строку
|
||||
for (const part of line.parts) {
|
||||
let tOpen = '';
|
||||
tOpen += (part.style.bold ? '<b>' : '');
|
||||
tOpen += (part.style.italic ? '<i>' : '');
|
||||
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
|
||||
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
|
||||
let tClose = '';
|
||||
tClose += (part.style.sub ? '</span>' : '');
|
||||
tClose += (part.style.sup ? '</span>' : '');
|
||||
tClose += (part.style.italic ? '</i>' : '');
|
||||
tClose += (part.style.bold ? '</b>' : '');
|
||||
|
||||
let text = '';
|
||||
if (i == 0 && this.searching) {
|
||||
for (let k = 0; k < part.text.length; k++) {
|
||||
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
||||
j++;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
j = pureText.indexOf(this.needle, j);
|
||||
if (j >= 0) {
|
||||
for (let k = 0; k < this.needle.length; k++) {
|
||||
sel.add(j + k);
|
||||
}
|
||||
} else
|
||||
text = part.text;
|
||||
|
||||
if (text && text.trim() == '')
|
||||
text = `<span style="white-space: pre">${text}</span>`;
|
||||
|
||||
lineText += `${tOpen}${text}${tClose}`;
|
||||
|
||||
center = center || part.style.center;
|
||||
space = (part.style.space > space ? part.style.space : space);
|
||||
|
||||
//избражения
|
||||
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||
const img = part.image;
|
||||
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > img.h) {
|
||||
resize = `height: ${img.h}px`;
|
||||
}
|
||||
|
||||
const left = (this.w - img.w)/2;
|
||||
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (i - img.imageLine)*this.lineHeight;
|
||||
if (img.local) {
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
} else {
|
||||
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||
}
|
||||
}
|
||||
imageDrawn.add(img.paraIndex);
|
||||
}
|
||||
|
||||
if (img && img.id && img.inline) {
|
||||
if (img.local) {
|
||||
const bin = this.parsed.binary[img.id];
|
||||
if (bin) {
|
||||
let resize = '';
|
||||
if (bin.h > this.fontSize) {
|
||||
resize = `height: ${this.fontSize - 3}px`;
|
||||
}
|
||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
|
||||
}
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
break;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
|
||||
if ((line.first || space) && !center) {
|
||||
let p = (line.first ? this.p : 0);
|
||||
p = (space ? p + this.p*space : p);
|
||||
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
|
||||
//отрисовка строк
|
||||
if (!this.dualPageMode) {
|
||||
out += `<div class="fit">`;
|
||||
for (let i = 0; i < len; i++) {
|
||||
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
|
||||
}
|
||||
out += `</div>`;
|
||||
} else {
|
||||
//левая страница
|
||||
out += `<div style="width: ${this.w}px; margin-left: ${this.dualIndentLR}px; position: relative;">`;
|
||||
const l2 = (this.pageRowsCount > len ? len : this.pageRowsCount);
|
||||
for (let i = 0; i < l2; i++) {
|
||||
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
|
||||
}
|
||||
out += '</div>';
|
||||
|
||||
if (line.last || center)
|
||||
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
||||
//разделитель
|
||||
out += `<div style="width: ${this.dualIndentLR*2}px;"></div>`;
|
||||
|
||||
out += (i > 0 ? '<br>' : '') + lineText;
|
||||
//правая страница
|
||||
out += `<div style="width: ${this.w}px; margin-right: ${this.dualIndentLR}px; position: relative;">`;
|
||||
for (let i = l2; i < len; i++) {
|
||||
out += this.drawLine(lines[i], i, l2, sel, imageDrawn2);
|
||||
}
|
||||
out += '</div>';
|
||||
}
|
||||
|
||||
out += '</div>';
|
||||
@@ -179,8 +213,8 @@ export default class DrawHelper {
|
||||
|
||||
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
|
||||
const barWidth = w - w1 - w2 - fh2;
|
||||
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarColor);
|
||||
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarColor);
|
||||
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarRgbaColor);
|
||||
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarRgbaColor);
|
||||
}
|
||||
|
||||
if (w1 <= w)
|
||||
@@ -193,12 +227,12 @@ export default class DrawHelper {
|
||||
|
||||
let out = `<div class="layout" style="` +
|
||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||
`color: ${this.statusBarColor}">`;
|
||||
`color: ${this.statusBarRgbaColor}">`;
|
||||
|
||||
const fontSize = statusBarHeight*0.75;
|
||||
const font = 'bold ' + this.fontBySize(fontSize);
|
||||
|
||||
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarColor);
|
||||
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarRgbaColor);
|
||||
|
||||
const date = new Date();
|
||||
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||
@@ -207,7 +241,7 @@ export default class DrawHelper {
|
||||
|
||||
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
||||
|
||||
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
|
||||
out += this.drawPercentBar(this.realWidth/2 + fontSize, 2, this.realWidth/2 - timeW - 3*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
|
||||
|
||||
out += '</div>';
|
||||
return out;
|
||||
@@ -274,7 +308,7 @@ export default class DrawHelper {
|
||||
}
|
||||
|
||||
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
|
||||
const s = this.w + this.fontSize;
|
||||
const s = this.boxW + this.fontSize;
|
||||
|
||||
if (isDown) {
|
||||
page1.style.transform = `translateX(${s}px)`;
|
||||
|
||||
93
client/components/Reader/TextPage/TextPage.css
Normal file
@@ -0,0 +1,93 @@
|
||||
@keyframes page1-animation-thaw {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes page2-animation-thaw {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.paper1 {
|
||||
background: url("images/paper1.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper2 {
|
||||
background: url("images/paper2.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper3 {
|
||||
background: url("images/paper3.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper4 {
|
||||
background: url("images/paper4.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper5 {
|
||||
background: url("images/paper5.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper6 {
|
||||
background: url("images/paper6.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper7 {
|
||||
background: url("images/paper7.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper8 {
|
||||
background: url("images/paper8.jpg") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper9 {
|
||||
background: url("images/paper9.jpg");
|
||||
}
|
||||
|
||||
.paper10 {
|
||||
background: url("images/paper10.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper11 {
|
||||
background: url("images/paper11.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper12 {
|
||||
background: url("images/paper12.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper13 {
|
||||
background: url("images/paper13.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper14 {
|
||||
background: url("images/paper14.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper15 {
|
||||
background: url("images/paper15.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper16 {
|
||||
background: url("images/paper16.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.paper17 {
|
||||
background: url("images/paper17.png") center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div ref="main" class="main">
|
||||
<div class="layout back" @wheel.prevent.stop="onMouseWheel">
|
||||
<div v-html="background"></div>
|
||||
<!-- img -->
|
||||
<div class="absolute" v-html="background"></div>
|
||||
<div class="absolute" v-html="pageDivider"></div>
|
||||
</div>
|
||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||
@@ -17,17 +17,24 @@
|
||||
<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>
|
||||
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||
</div>
|
||||
@@ -35,12 +42,15 @@
|
||||
|
||||
<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 './TextPage.css';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
import bookManager from '../share/bookManager';
|
||||
import DrawHelper from './DrawHelper';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
@@ -48,7 +58,7 @@ 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});
|
||||
@@ -67,13 +77,16 @@ export default @Component({
|
||||
this.updateLayout();
|
||||
},
|
||||
},
|
||||
})
|
||||
class TextPage extends Vue {
|
||||
};
|
||||
class TextPage {
|
||||
_options = componentOptions;
|
||||
|
||||
toggleLayout = false;
|
||||
showStatusBar = false;
|
||||
clickControl = true;
|
||||
|
||||
background = null;
|
||||
pageDivider = null;
|
||||
page1 = null;
|
||||
page2 = null;
|
||||
statusBar = null;
|
||||
@@ -110,7 +123,11 @@ class TextPage extends Vue {
|
||||
|
||||
this.debouncedDrawStatusBar = _.throttle(() => {
|
||||
this.drawStatusBar();
|
||||
}, 60);
|
||||
}, 60);
|
||||
|
||||
this.debouncedDrawPageDividerAndOrnament = _.throttle(() => {
|
||||
this.drawPageDividerAndOrnament();
|
||||
}, 65);
|
||||
|
||||
this.debouncedLoadSettings = _.debounce(() => {
|
||||
this.loadSettings();
|
||||
@@ -132,7 +149,7 @@ 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);
|
||||
this.$nextTick(this.onResize);
|
||||
@@ -152,7 +169,7 @@ class TextPage extends Vue {
|
||||
const wideLetter = 'Щ';
|
||||
|
||||
//preloaded fonts
|
||||
this.fontList = [`12px ${this.fontName}`];
|
||||
this.fontList = [`12px '${this.fontName}'`];
|
||||
|
||||
//widths
|
||||
this.realWidth = this.$refs.main.clientWidth;
|
||||
@@ -161,14 +178,16 @@ class TextPage extends Vue {
|
||||
this.$refs.layoutEvents.style.width = this.realWidth + 'px';
|
||||
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
|
||||
|
||||
this.w = this.realWidth - 2*this.indentLR;
|
||||
const dual = (this.dualPageMode ? 2 : 1);
|
||||
this.boxW = this.realWidth - 2*this.indentLR;
|
||||
this.w = this.boxW/dual - (this.dualPageMode ? 2*this.dualIndentLR : 0);
|
||||
|
||||
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
|
||||
this.h = this.scrollHeight - 2*this.indentTB;
|
||||
this.lineHeight = this.fontSize + this.lineInterval;
|
||||
this.pageLineCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
|
||||
|
||||
this.$refs.scrollingPage1.style.width = this.w + 'px';
|
||||
this.$refs.scrollingPage2.style.width = this.w + 'px';
|
||||
this.lineHeight = this.fontSize + this.lineInterval;
|
||||
this.pageRowsCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
|
||||
this.pageLineCount = (this.dualPageMode ? this.pageRowsCount*2 : this.pageRowsCount)
|
||||
|
||||
//stuff
|
||||
this.currentAnimation = '';
|
||||
@@ -180,7 +199,10 @@ class TextPage extends Vue {
|
||||
this.$refs.statusBar.style.left = '0px';
|
||||
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
|
||||
|
||||
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
|
||||
const sbColor = (this.statusBarColorAsText ? this.textColor : this.statusBarColor);
|
||||
this.statusBarRgbaColor = this.hex2rgba(sbColor || '#000000', this.statusBarColorAlpha);
|
||||
const ddColor = (this.dualDivColorAsText ? this.textColor : this.dualDivColor);
|
||||
this.dualDivRgbaColor = this.hex2rgba(ddColor || '#000000', this.dualDivColorAlpha);
|
||||
|
||||
//drawHelper
|
||||
this.drawHelper.realWidth = this.realWidth;
|
||||
@@ -188,10 +210,20 @@ class TextPage extends Vue {
|
||||
this.drawHelper.lastBook = this.lastBook;
|
||||
this.drawHelper.book = this.book;
|
||||
this.drawHelper.parsed = this.parsed;
|
||||
this.drawHelper.pageRowsCount = this.pageRowsCount;
|
||||
this.drawHelper.pageLineCount = this.pageLineCount;
|
||||
|
||||
this.drawHelper.dualPageMode = this.dualPageMode;
|
||||
this.drawHelper.dualIndentLR = this.dualIndentLR;
|
||||
/*this.drawHelper.dualDivWidth = this.dualDivWidth;
|
||||
this.drawHelper.dualDivHeight = this.dualDivHeight;
|
||||
this.drawHelper.dualDivRgbaColor = this.dualDivRgbaColor;
|
||||
this.drawHelper.dualDivStrokeFill = this.dualDivStrokeFill;
|
||||
this.drawHelper.dualDivStrokeGap = this.dualDivStrokeGap;
|
||||
this.drawHelper.dualDivShadowWidth = this.dualDivShadowWidth;*/
|
||||
|
||||
this.drawHelper.backgroundColor = this.backgroundColor;
|
||||
this.drawHelper.statusBarColor = this.statusBarColor;
|
||||
this.drawHelper.statusBarRgbaColor = this.statusBarRgbaColor;
|
||||
this.drawHelper.fontStyle = this.fontStyle;
|
||||
this.drawHelper.fontWeight = this.fontWeight;
|
||||
this.drawHelper.fontSize = this.fontSize;
|
||||
@@ -200,6 +232,7 @@ class TextPage extends Vue {
|
||||
this.drawHelper.textColor = this.textColor;
|
||||
this.drawHelper.textShift = this.textShift;
|
||||
this.drawHelper.p = this.p;
|
||||
this.drawHelper.boxW = this.boxW;
|
||||
this.drawHelper.w = this.w;
|
||||
this.drawHelper.h = this.h;
|
||||
this.drawHelper.indentLR = this.indentLR;
|
||||
@@ -228,32 +261,33 @@ class TextPage extends Vue {
|
||||
|
||||
//parsed
|
||||
if (this.parsed) {
|
||||
this.parsed.p = this.p;
|
||||
this.parsed.w = this.w;// px, ширина текста
|
||||
this.parsed.font = this.font;
|
||||
this.parsed.fontSize = this.fontSize;
|
||||
this.parsed.wordWrap = this.wordWrap;
|
||||
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
|
||||
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
|
||||
let t = wideLetter;
|
||||
if (!this.drawHelper.measureText(t, {}))
|
||||
let wideLine = wideLetter;
|
||||
if (!this.drawHelper.measureText(wideLine, {}))
|
||||
throw new Error('Ошибка measureText');
|
||||
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
|
||||
this.parsed.maxWordLength = t.length - 1;
|
||||
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
|
||||
this.parsed.lineHeight = this.lineHeight;
|
||||
this.parsed.showImages = this.showImages;
|
||||
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||
while (this.drawHelper.measureText(wideLine, {}) < this.w) wideLine += wideLetter;
|
||||
|
||||
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
|
||||
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
|
||||
this.parsed.setSettings({
|
||||
p: this.p,
|
||||
w: this.w,
|
||||
font: this.font,
|
||||
fontSize: this.fontSize,
|
||||
wordWrap: this.wordWrap,
|
||||
cutEmptyParagraphs: this.cutEmptyParagraphs,
|
||||
addEmptyParagraphs: this.addEmptyParagraphs,
|
||||
maxWordLength: wideLine.length - 1,
|
||||
lineHeight: this.lineHeight,
|
||||
showImages: this.showImages,
|
||||
showInlineImagesInCenter: this.showInlineImagesInCenter,
|
||||
imageHeightLines: this.imageHeightLines,
|
||||
imageFitWidth: this.imageFitWidth,
|
||||
compactTextPerc: this.compactTextPerc,
|
||||
testWidth: 0,
|
||||
measureText: this.drawHelper.measureText.bind(this.drawHelper),
|
||||
});
|
||||
}
|
||||
|
||||
//scrolling page
|
||||
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
|
||||
const pageSpace = this.scrollHeight - this.pageRowsCount*this.lineHeight;
|
||||
let top = pageSpace/2;
|
||||
if (this.showStatusBar)
|
||||
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
@@ -262,14 +296,14 @@ class TextPage extends Vue {
|
||||
|
||||
page1.perspective = page2.perspective = '3072px';
|
||||
|
||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
||||
page1.width = page2.width = this.boxW + this.indentLR + 'px';
|
||||
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||
page1.top = page2.top = top + 'px';
|
||||
page1.left = page2.left = this.indentLR + 'px';
|
||||
|
||||
page1 = this.$refs.scrollingPage1.style;
|
||||
page2 = this.$refs.scrollingPage2.style;
|
||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
||||
page1.width = page2.width = this.boxW + this.indentLR + 'px';
|
||||
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
|
||||
}
|
||||
|
||||
@@ -333,20 +367,36 @@ class TextPage extends Vue {
|
||||
if (!omitLoadFonts)
|
||||
await this.loadFonts();
|
||||
|
||||
this.draw();
|
||||
|
||||
// ширина шрифта некоторое время выдается неверно, поэтому
|
||||
if (!omitLoadFonts) {
|
||||
const parsed = this.parsed;
|
||||
|
||||
let i = 0;
|
||||
const t = this.parsed.testText;
|
||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
||||
if (omitLoadFonts) {
|
||||
this.draw();
|
||||
} else {
|
||||
// ширина шрифта некоторое время выдается неверно,
|
||||
// не удалось событийно отловить этот момент, поэтому костыль
|
||||
while (this.checkingFont) {
|
||||
this.stopCheckingFont = true;
|
||||
await utils.sleep(100);
|
||||
}
|
||||
|
||||
if (this.parsed === parsed) {
|
||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||
this.draw();
|
||||
this.checkingFont = true;
|
||||
this.stopCheckingFont = false;
|
||||
try {
|
||||
const parsed = this.parsed;
|
||||
|
||||
let i = 0;
|
||||
const t = 'Это тестовый текст. Его ширина выдается системой неправильно некоторое время.';
|
||||
let twprev = 0;
|
||||
//5 секунд проверяем изменения шрифта
|
||||
while (!this.stopCheckingFont && i++ < 50 && this.parsed === parsed) {
|
||||
const tw = this.drawHelper.measureText(t, {});
|
||||
if (tw !== twprev) {
|
||||
this.parsed.setSettings({testWidth: tw});
|
||||
this.draw();
|
||||
twprev = tw;
|
||||
}
|
||||
await utils.sleep(100);
|
||||
}
|
||||
} finally {
|
||||
this.checkingFont = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,7 +445,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;
|
||||
|
||||
@@ -430,8 +480,18 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
setBackground() {
|
||||
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
|
||||
` background-color: ${this.backgroundColor}"></div>`;
|
||||
if (this.wallpaperIgnoreStatusBar) {
|
||||
this.background = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
|
||||
` background-color: ${this.backgroundColor}">` +
|
||||
`<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
|
||||
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
|
||||
`</div>` +
|
||||
`</div>`;
|
||||
} else {
|
||||
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
|
||||
` background-color: ${this.backgroundColor}"></div>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async onResize() {
|
||||
@@ -449,7 +509,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
get font() {
|
||||
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontName}`;
|
||||
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
|
||||
}
|
||||
|
||||
onPage1TransitionEnd() {
|
||||
@@ -490,7 +550,7 @@ class TextPage extends Vue {
|
||||
|
||||
async startTextScrolling() {
|
||||
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
||||
this.linesDown.length <= this.pageLineCount) {
|
||||
this.linesDown.length <= this.pageLineCount || this.dualPageMode) {
|
||||
this.doStopScrolling();
|
||||
return;
|
||||
}
|
||||
@@ -608,6 +668,7 @@ class TextPage extends Vue {
|
||||
if (!this.pageChangeAnimation)
|
||||
this.debouncedPrepareNextPage();
|
||||
this.debouncedDrawStatusBar();
|
||||
this.debouncedDrawPageDividerAndOrnament();
|
||||
|
||||
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
||||
this.doEnd(true);
|
||||
@@ -747,6 +808,25 @@ class TextPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
drawPageDividerAndOrnament() {
|
||||
if (this.dualPageMode) {
|
||||
this.pageDivider = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
|
||||
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
|
||||
`<div class="fit row justify-center items-center no-wrap">` +
|
||||
`<div style="height: ${Math.round(this.scrollHeight*this.dualDivHeight/100)}px; width: ${this.dualDivWidth}px; ` +
|
||||
`box-shadow: 0 0 ${this.dualDivShadowWidth}px ${this.dualDivRgbaColor}; ` +
|
||||
`background-image: url("data:image/svg+xml;utf8,<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'>` +
|
||||
`<line x1='${this.dualDivWidth/2}' y1='0' x2='${this.dualDivWidth/2}' y2='100%' stroke='${this.dualDivRgbaColor}' ` +
|
||||
`stroke-width='${this.dualDivWidth}' stroke-dasharray='${this.dualDivStrokeFill} ${this.dualDivStrokeGap}'/>` +
|
||||
`</svg>");">` +
|
||||
`</div>` +
|
||||
`</div>` +
|
||||
`</div>`;
|
||||
} else {
|
||||
this.pageDivider = null;
|
||||
}
|
||||
}
|
||||
|
||||
blinkCachedLoadMessage(state) {
|
||||
if (state === 'finish') {
|
||||
this.statusBarMessage = '';
|
||||
@@ -965,7 +1045,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
@@ -993,7 +1073,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchEnd(event) {
|
||||
if (!this.$isMobileDevice)
|
||||
if (!this.$root.isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
|
||||
@@ -1029,13 +1109,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) {
|
||||
@@ -1052,13 +1132,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();
|
||||
@@ -1124,6 +1204,8 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default vueComponent(TextPage);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
@@ -1161,60 +1243,3 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.paper1 {
|
||||
background: url("images/paper1.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper2 {
|
||||
background: url("images/paper2.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper3 {
|
||||
background: url("images/paper3.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper4 {
|
||||
background: url("images/paper4.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper5 {
|
||||
background: url("images/paper5.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper6 {
|
||||
background: url("images/paper6.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper7 {
|
||||
background: url("images/paper7.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper8 {
|
||||
background: url("images/paper8.jpg") center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.paper9 {
|
||||
background: url("images/paper9.jpg");
|
||||
}
|
||||
|
||||
@keyframes page1-animation-thaw {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes page2-animation-thaw {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
BIN
client/components/Reader/TextPage/images/paper10.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
client/components/Reader/TextPage/images/paper11.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/components/Reader/TextPage/images/paper12.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/components/Reader/TextPage/images/paper13.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/components/Reader/TextPage/images/paper14.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/components/Reader/TextPage/images/paper15.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
client/components/Reader/TextPage/images/paper16.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/components/Reader/TextPage/images/paper17.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
@@ -4,23 +4,55 @@ import * as utils from '../../../share/utils';
|
||||
|
||||
const maxImageLineCount = 100;
|
||||
|
||||
// defaults
|
||||
const defaultSettings = {
|
||||
p: 30, //px, отступ параграфа
|
||||
w: 500, //px, ширина страницы
|
||||
|
||||
font: '', //css описание шрифта
|
||||
fontSize: 20, //px, размер шрифта
|
||||
wordWrap: false, //перенос по слогам
|
||||
cutEmptyParagraphs: false, //убирать пустые параграфы
|
||||
addEmptyParagraphs: 0, //добавлять n пустых параграфов перед непустым
|
||||
maxWordLength: 500, //px, максимальная длина слова без пробелов
|
||||
lineHeight: 26, //px, высота строки
|
||||
showImages: true, //показыввать изображения
|
||||
showInlineImagesInCenter: true, //выносить изображения в центр, работает на этапе первичного парсинга (parse)
|
||||
imageHeightLines: 100, //кол-во строк, максимальная высота изображения
|
||||
imageFitWidth: true, //ширина изображения не более ширины страницы
|
||||
dualPageMode: false, //двухстраничный режим
|
||||
compactTextPerc: 0, //проценты, степень компактности текста
|
||||
testWidth: 0, //ширина тестовой строки, пересчитывается извне при изменении шрифта браузером
|
||||
isTesting: false, //тестовый режим
|
||||
|
||||
//заглушка, измеритель ширины текста
|
||||
measureText: (text, style) => {// eslint-disable-line no-unused-vars
|
||||
return text.length*20;
|
||||
},
|
||||
};
|
||||
|
||||
//for splitToSlogi()
|
||||
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
||||
const soglas = new Set([
|
||||
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
||||
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
||||
]);
|
||||
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
||||
const alpha = new Set([...glas, ...soglas, ...znak]);
|
||||
|
||||
export default class BookParser {
|
||||
constructor(settings) {
|
||||
if (settings) {
|
||||
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
|
||||
}
|
||||
constructor(settings = {}) {
|
||||
this.sets = {};
|
||||
|
||||
// defaults
|
||||
this.p = 30;// px, отступ параграфа
|
||||
this.w = 300;// px, ширина страницы
|
||||
this.wordWrap = false;// перенос по слогам
|
||||
|
||||
//заглушка
|
||||
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
|
||||
return text.length*20;
|
||||
};
|
||||
this.setSettings(defaultSettings);
|
||||
this.setSettings(settings);
|
||||
}
|
||||
|
||||
setSettings(settings = {}) {
|
||||
this.sets = Object.assign({}, this.sets, settings);
|
||||
this.measureText = this.sets.measureText;
|
||||
}
|
||||
|
||||
async parse(data, callback) {
|
||||
if (!callback)
|
||||
callback = () => {};
|
||||
@@ -76,6 +108,7 @@ export default class BookParser {
|
||||
*/
|
||||
const getImageDimensions = (binaryId, binaryType, data) => {
|
||||
return new Promise ((resolve, reject) => { (async() => {
|
||||
data = data.replace(/[\n\r\s]/g, '');
|
||||
const i = new Image();
|
||||
let resolved = false;
|
||||
i.onload = () => {
|
||||
@@ -120,14 +153,67 @@ export default class BookParser {
|
||||
})().catch(reject); });
|
||||
};
|
||||
|
||||
const newParagraph = (text, len, addIndex) => {
|
||||
const correctCurrentPara = () => {
|
||||
//коррекция текущего параграфа
|
||||
if (paraIndex >= 0) {
|
||||
const prevParaIndex = paraIndex;
|
||||
let p = para[paraIndex];
|
||||
paraOffset -= p.length;
|
||||
|
||||
//уберем пробелы с концов параграфа, минимум 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] = {
|
||||
index: paraIndex,
|
||||
offset: paraOffset,
|
||||
length: 1,
|
||||
text: ' ',
|
||||
addIndex: i + 1,
|
||||
};
|
||||
paraIndex++;
|
||||
paraOffset++;
|
||||
}
|
||||
|
||||
if (curTitle.paraIndex == prevParaIndex)
|
||||
curTitle.paraIndex = paraIndex;
|
||||
if (curSubtitle.paraIndex == prevParaIndex)
|
||||
curSubtitle.paraIndex = paraIndex;
|
||||
}
|
||||
|
||||
p.index = paraIndex;
|
||||
p.offset = paraOffset;
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
}
|
||||
};
|
||||
|
||||
const newParagraph = (text = '', len = 0) => {
|
||||
correctCurrentPara();
|
||||
|
||||
//новый параграф
|
||||
paraIndex++;
|
||||
let p = {
|
||||
index: paraIndex,
|
||||
offset: paraOffset,
|
||||
length: len,
|
||||
length: len,//длина текста внутри параграфа без учета длины разметки
|
||||
text: text,
|
||||
addIndex: (addIndex ? addIndex : 0),
|
||||
addIndex: 0,
|
||||
};
|
||||
|
||||
if (inSubtitle) {
|
||||
@@ -137,53 +223,26 @@ export default class BookParser {
|
||||
}
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
paraOffset += len;
|
||||
};
|
||||
|
||||
const growParagraph = (text, len) => {
|
||||
if (paraIndex < 0) {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
growParagraph(text, len);
|
||||
return;
|
||||
}
|
||||
|
||||
const prevParaIndex = paraIndex;
|
||||
let p = para[paraIndex];
|
||||
paraOffset -= p.length;
|
||||
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
|
||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
||||
paraIndex--;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
newParagraph(' ', 1, i + 1);
|
||||
}
|
||||
|
||||
paraIndex++;
|
||||
p.index = paraIndex;
|
||||
p.offset = paraOffset;
|
||||
para[paraIndex] = p;
|
||||
|
||||
if (curTitle.paraIndex == prevParaIndex)
|
||||
curTitle.paraIndex = paraIndex;
|
||||
if (curSubtitle.paraIndex == prevParaIndex)
|
||||
curSubtitle.paraIndex = paraIndex;
|
||||
|
||||
//уберем начальный пробел
|
||||
p.length = 0;
|
||||
p.text = p.text.substr(1);
|
||||
}
|
||||
|
||||
p.length += len;
|
||||
p.text += text;
|
||||
|
||||
|
||||
if (inSubtitle) {
|
||||
curSubtitle.title += text;
|
||||
} else if (inTitle) {
|
||||
curTitle.title += text;
|
||||
}
|
||||
|
||||
para[paraIndex] = p;
|
||||
paraOffset += p.length;
|
||||
const p = para[paraIndex];
|
||||
p.length += len;
|
||||
p.text += text;
|
||||
paraOffset += len;
|
||||
};
|
||||
|
||||
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
|
||||
@@ -196,8 +255,8 @@ export default class BookParser {
|
||||
if (tag == 'binary') {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||
binaryType = (binaryType == 'image/jpg' ? 'image/jpeg' : binaryType);
|
||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
||||
binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
|
||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||
}
|
||||
|
||||
@@ -205,26 +264,31 @@ export default class BookParser {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
if (attrs.href && attrs.href.value) {
|
||||
const href = attrs.href.value;
|
||||
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||
const {id, local} = this.imageHrefToId(href);
|
||||
if (href[0] == '#') {//local
|
||||
imageNum++;
|
||||
|
||||
if (inPara && !this.showInlineImagesInCenter && !center)
|
||||
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||
else
|
||||
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
|
||||
this.images.push({paraIndex, num: imageNum, id, local});
|
||||
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||
|
||||
if (inPara && this.showInlineImagesInCenter)
|
||||
newParagraph(' ', 1);
|
||||
if (inPara && this.sets.showInlineImagesInCenter)
|
||||
newParagraph();
|
||||
} else {//external
|
||||
imageNum++;
|
||||
|
||||
dimPromises.push(getExternalImageDimensions(href));
|
||||
if (!this.sets.isTesting) {
|
||||
dimPromises.push(getExternalImageDimensions(href));
|
||||
} else {
|
||||
dimPromises.push(this.sets.getExternalImageDimensions(this, href));
|
||||
}
|
||||
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||
|
||||
this.images.push({paraIndex, num: imageNum, id, local});
|
||||
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,25 +327,25 @@ export default class BookParser {
|
||||
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
|
||||
});
|
||||
if (ann.length)
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
|
||||
const bt = utils.getBookTitle(fb2);
|
||||
if (bt.sequence) {
|
||||
newParagraph(bt.sequence, bt.sequence.length);
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFirstBody)
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
isFirstBody = false;
|
||||
bodyIndex++;
|
||||
}
|
||||
|
||||
if (tag == 'title') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
isFirstTitlePara = true;
|
||||
bold = true;
|
||||
center = true;
|
||||
@@ -293,7 +357,7 @@ export default class BookParser {
|
||||
|
||||
if (tag == 'section') {
|
||||
if (!isFirstSection)
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
isFirstSection = false;
|
||||
sectionLevel++;
|
||||
}
|
||||
@@ -304,7 +368,7 @@ export default class BookParser {
|
||||
|
||||
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
|
||||
if (!(tag == 'p' && isFirstTitlePara))
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
if (tag == 'p') {
|
||||
inPara = true;
|
||||
isFirstTitlePara = false;
|
||||
@@ -312,7 +376,7 @@ export default class BookParser {
|
||||
}
|
||||
|
||||
if (tag == 'subtitle') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
isFirstTitlePara = true;
|
||||
bold = true;
|
||||
center = true;
|
||||
@@ -333,11 +397,12 @@ export default class BookParser {
|
||||
}
|
||||
|
||||
if (tag == 'poem') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (tag == 'text-author') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
bold = true;
|
||||
space += 1;
|
||||
}
|
||||
}
|
||||
@@ -379,15 +444,15 @@ export default class BookParser {
|
||||
if (tag == 'epigraph' || tag == 'annotation') {
|
||||
italic = false;
|
||||
space -= 1;
|
||||
if (tag == 'annotation')
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (tag == 'stanza') {
|
||||
newParagraph(' ', 1);
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
if (tag == 'text-author') {
|
||||
bold = false;
|
||||
space -= 1;
|
||||
}
|
||||
}
|
||||
@@ -404,17 +469,14 @@ export default class BookParser {
|
||||
|
||||
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
|
||||
text = he.decode(text);
|
||||
text = text.replace(/>/g, '>');
|
||||
text = text.replace(/</g, '<');
|
||||
text = text.replace(/>/g, '>').replace(/</g, '<').replace(/[\t\n\r\xa0]/g, ' ');
|
||||
|
||||
if (text && text.trim() == '')
|
||||
text = (text.indexOf(' ') >= 0 ? ' ' : '');
|
||||
text = ' ';
|
||||
|
||||
if (!text)
|
||||
return;
|
||||
|
||||
text = text.replace(/[\t\n\r\xa0]/g, ' ');
|
||||
|
||||
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
||||
switch (path) {
|
||||
case '/fictionbook/description/title-info/author/first-name':
|
||||
@@ -452,24 +514,31 @@ export default class BookParser {
|
||||
fb2.annotation += text;
|
||||
}
|
||||
|
||||
let tOpen = (center ? '<center>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (space ? `<space w="${space}">` : '');
|
||||
let tClose = (space ? '</space>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (center ? '</center>' : '');
|
||||
if (binaryId) {
|
||||
if (!this.sets.isTesting) {
|
||||
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
||||
} else {
|
||||
dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
|
||||
}
|
||||
}
|
||||
|
||||
if (path.indexOf('/fictionbook/body/title') == 0 ||
|
||||
path.indexOf('/fictionbook/body/section') == 0 ||
|
||||
path.indexOf('/fictionbook/body/epigraph') == 0
|
||||
) {
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
}
|
||||
let tOpen = (center ? '<center>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (space ? `<space w="${space}">` : '');
|
||||
let tClose = (space ? '</space>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (center ? '</center>' : '');
|
||||
|
||||
if (binaryId) {
|
||||
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
||||
if (text != ' ')
|
||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||
else
|
||||
growParagraph(' ', 1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -481,6 +550,7 @@ export default class BookParser {
|
||||
await sax.parse(data, {
|
||||
onStartNode, onEndNode, onTextNode, onProgress
|
||||
});
|
||||
correctCurrentPara();
|
||||
|
||||
if (dimPromises.length) {
|
||||
try {
|
||||
@@ -541,9 +611,19 @@ export default class BookParser {
|
||||
let style = {};
|
||||
let image = {};
|
||||
|
||||
//оптимизация по памяти
|
||||
const copyStyle = (s) => {
|
||||
const r = {};
|
||||
for (const prop in s) {
|
||||
if (s[prop])
|
||||
r[prop] = s[prop];
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
|
||||
result.push({
|
||||
style: Object.assign({}, style),
|
||||
style: copyStyle(style),
|
||||
image,
|
||||
text
|
||||
});
|
||||
@@ -588,7 +668,7 @@ export default class BookParser {
|
||||
img.inline = true;
|
||||
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||
result.push({
|
||||
style: Object.assign({}, style),
|
||||
style: copyStyle(style),
|
||||
image: img,
|
||||
text: ''
|
||||
});
|
||||
@@ -631,7 +711,7 @@ export default class BookParser {
|
||||
});
|
||||
|
||||
//длинные слова (или белиберду без пробелов) тоже разобьем
|
||||
const maxWordLength = this.maxWordLength;
|
||||
const maxWordLength = this.sets.maxWordLength;
|
||||
const parts = result;
|
||||
result = [];
|
||||
for (const part of parts) {
|
||||
@@ -644,7 +724,7 @@ export default class BookParser {
|
||||
spaceIndex = i;
|
||||
|
||||
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
|
||||
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) {
|
||||
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.sets.w - this.sets.p) {
|
||||
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
|
||||
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
|
||||
spaceIndex = -1;
|
||||
@@ -662,86 +742,87 @@ export default class BookParser {
|
||||
splitToSlogi(word) {
|
||||
let result = [];
|
||||
|
||||
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
||||
const soglas = new Set([
|
||||
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
||||
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
||||
]);
|
||||
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
||||
const alpha = new Set([...glas, ...soglas, ...znak]);
|
||||
|
||||
let slog = '';
|
||||
let slogLen = 0;
|
||||
const len = word.length;
|
||||
word += ' ';
|
||||
for (let i = 0; i < len; i++) {
|
||||
slog += word[i];
|
||||
if (alpha.has(word[i]))
|
||||
slogLen++;
|
||||
if (len > 3) {
|
||||
let slog = '';
|
||||
let slogLen = 0;
|
||||
word += ' ';
|
||||
for (let i = 0; i < len; i++) {
|
||||
slog += word[i];
|
||||
if (alpha.has(word[i]))
|
||||
slogLen++;
|
||||
|
||||
if (slogLen > 1 && i < len - 2 && (
|
||||
//гласная, а следом не 2 согласные буквы
|
||||
(glas.has(word[i]) && !(soglas.has(word[i + 1]) &&
|
||||
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
||||
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) &&
|
||||
soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
||||
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) &&
|
||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//мягкий или твердый знак или Й
|
||||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
|
||||
(word[i] == '-')
|
||||
) &&
|
||||
//нельзя оставлять окончания на ь, ъ, й
|
||||
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
|
||||
if (slogLen > 1 && i < len - 2 && (
|
||||
//гласная, а следом не 2 согласные буквы
|
||||
(glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
|
||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
||||
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
||||
( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
|
||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||
) ||
|
||||
//мягкий или твердый знак или Й
|
||||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
|
||||
(word[i] == '-')
|
||||
) &&
|
||||
//нельзя оставлять окончания на ь, ъ, й
|
||||
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
|
||||
|
||||
) {
|
||||
result.push(slog);
|
||||
slog = '';
|
||||
slogLen = 0;
|
||||
) {
|
||||
result.push(slog);
|
||||
slog = '';
|
||||
slogLen = 0;
|
||||
}
|
||||
}
|
||||
if (slog)
|
||||
result.push(slog);
|
||||
} else {
|
||||
result.push(word);
|
||||
}
|
||||
if (slog)
|
||||
result.push(slog);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parsePara(paraIndex) {
|
||||
const para = this.para[paraIndex];
|
||||
const s = this.sets;
|
||||
|
||||
//перераспарсиваем только при изменении одного из параметров
|
||||
if (!this.force &&
|
||||
para.parsed &&
|
||||
para.parsed.testWidth === this.testWidth &&
|
||||
para.parsed.w === this.w &&
|
||||
para.parsed.p === this.p &&
|
||||
para.parsed.wordWrap === this.wordWrap &&
|
||||
para.parsed.maxWordLength === this.maxWordLength &&
|
||||
para.parsed.font === this.font &&
|
||||
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs &&
|
||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
||||
para.parsed.showImages === this.showImages &&
|
||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
||||
para.parsed.imageFitWidth === this.imageFitWidth &&
|
||||
para.parsed.compactTextPerc === this.compactTextPerc
|
||||
para.parsed.p === s.p &&
|
||||
para.parsed.w === s.w &&
|
||||
para.parsed.font === s.font &&
|
||||
para.parsed.fontSize === s.fontSize &&
|
||||
para.parsed.wordWrap === s.wordWrap &&
|
||||
para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
|
||||
para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
|
||||
para.parsed.maxWordLength === s.maxWordLength &&
|
||||
para.parsed.lineHeight === s.lineHeight &&
|
||||
para.parsed.showImages === s.showImages &&
|
||||
para.parsed.imageHeightLines === s.imageHeightLines &&
|
||||
para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
|
||||
para.parsed.compactTextPerc === s.compactTextPerc &&
|
||||
para.parsed.testWidth === s.testWidth
|
||||
)
|
||||
return para.parsed;
|
||||
|
||||
const parsed = {
|
||||
testWidth: this.testWidth,
|
||||
w: this.w,
|
||||
p: this.p,
|
||||
wordWrap: this.wordWrap,
|
||||
maxWordLength: this.maxWordLength,
|
||||
font: this.font,
|
||||
cutEmptyParagraphs: this.cutEmptyParagraphs,
|
||||
addEmptyParagraphs: this.addEmptyParagraphs,
|
||||
showImages: this.showImages,
|
||||
imageHeightLines: this.imageHeightLines,
|
||||
imageFitWidth: this.imageFitWidth,
|
||||
compactTextPerc: this.compactTextPerc,
|
||||
p: s.p,
|
||||
w: s.w,
|
||||
font: s.font,
|
||||
fontSize: s.fontSize,
|
||||
wordWrap: s.wordWrap,
|
||||
cutEmptyParagraphs: s.cutEmptyParagraphs,
|
||||
addEmptyParagraphs: s.addEmptyParagraphs,
|
||||
maxWordLength: s.maxWordLength,
|
||||
lineHeight: s.lineHeight,
|
||||
showImages: s.showImages,
|
||||
imageHeightLines: s.imageHeightLines,
|
||||
imageFitWidth: (s.imageFitWidth || s.dualPageMode),
|
||||
compactTextPerc: s.compactTextPerc,
|
||||
testWidth: s.testWidth,
|
||||
visible: true, //вычисляется позже
|
||||
};
|
||||
|
||||
@@ -773,7 +854,7 @@ export default class BookParser {
|
||||
let ofs = 0;//смещение от начала параграфа para.offset
|
||||
let imgW = 0;
|
||||
let imageInPara = false;
|
||||
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||
const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
|
||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||
for (const part of parts) {
|
||||
style = part.style;
|
||||
@@ -786,14 +867,14 @@ export default class BookParser {
|
||||
if (!bin)
|
||||
bin = {h: 1, w: 1};
|
||||
|
||||
let lineCount = this.imageHeightLines;
|
||||
let c = Math.ceil(bin.h/this.lineHeight);
|
||||
let lineCount = parsed.imageHeightLines;
|
||||
let c = Math.ceil(bin.h/parsed.lineHeight);
|
||||
|
||||
const maxH = lineCount*this.lineHeight;
|
||||
const maxH = lineCount*parsed.lineHeight;
|
||||
let maxH2 = maxH;
|
||||
if (this.imageFitWidth && bin.w > this.w) {
|
||||
maxH2 = bin.h*this.w/bin.w;
|
||||
c = Math.ceil(maxH2/this.lineHeight);
|
||||
if (parsed.imageFitWidth && bin.w > parsed.w) {
|
||||
maxH2 = bin.h*parsed.w/bin.w;
|
||||
c = Math.ceil(maxH2/parsed.lineHeight);
|
||||
}
|
||||
lineCount = (c < lineCount ? c : lineCount);
|
||||
|
||||
@@ -833,10 +914,10 @@ export default class BookParser {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.image.id && part.image.inline && this.showImages) {
|
||||
if (part.image.id && part.image.inline && parsed.showImages) {
|
||||
const bin = this.binary[part.image.id];
|
||||
if (bin) {
|
||||
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
||||
let imgH = (bin.h > parsed.fontSize ? parsed.fontSize : bin.h);
|
||||
imgW += bin.w*imgH/bin.h;
|
||||
line.parts.push({style, text: '',
|
||||
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
|
||||
@@ -951,11 +1032,11 @@ export default class BookParser {
|
||||
|
||||
//parsed.visible
|
||||
if (imageInPara) {
|
||||
parsed.visible = this.showImages;
|
||||
parsed.visible = parsed.showImages;
|
||||
} else {
|
||||
parsed.visible = !(
|
||||
(para.addIndex > this.addEmptyParagraphs) ||
|
||||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
|
||||
(para.addIndex > parsed.addEmptyParagraphs) ||
|
||||
(para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,48 +63,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;
|
||||
@@ -374,7 +332,7 @@ class BookManager {
|
||||
//-- 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');
|
||||
@@ -455,33 +413,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];
|
||||
|
||||
40
client/components/Reader/share/wallpaperStorage.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import localForage from 'localforage';
|
||||
//import _ from 'lodash';
|
||||
|
||||
const wpStore = localForage.createInstance({
|
||||
name: 'wallpaperStorage'
|
||||
});
|
||||
|
||||
class WallpaperStorage {
|
||||
constructor() {
|
||||
this.cachedKeys = [];
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
async getLength() {
|
||||
return await wpStore.length();
|
||||
}
|
||||
|
||||
async setData(key, data) {
|
||||
await wpStore.setItem(key, data);
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
async getData(key) {
|
||||
return await wpStore.getItem(key);
|
||||
}
|
||||
|
||||
async removeData(key) {
|
||||
await wpStore.removeItem(key);
|
||||
this.cachedKeys = await wpStore.keys();
|
||||
}
|
||||
|
||||
keyExists(key) {//не асинхронная
|
||||
return this.cachedKeys.includes(key);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WallpaperStorage();
|
||||
@@ -1,18 +1,132 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
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',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлен двухстраничный режим</li>
|
||||
<li>в настройки добавлены все кириллические веб-шрифты от google</li>
|
||||
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||
<li>немного улучшен парсинг fb2</li>
|
||||
</ul>
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
version: '0.9.12',
|
||||
releaseDate: '2020-12-18',
|
||||
showUntil: '2020-12-17',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>добавлена вкладка "Изображения" в окно оглавления</li>
|
||||
<li>настройки конвертирования вынесены в отдельную вкладку</li>
|
||||
<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>
|
||||
@@ -20,69 +134,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>
|
||||
@@ -90,23 +216,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>
|
||||
@@ -114,119 +244,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>
|
||||
@@ -238,34 +388,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>
|
||||
@@ -274,12 +430,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>
|
||||
@@ -287,12 +445,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>
|
||||
@@ -303,23 +463,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>
|
||||
@@ -330,12 +494,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>
|
||||
@@ -348,36 +514,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>
|
||||
@@ -386,12 +558,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>
|
||||
@@ -399,12 +573,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>
|
||||
@@ -414,17 +590,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,12 @@
|
||||
</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>
|
||||
|
||||
@@ -57,7 +63,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 +71,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 +95,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 +104,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 +125,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 +148,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +330,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>
|
||||
|
||||
|
||||
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 = {};
|
||||
|
||||
@@ -21,7 +20,8 @@ import {QSlider} from 'quasar/src/components/slider';
|
||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||
import {QSeparator} from 'quasar/src/components/separator';
|
||||
//import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
//import {QList} from 'quasar/src/components/item';
|
||||
import {QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||
import {QSpinner} from 'quasar/src/components/spinner';
|
||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||
@@ -49,7 +49,8 @@ const components = {
|
||||
QTabs, QTab,
|
||||
//QTabPanels, QTabPanel,
|
||||
QSeparator,
|
||||
//QList, QItem, QItemSection, QItemLabel,
|
||||
//QList,
|
||||
QItem, QItemSection, QItemLabel,
|
||||
QTooltip,
|
||||
QSpinner,
|
||||
QTable, QTh, QTr, QTd,
|
||||
@@ -78,16 +79,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
|
||||
});
|
||||
|
||||
22
client/share/dynamicCss.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class DynamicCss {
|
||||
constructor() {
|
||||
this.cssNodes = {};
|
||||
}
|
||||
|
||||
replace(name, cssText) {
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = cssText;
|
||||
|
||||
const parent = document.getElementsByTagName('head')[0];
|
||||
|
||||
if (this.cssNodes[name]) {
|
||||
parent.removeChild(this.cssNodes[name]);
|
||||
delete this.cssNodes[name];
|
||||
}
|
||||
|
||||
this.cssNodes[name] = parent.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
export default new DynamicCss();
|
||||
@@ -13,6 +13,10 @@ export function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function toHex(buf) {
|
||||
return Buffer.from(buf).toString('hex');
|
||||
}
|
||||
|
||||
export function stringToHex(str) {
|
||||
return Buffer.from(str).toString('hex');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
client/store/modules/fonts/fonts.json
Normal file
@@ -0,0 +1 @@
|
||||
["Alegreya","Alegreya SC","Alegreya Sans","Alegreya Sans SC","Alice","Amatic SC","Andika","Anonymous Pro","Arimo","Arsenal","Bad Script","Balsamiq Sans","Bellota","Bellota Text","Bitter","Caveat","Comfortaa","Commissioner","Cormorant","Cormorant Garamond","Cormorant Infant","Cormorant SC","Cormorant Unicase","Cousine","Cuprum","Didact Gothic","EB Garamond","El Messiri","Exo 2","Fira Code","Fira Mono","Fira Sans","Fira Sans Condensed","Fira Sans Extra Condensed","Forum","Gabriela","Hachi Maru Pop","IBM Plex Mono","IBM Plex Sans","IBM Plex Serif","Inter","Istok Web","JetBrains Mono","Jost","Jura","Kelly Slab","Kosugi","Kosugi Maru","Kurale","Ledger","Literata","Lobster","Lora","M PLUS 1p","M PLUS Rounded 1c","Manrope","Marck Script","Marmelad","Merriweather","Montserrat","Montserrat Alternates","Neucha","Noto Sans","Noto Serif","Nunito","Old Standard TT","Open Sans","Open Sans Condensed","Oranienbaum","Oswald","PT Mono","PT Sans","PT Sans Caption","PT Sans Narrow","PT Serif","PT Serif Caption","Pacifico","Pangolin","Pattaya","Philosopher","Piazzolla","Play","Playfair Display","Playfair Display SC","Podkova","Poiret One","Prata","Press Start 2P","Prosto One","Raleway","Roboto","Roboto Condensed","Roboto Mono","Roboto Slab","Rubik","Rubik Mono One","Ruda","Ruslan Display","Russo One","Sawarabi Gothic","Scada","Seymour One","Source Code Pro","Source Sans Pro","Source Serif Pro","Spectral","Spectral SC","Stalinist One","Tenor Sans","Tinos","Ubuntu","Ubuntu Condensed","Ubuntu Mono","Underdog","Viaoda Libre","Vollkorn","Vollkorn SC","Yanone Kaffeesatz","Yeseva One"]
|
||||
13
client/store/modules/fonts/fonts2list.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
async function main() {
|
||||
const webfonts = await fs.readFile('webfonts.json');
|
||||
let fonts = JSON.parse(webfonts);
|
||||
|
||||
fonts = fonts.items.filter(item => item.subsets.includes('cyrillic'));
|
||||
fonts = fonts.map(item => item.family);
|
||||
fonts.sort();
|
||||
|
||||
await fs.writeFile('fonts.json', JSON.stringify(fonts));
|
||||
}
|
||||
main();
|
||||
@@ -1,8 +1,11 @@
|
||||
import * as utils from '../../share/utils';
|
||||
import googleFonts from './fonts/fonts.json';
|
||||
|
||||
const readerActions = {
|
||||
'help': 'Вызвать cправку',
|
||||
'loader': 'На страницу загрузки',
|
||||
'loadFile': 'Загрузить файл с диска',
|
||||
'loadBuffer': 'Загрузить из буфера обмена',
|
||||
'help': 'Вызвать cправку',
|
||||
'settings': 'Настроить',
|
||||
'undoAction': 'Действие назад',
|
||||
'redoAction': 'Действие вперед',
|
||||
@@ -12,8 +15,9 @@ const readerActions = {
|
||||
'setPosition': 'Установить позицию',
|
||||
'search': 'Найти в тексте',
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'splitToPara': 'Обновить с разбиением на параграфы',
|
||||
'convOptions': 'Настроить конвертирование',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'clickControl': 'Управление кликом',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'contents': 'Оглавление/закладки',
|
||||
'libs': 'Сетевая библиотека',
|
||||
@@ -34,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},
|
||||
@@ -41,18 +48,21 @@ const toolButtons = [
|
||||
{name: 'setPosition', show: true},
|
||||
{name: 'search', show: true},
|
||||
{name: 'copyText', show: false},
|
||||
{name: 'splitToPara', show: false},
|
||||
{name: 'convOptions', show: true},
|
||||
{name: 'refresh', show: true},
|
||||
{name: 'contents', show: true},
|
||||
{name: 'libs', show: true},
|
||||
{name: 'recentBooks', show: true},
|
||||
{name: 'clickControl', show: false},
|
||||
{name: 'offlineMode', show: false},
|
||||
];
|
||||
|
||||
//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']},
|
||||
@@ -60,12 +70,13 @@ const hotKeys = [
|
||||
{name: 'scrolling', codes: ['Z']},
|
||||
{name: 'setPosition', codes: ['P']},
|
||||
{name: 'search', codes: ['Ctrl+F']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'splitToPara', codes: ['Shift+R']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{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']},
|
||||
@@ -91,125 +102,22 @@ const fonts = [
|
||||
{name: 'Rubik', fontVertShift: 0},
|
||||
];
|
||||
|
||||
const webFonts = [
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya+Sans', name: 'Alegreya Sans', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya+SC', name: 'Alegreya SC', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Alice', name: 'Alice', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Amatic+SC', name: 'Amatic SC', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Andika', name: 'Andika', fontVertShift: -35},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Anonymous+Pro', name: 'Anonymous Pro', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Arsenal', name: 'Arsenal', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Bad+Script', name: 'Bad Script', fontVertShift: -30},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Caveat', name: 'Caveat', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Comfortaa', name: 'Comfortaa', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant', name: 'Cormorant', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Garamond', name: 'Cormorant Garamond', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Infant', name: 'Cormorant Infant', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Unicase', name: 'Cormorant Unicase', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cousine', name: 'Cousine', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Cuprum', name: 'Cuprum', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Didact+Gothic', name: 'Didact Gothic', fontVertShift: -10},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=EB+Garamond', name: 'EB Garamond', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=El+Messiri', name: 'El Messiri', fontVertShift: -5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Mono', name: 'Fira Mono', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans', name: 'Fira Sans', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Condensed', name: 'Fira Sans Condensed', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Extra+Condensed', name: 'Fira Sans Extra Condensed', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Forum', name: 'Forum', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Gabriela', name: 'Gabriela', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Mono', name: 'IBM Plex Mono', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Sans', name: 'IBM Plex Sans', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Serif', name: 'IBM Plex Serif', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Istok+Web', name: 'Istok Web', fontVertShift: -5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Jura', name: 'Jura', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kelly+Slab', name: 'Kelly Slab', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kosugi', name: 'Kosugi', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kosugi+Maru', name: 'Kosugi Maru', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Kurale', name: 'Kurale', fontVertShift: -15},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ledger', name: 'Ledger', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Lobster', name: 'Lobster', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Lora', name: 'Lora', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Marck+Script', name: 'Marck Script', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Marmelad', name: 'Marmelad', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Merriweather', name: 'Merriweather', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Montserrat', name: 'Montserrat', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Montserrat+Alternates', name: 'Montserrat Alternates', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Neucha', name: 'Neucha', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans', name: 'Noto Sans', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans+SC', name: 'Noto Sans SC', fontVertShift: -15},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif', name: 'Noto Serif', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif+TC', name: 'Noto Serif TC', fontVertShift: -15},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Old+Standard+TT', name: 'Old Standard TT', fontVertShift: 15},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300', name: 'Open Sans Condensed', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Oranienbaum', name: 'Oranienbaum', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Oswald', name: 'Oswald', fontVertShift: -20},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Pacifico', name: 'Pacifico', fontVertShift: -35},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Pangolin', name: 'Pangolin', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Pattaya', name: 'Pattaya', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Philosopher', name: 'Philosopher', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Play', name: 'Play', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display', name: 'Playfair Display', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display+SC', name: 'Playfair Display SC', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Podkova', name: 'Podkova', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Poiret+One', name: 'Poiret One', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Prata', name: 'Prata', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Prosto+One', name: 'Prosto One', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Mono', name: 'PT Mono', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans', name: 'PT Sans', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Caption', name: 'PT Sans Caption', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Narrow', name: 'PT Sans Narrow', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Serif', name: 'PT Serif', fontVertShift: -10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=PT+Serif+Caption', name: 'PT Serif Caption', fontVertShift: -10},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Condensed', name: 'Roboto Condensed', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Mono', name: 'Roboto Mono', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Slab', name: 'Roboto Slab', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ruslan+Display', name: 'Ruslan Display', fontVertShift: 20},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Russo+One', name: 'Russo One', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Sawarabi+Gothic', name: 'Sawarabi Gothic', fontVertShift: -15},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Scada', name: 'Scada', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Seymour+One', name: 'Seymour One', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro', name: 'Source Sans Pro', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Spectral', name: 'Spectral', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Stalinist+One', name: 'Stalinist One', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Tinos', name: 'Tinos', fontVertShift: 5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Tenor+Sans', name: 'Tenor Sans', fontVertShift: 5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Underdog', name: 'Underdog', fontVertShift: 10},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Mono', name: 'Ubuntu Mono', fontVertShift: 0},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Condensed', name: 'Ubuntu Condensed', fontVertShift: -5},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Vollkorn', name: 'Vollkorn', fontVertShift: -5},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Vollkorn+SC', name: 'Vollkorn SC', fontVertShift: 0},
|
||||
|
||||
{css: 'https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz', name: 'Yanone Kaffeesatz', fontVertShift: 20},
|
||||
{css: 'https://fonts.googleapis.com/css?family=Yeseva+One', name: 'Yeseva One', fontVertShift: 10},
|
||||
|
||||
|
||||
];
|
||||
//webFonts: [{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: 0}, ...],
|
||||
const webFonts = [];
|
||||
for (const family of googleFonts) {
|
||||
webFonts.push({
|
||||
css: `https://fonts.googleapis.com/css?family=${family.replace(/\s/g, '+')}`,
|
||||
name: family,
|
||||
fontVertShift: 0,
|
||||
});
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------------
|
||||
const settingDefaults = {
|
||||
textColor: '#000000',
|
||||
backgroundColor: '#EBE2C9',
|
||||
backgroundColor: '#ebe2c9',
|
||||
wallpaper: '',
|
||||
wallpaperIgnoreStatusBar: false,
|
||||
fontStyle: '',// 'italic'
|
||||
fontWeight: '',// 'bold'
|
||||
fontSize: 20,// px
|
||||
@@ -226,9 +134,22 @@ const settingDefaults = {
|
||||
wordWrap: true,//перенос по слогам
|
||||
keepLastToFirst: false,// перенос последней строки в первую при листании
|
||||
|
||||
dualPageMode: false,
|
||||
dualIndentLR: 10,// px, отступ слева и справа внутри страницы в двухстраничном режиме
|
||||
dualDivWidth: 2,// px, ширина разделителя
|
||||
dualDivHeight: 100,// процент, высота разделителя
|
||||
dualDivColorAsText: true,//цвет как у текста
|
||||
dualDivColor: '#000000',
|
||||
dualDivColorAlpha: 0.7,// прозрачность разделителя
|
||||
dualDivStrokeFill: 1,// px, заполнение пунктира
|
||||
dualDivStrokeGap: 1,// px, промежуток пунктира
|
||||
dualDivShadowWidth: 0,// px, ширина тени
|
||||
|
||||
showStatusBar: true,
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAsText: true,//цвет как у текста
|
||||
statusBarColor: '#000000',
|
||||
statusBarColorAlpha: 0.4,
|
||||
statusBarClickOpen: true,
|
||||
|
||||
@@ -252,16 +173,20 @@ const settingDefaults = {
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
enableSitesFilter: true,
|
||||
splitToPara: false,
|
||||
djvuQuality: 20,
|
||||
pdfAsText: true,
|
||||
pdfQuality: 20,
|
||||
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
showDonationDialog2020: true,
|
||||
showLiberamaTopDialog2020: true,
|
||||
showNeedUpdateNotify: true,
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
userHotKeys: {},
|
||||
userWallpapers: [],
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
@@ -273,12 +198,13 @@ for (const button of toolButtons)
|
||||
for (const hotKey of hotKeys)
|
||||
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
||||
|
||||
const excludeDiffHotKeys = [];
|
||||
const diffExclude = [];
|
||||
for (const hotKey of hotKeys)
|
||||
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
|
||||
diffExclude.push(`userHotKeys/${hotKey.name}`);
|
||||
diffExclude.push('userWallpapers');
|
||||
|
||||
function addDefaultsToSettings(settings) {
|
||||
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
|
||||
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
|
||||
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
|
||||
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
|
||||
}
|
||||
|
||||
43
docs/beta/beta.omnireader_http
Normal file
@@ -0,0 +1,43 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name beta.omnireader.ru;
|
||||
|
||||
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 /api {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location / {
|
||||
root /home/beta.liberama/public;
|
||||
|
||||
location /tmp {
|
||||
types { } default_type "application/xml; charset=utf-8";
|
||||
add_header Content-Encoding gzip;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
expires -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name beta.omnireader.ru;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
@@ -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,
|
||||
```
|
||||
и перезапустить сервер.
|
||||
@@ -24,6 +24,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -66,6 +67,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,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;
|
||||
}
|
||||
}
|
||||
|
||||
27888
package-lock.json
generated
124
package.json
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.9.11",
|
||||
"version": "0.11.5",
|
||||
"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 latest-linux-x64 -o dist/linux/liberama .",
|
||||
"build:win": "npm run build:client && node build/win && pkg -t latest-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,68 @@
|
||||
"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": "^9.1.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"css-minimizer-webpack-plugin": "^3.1.3",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"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": "^8.0.1",
|
||||
"vue-loader": "^16.8.3",
|
||||
"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.24.0",
|
||||
"base-x": "^3.0.9",
|
||||
"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": "^9.0.1",
|
||||
"got": "^11.8.2",
|
||||
"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": "^2.3.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.5",
|
||||
"multer": "^1.4.3",
|
||||
"pako": "^2.0.4",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pidusage": "^3.0.0",
|
||||
"quasar": "^2.3.2",
|
||||
"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.22",
|
||||
"vue-router": "^4.0.12",
|
||||
"vuex": "^4.0.2",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
"webdav": "^4.7.0",
|
||||
"ws": "^8.2.3",
|
||||
"zip-stream": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||