Compare commits
331 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab46a1b99d | ||
|
|
4a08465f5b | ||
|
|
a7960d6cd6 | ||
|
|
3caea77dde | ||
|
|
fdaa3b7f93 | ||
|
|
4f433b4456 | ||
|
|
309a9ad4fb | ||
|
|
b0e7431e72 | ||
|
|
158118d183 | ||
|
|
382e37fc5a | ||
|
|
3390676847 | ||
|
|
544a995312 | ||
|
|
f209d49bb5 | ||
|
|
42ed691fdc | ||
|
|
98d2e9d266 | ||
|
|
6111158896 | ||
|
|
3267fc653c | ||
|
|
7250608767 | ||
|
|
e82063e435 | ||
|
|
6d4c44bc25 | ||
|
|
2bc94d8792 | ||
|
|
4ca3edd789 | ||
|
|
d6859efde2 | ||
|
|
3f8cbfa259 | ||
|
|
5d18c9371d | ||
|
|
631990e3bb | ||
|
|
4ae7338f94 | ||
|
|
0d1e51cb21 | ||
|
|
475fb833ea | ||
|
|
580b030ee4 | ||
|
|
6a7cbc70d6 | ||
|
|
d76f60639c | ||
|
|
e2bea407ee | ||
|
|
558fed31aa | ||
|
|
f6513d40c8 | ||
|
|
259f9baa59 | ||
|
|
db5650e276 | ||
|
|
51ebbbc569 | ||
|
|
5184661652 | ||
|
|
7853a14ce6 | ||
|
|
a01e78ace9 | ||
|
|
f7eb576d0d | ||
|
|
34f1ad8fae | ||
|
|
c60f0991df | ||
|
|
d505fd0795 | ||
|
|
93cf506535 | ||
|
|
bfb37e55d4 | ||
|
|
92afc5cb33 | ||
|
|
75cb611701 | ||
|
|
2ec1dd58a5 | ||
|
|
7d59af54de | ||
|
|
2b5f47b3de | ||
|
|
16eebfb9a4 | ||
|
|
9025218671 | ||
|
|
6bccb546bb | ||
|
|
29d49046a0 | ||
|
|
717af9ffaf | ||
|
|
00060c9f43 | ||
|
|
759ff46c92 | ||
|
|
41957cdceb | ||
|
|
d418e3a1c9 | ||
|
|
f650124428 | ||
|
|
795d109c76 | ||
|
|
6868b3effc | ||
|
|
26747b7013 | ||
|
|
5198f8aa60 | ||
|
|
552da48a32 | ||
|
|
db8a688620 | ||
|
|
3088028d05 | ||
|
|
fd62ef865d | ||
|
|
ed74ed00ed | ||
|
|
741317aaaf | ||
|
|
9b6ecd4e6b | ||
|
|
7863b3358e | ||
|
|
e1be68ec3d | ||
|
|
a054186d4b | ||
|
|
2d5c549c83 | ||
|
|
9f6072dfe1 | ||
|
|
69c44fe1ab | ||
|
|
4fa7b2443e | ||
|
|
25a69592bb | ||
|
|
44e0b26990 | ||
|
|
c4496f8dc8 | ||
|
|
9e296231d9 | ||
|
|
49b3f05d65 | ||
|
|
f124b9c050 | ||
|
|
63a86f7c06 | ||
|
|
fd0f523c64 | ||
|
|
487e605520 | ||
|
|
9e169e1f4b | ||
|
|
9612e7ebcd | ||
|
|
f66162efe7 | ||
|
|
656642697b | ||
|
|
feb70f85f8 | ||
|
|
ab1981559b | ||
|
|
c8852d9a8e | ||
|
|
9ac8dc7fd1 | ||
|
|
c9419d99e6 | ||
|
|
a1f4a83e72 | ||
|
|
a8abd5d427 | ||
|
|
629d1b0630 | ||
|
|
97c368f63a | ||
|
|
3266a444d0 | ||
|
|
1c246f71f8 | ||
|
|
96945dfc4a | ||
|
|
30eb3001ef | ||
|
|
bdd8636390 | ||
|
|
f762d2a271 | ||
|
|
cf2efc2b92 | ||
|
|
7670da4cba | ||
|
|
d87f9f2a21 | ||
|
|
6e690f3fea | ||
|
|
6321002617 | ||
|
|
15ec362428 | ||
|
|
454004e705 | ||
|
|
e14b414fc1 | ||
|
|
c4b47a5915 | ||
|
|
957c252cd7 | ||
|
|
d6a6c21762 | ||
|
|
834580cfdf | ||
|
|
de13cfb555 | ||
|
|
4f87508834 | ||
|
|
682a044f32 | ||
|
|
bdb5d90b1d | ||
|
|
01880f4456 | ||
|
|
39f78ce7e8 | ||
|
|
755c6b92da | ||
|
|
2eab9c2837 | ||
|
|
63861789de | ||
|
|
086c353eff | ||
|
|
4fe5b44655 | ||
|
|
036547e260 | ||
|
|
696f434c90 | ||
|
|
0c654d9346 | ||
|
|
a2c393b06b | ||
|
|
eae2c2b102 | ||
|
|
d9e49e3484 | ||
|
|
a28d4c2f1c | ||
|
|
9af055ec54 | ||
|
|
0d41171e9d | ||
|
|
08af826ae9 | ||
|
|
4fd577d7c5 | ||
|
|
2c8efebe98 | ||
|
|
93c9fb53ac | ||
|
|
5a4d249cf9 | ||
|
|
4cc7bdee37 | ||
|
|
a6af568411 | ||
|
|
576a6a094a | ||
|
|
e671e4b6f5 | ||
|
|
a66b2a4c70 | ||
|
|
f1ae409535 | ||
|
|
a4b56b477d | ||
|
|
d9c389812a | ||
|
|
074ef3645f | ||
|
|
cc3aa413e8 | ||
|
|
7f90c09227 | ||
|
|
f6f4d8ccc9 | ||
|
|
31afce8304 | ||
|
|
2c4ff856cd | ||
|
|
f59974e310 | ||
|
|
70e2c12a6b | ||
|
|
11f3c6ce6f | ||
|
|
e213c4640b | ||
|
|
959c5eaa59 | ||
|
|
66fa510b26 | ||
|
|
f26a3b31ac | ||
|
|
724fbf579e | ||
|
|
f192f8e3cd | ||
|
|
f13c3d19fb | ||
|
|
b51a09efcc | ||
|
|
6004043782 | ||
|
|
f9fd0dc2c3 | ||
|
|
eb5411cd20 | ||
|
|
da3c7a02f0 | ||
|
|
e67d05007f | ||
|
|
b0a9a6a08e | ||
|
|
d848ea35f4 | ||
|
|
350f20effe | ||
|
|
b6dc8f98fe | ||
|
|
1b762ee48d | ||
|
|
cc3d7f1eac | ||
|
|
4107282fbf | ||
|
|
c29ffc3fcd | ||
|
|
f648bcda13 | ||
|
|
aa0044eed2 | ||
|
|
2312a721ae | ||
|
|
b93fc39b00 | ||
|
|
2dc2cd700f | ||
|
|
d69e534f8b | ||
|
|
1de9ddd394 | ||
|
|
77c68d4e11 | ||
|
|
2a0d1dcfce | ||
|
|
5a19cca407 | ||
|
|
4e8773ecde | ||
|
|
4c7dada809 | ||
|
|
65690b15da | ||
|
|
8ba07812ce | ||
|
|
2dd8f35001 | ||
|
|
2d15aa88d4 | ||
|
|
e4257e50f0 | ||
|
|
33ebc07915 | ||
|
|
bc07299626 | ||
|
|
25e8aeef53 | ||
|
|
a2ed34abf3 | ||
|
|
36a7b7b91a | ||
|
|
b4e8b7375f | ||
|
|
153b635bdb | ||
|
|
80af72465e | ||
|
|
a91a8f9993 | ||
|
|
a0ccc7fe07 | ||
|
|
c162c9ae0e | ||
|
|
25542cdff3 | ||
|
|
16d0ae60c1 | ||
|
|
b1937eb8c0 | ||
|
|
3f6b468021 | ||
|
|
92d929b704 | ||
|
|
737ae75c28 | ||
|
|
79ced4eca4 | ||
|
|
329ac44c11 | ||
|
|
f65a91dfed | ||
|
|
2a79207427 | ||
|
|
70be3d10d0 | ||
|
|
3500a40599 | ||
|
|
090ffa9921 | ||
|
|
b12198fdcf | ||
|
|
826ee18666 | ||
|
|
f9d8b37b1a | ||
|
|
e626cb6b40 | ||
|
|
20697ad9e4 | ||
|
|
0800385b96 | ||
|
|
d6e326e8be | ||
|
|
8b969a6d36 | ||
|
|
d520e13c88 | ||
|
|
ae4081001c | ||
|
|
5a48b597b9 | ||
|
|
c8a953db7c | ||
|
|
d20ec144ff | ||
|
|
0147a82b0a | ||
|
|
8732a78d01 | ||
|
|
015254ae40 | ||
|
|
712bf405bb | ||
|
|
3a46a157f9 | ||
|
|
2a4ff926ae | ||
|
|
58941116c8 | ||
|
|
a13146d722 | ||
|
|
02e6f392b4 | ||
|
|
d4515bd643 | ||
|
|
a73555b7ca | ||
|
|
983d9ee1b9 | ||
|
|
e800dfe796 | ||
|
|
b0c59be340 | ||
|
|
dca12b6467 | ||
|
|
5a0d98cbd0 | ||
|
|
9cbaf22270 | ||
|
|
a64687f64f | ||
|
|
d229aab8c9 | ||
|
|
2ff94c1458 | ||
|
|
3b9f3ea81d | ||
|
|
23f12ad3cf | ||
|
|
01e7c1f183 | ||
|
|
37d60bc9b9 | ||
|
|
cd5d3903fe | ||
|
|
6904cfd224 | ||
|
|
c430e2c8f4 | ||
|
|
0cf8a94b24 | ||
|
|
ff3674aca7 | ||
|
|
b50498fa46 | ||
|
|
571f71c7f0 | ||
|
|
091c50ec84 | ||
|
|
e473dc8843 | ||
|
|
886af11d3a | ||
|
|
c72fd7ee9c | ||
|
|
7dc76b4222 | ||
|
|
5011e23050 | ||
|
|
89d9a90901 | ||
|
|
05128b12a8 | ||
|
|
c287ca9ea8 | ||
|
|
5122cda6db | ||
|
|
a39626f867 | ||
|
|
c7abae10b7 | ||
|
|
9a8f35fd8a | ||
|
|
0341cc1630 | ||
|
|
d307d233f0 | ||
|
|
5931b9625b | ||
|
|
fb837f5b97 | ||
|
|
8cfe95b3cf | ||
|
|
5fd73ac1e1 | ||
|
|
b51a574038 | ||
|
|
51b39f0775 | ||
|
|
17c4f96c94 | ||
|
|
89bf907613 | ||
|
|
641d0e45fd | ||
|
|
b3e579d8b7 | ||
|
|
fcb61c89d5 | ||
|
|
3483d78c2c | ||
|
|
36b14d0b3a | ||
|
|
2f8b68ec62 | ||
|
|
cb65cac333 | ||
|
|
d12ffc3d0d | ||
|
|
921744167e | ||
|
|
ebd96c4759 | ||
|
|
dd9876fc43 | ||
|
|
e0de614f30 | ||
|
|
30260883fb | ||
|
|
91c331e5f3 | ||
|
|
db803bcd23 | ||
|
|
cd482ea890 | ||
|
|
a2497c939a | ||
|
|
2e5249d30b | ||
|
|
b1d60c19d5 | ||
|
|
d28a82b33a | ||
|
|
787821f64b | ||
|
|
612b15fecc | ||
|
|
d88d5a1352 | ||
|
|
8584ddd00e | ||
|
|
4f572b5a10 | ||
|
|
90a0882c59 | ||
|
|
759344bb34 | ||
|
|
c9b65a3c43 | ||
|
|
b06e600946 | ||
|
|
2777751e54 | ||
|
|
b4493b2e8d | ||
|
|
55d5f6524d | ||
|
|
c7d376adf2 | ||
|
|
56bf69a770 | ||
|
|
22f9287d8b | ||
|
|
ca47d9272c | ||
|
|
0d61f5523a | ||
|
|
863ea9089a | ||
|
|
ad2af95ebd | ||
|
|
d65092c203 |
@@ -5,8 +5,7 @@ const stream = require('stream');
|
|||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const got = require('got');
|
||||||
const decompress = require('decompress');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
const decompressTargz = require('decompress-targz');
|
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
const publicDir = `${distDir}/tmp/public`;
|
const publicDir = `${distDir}/tmp/public`;
|
||||||
@@ -15,6 +14,8 @@ const outDir = `${distDir}/linux`;
|
|||||||
const tempDownloadDir = `${distDir}/tmp/download`;
|
const tempDownloadDir = `${distDir}/tmp/download`;
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const decomp = new FileDecompressor();
|
||||||
|
|
||||||
await fs.emptyDir(outDir);
|
await fs.emptyDir(outDir);
|
||||||
// перемещаем public на место
|
// перемещаем public на место
|
||||||
if (await fs.pathExists(publicDir))
|
if (await fs.pathExists(publicDir))
|
||||||
@@ -32,11 +33,7 @@ async function main() {
|
|||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
|
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
||||||
plugins: [
|
|
||||||
decompressTargz()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('files decompressed');
|
console.log('files decompressed');
|
||||||
}
|
}
|
||||||
// копируем в дистрибутив
|
// копируем в дистрибутив
|
||||||
@@ -53,11 +50,7 @@ async function main() {
|
|||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
await decompress(`${tempDownloadDir}/ipfs.tar.gz`, `${tempDownloadDir}`, {
|
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/ipfs.tar.gz`, tempDownloadDir));
|
||||||
plugins: [
|
|
||||||
decompressTargz()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('files decompressed');
|
console.log('files decompressed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
const AppCachePlugin = require('appcache-webpack-plugin');
|
||||||
|
|
||||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
@@ -32,7 +33,15 @@ module.exports = merge(baseWpConfig, {
|
|||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin(),
|
new TerserPlugin({
|
||||||
|
cache: true,
|
||||||
|
parallel: true,
|
||||||
|
terserOptions: {
|
||||||
|
output: {
|
||||||
|
comments: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
new OptimizeCSSAssetsPlugin()
|
new OptimizeCSSAssetsPlugin()
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -45,6 +54,7 @@ module.exports = merge(baseWpConfig, {
|
|||||||
template: `${clientDir}/index.html.template`,
|
template: `${clientDir}/index.html.template`,
|
||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||||
|
new AppCachePlugin({exclude: ['../index.html']})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
13
build/win.js
13
build/win.js
@@ -5,8 +5,7 @@ const stream = require('stream');
|
|||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
const got = require('got');
|
const got = require('got');
|
||||||
const decompress = require('decompress');
|
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||||
const decompressTargz = require('decompress-targz');
|
|
||||||
|
|
||||||
const distDir = path.resolve(__dirname, '../dist');
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
const publicDir = `${distDir}/tmp/public`;
|
const publicDir = `${distDir}/tmp/public`;
|
||||||
@@ -15,6 +14,8 @@ const outDir = `${distDir}/win`;
|
|||||||
const tempDownloadDir = `${distDir}/tmp/download`;
|
const tempDownloadDir = `${distDir}/tmp/download`;
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const decomp = new FileDecompressor();
|
||||||
|
|
||||||
await fs.emptyDir(outDir);
|
await fs.emptyDir(outDir);
|
||||||
// перемещаем public на место
|
// перемещаем public на место
|
||||||
if (await fs.pathExists(publicDir))
|
if (await fs.pathExists(publicDir))
|
||||||
@@ -32,11 +33,7 @@ async function main() {
|
|||||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
|
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
||||||
plugins: [
|
|
||||||
decompressTargz()
|
|
||||||
]
|
|
||||||
});
|
|
||||||
console.log('files decompressed');
|
console.log('files decompressed');
|
||||||
}
|
}
|
||||||
// копируем в дистрибутив
|
// копируем в дистрибутив
|
||||||
@@ -53,7 +50,7 @@ async function main() {
|
|||||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||||
|
|
||||||
//распаковываем
|
//распаковываем
|
||||||
await decompress(`${tempDownloadDir}/ipfs.zip`, `${tempDownloadDir}`);
|
console.log(await decomp.unpack(`${tempDownloadDir}/ipfs.zip`, tempDownloadDir));
|
||||||
console.log('files decompressed');
|
console.log('files decompressed');
|
||||||
}
|
}
|
||||||
// копируем в дистрибутив
|
// копируем в дистрибутив
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {sleep} from '../share/utils';
|
|
||||||
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api/reader'
|
baseURL: '/api/reader'
|
||||||
});
|
});
|
||||||
|
|
||||||
const workerApi = axios.create({
|
const workerApi = axios.create({
|
||||||
baseURL: '/api/worker'
|
baseURL: '/api/worker'
|
||||||
});
|
});
|
||||||
|
|
||||||
class Reader {
|
class Reader {
|
||||||
async loadBook(url, callback) {
|
async loadBook(opts, callback) {
|
||||||
const refreshPause = 300;
|
const refreshPause = 300;
|
||||||
if (!callback) callback = () => {};
|
if (!callback) callback = () => {};
|
||||||
|
|
||||||
let response = await api.post('/load-book', {type: 'url', url});
|
let response = await api.post('/load-book', opts);
|
||||||
|
|
||||||
const workerId = response.data.workerId;
|
const workerId = response.data.workerId;
|
||||||
if (!workerId)
|
if (!workerId)
|
||||||
@@ -41,7 +42,7 @@ class Reader {
|
|||||||
throw new Error(errMes);
|
throw new Error(errMes);
|
||||||
}
|
}
|
||||||
if (i > 0)
|
if (i > 0)
|
||||||
await sleep(refreshPause);
|
await utils.sleep(refreshPause);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||||
@@ -49,8 +50,9 @@ class Reader {
|
|||||||
}
|
}
|
||||||
//проверка воркера
|
//проверка воркера
|
||||||
const prevProgress = response.data.progress;
|
const prevProgress = response.data.progress;
|
||||||
|
const prevState = response.data.state;
|
||||||
response = await workerApi.post('/get-state', {workerId});
|
response = await workerApi.post('/get-state', {workerId});
|
||||||
i = (prevProgress != response.data.progress ? 1 : i);
|
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +64,13 @@ class Reader {
|
|||||||
estSize = response.headers['content-length'];
|
estSize = response.headers['content-length'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callback({state: 'loading', progress: 0});
|
||||||
const options = {
|
const options = {
|
||||||
onDownloadProgress: progress => {
|
onDownloadProgress: progress => {
|
||||||
while (progress.loaded > estSize) estSize *= 1.5;
|
while (progress.loaded > estSize) estSize *= 1.5;
|
||||||
|
|
||||||
if (callback)
|
if (callback)
|
||||||
callback({state: 'loading', progress: Math.round((progress.loaded*100)/estSize)});
|
callback({progress: Math.round((progress.loaded*100)/estSize)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//загрузка
|
//загрузка
|
||||||
@@ -105,6 +108,16 @@ class Reader {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async storage(request) {
|
||||||
|
let response = await api.post('/storage', request);
|
||||||
|
|
||||||
|
const state = response.data.state;
|
||||||
|
if (!state)
|
||||||
|
throw new Error('Неверный ответ api');
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Reader();
|
export default new Reader();
|
||||||
@@ -47,14 +47,12 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
watch: {
|
watch: {
|
||||||
rootRoute: function() {
|
|
||||||
this.setAppTitle();
|
|
||||||
this.redirectIfNeeded();
|
|
||||||
},
|
|
||||||
mode: function() {
|
mode: function() {
|
||||||
|
this.setAppTitle();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -113,13 +111,19 @@ class App extends Vue {
|
|||||||
this.dispatch('config/loadConfig');
|
this.dispatch('config/loadConfig');
|
||||||
this.$watch('apiError', function(newError) {
|
this.$watch('apiError', function(newError) {
|
||||||
if (newError) {
|
if (newError) {
|
||||||
|
let mes = newError.message;
|
||||||
|
if (newError.response && newError.response.config)
|
||||||
|
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
||||||
this.$notify.error({
|
this.$notify.error({
|
||||||
title: 'Ошибка API',
|
title: 'Ошибка API',
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
message: newError.response.config.url + '<br>' + newError.response.statusText
|
message: mes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.setAppTitle();
|
||||||
|
this.redirectIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
@@ -198,15 +202,18 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redirectIfNeeded() {
|
redirectIfNeeded() {
|
||||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
|
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
|
||||||
//старый url
|
//старый url
|
||||||
const search = window.location.search.substr(1);
|
const search = window.location.search.substr(1);
|
||||||
const url = search.split('url=')[1] || '';
|
const s = search.split('url=');
|
||||||
|
const url = s[1] || '';
|
||||||
|
const q = utils.parseQuery(s[0] || '');
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location = `/#/reader?url=${url}`;
|
q.url = decodeURIComponent(url);
|
||||||
} else {
|
|
||||||
this.$router.replace('/reader');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
this.$router.replace({ path: '/reader', query: q });
|
||||||
}
|
}
|
||||||
|
|
||||||
//yandex-метрика для omnireader
|
//yandex-метрика для omnireader
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
<template slot="header">
|
||||||
<Window @close="close">
|
Скопировать текст
|
||||||
<template slot="header">
|
</template>
|
||||||
Скопировать текст
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div ref="text" class="text" tabindex="-1">
|
<div ref="text" class="text" tabindex="-1">
|
||||||
<div v-html="text"></div>
|
<div v-html="text"></div>
|
||||||
</div>
|
|
||||||
</Window>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -109,23 +105,6 @@ class CopyTextPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
<h4>Возможности читалки:</h4>
|
<h4>Возможности читалки:</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li>загрузка любой страницы интернета</li>
|
<li>загрузка любой страницы интернета</li>
|
||||||
|
<li>работа в автономном режиме (без связи)</li>
|
||||||
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
||||||
<li>установка и запоминание текущей позиции и настроек в браузере (в будущем планируется сохранение и на сервер)</li>
|
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
||||||
|
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||||
<li>кэширование файлов книг на клиенте и на сервере</li>
|
<li>кэширование файлов книг на клиенте и на сервере</li>
|
||||||
<li>открытие книг с локального диска</li>
|
<li>открытие книг с локального диска</li>
|
||||||
<li>плавный скроллинг текста</li>
|
<li>плавный скроллинг текста</li>
|
||||||
@@ -12,16 +14,27 @@
|
|||||||
<li>поиск по тексту и копирование фрагмента</li>
|
<li>поиск по тексту и копирование фрагмента</li>
|
||||||
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
||||||
<li>управление кликом и с клавиатуры</li>
|
<li>управление кликом и с клавиатуры</li>
|
||||||
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
|
|
||||||
<li>регистрация не требуется</li>
|
<li>регистрация не требуется</li>
|
||||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>В качестве URL можно задавать html-страничку с книгой, либо прямую ссылку
|
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
|
||||||
<p>Поддерживаемые форматы: <strong>html, txt, fb2, fb2.zip</strong></p>
|
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||||
|
|
||||||
<div v-html="automationHtml"></div>
|
<div v-show="mode == 'omnireader'">
|
||||||
|
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||||
|
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||||
|
|
||||||
|
<span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||||
|
(скопировать)
|
||||||
|
</span>
|
||||||
|
<br>или перетащив на панель закладок следующую ссылку:
|
||||||
|
<br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
|
||||||
|
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||||
|
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,21 +44,25 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import {copyTextToClipboard} from '../../../../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
})
|
})
|
||||||
class CommonHelpPage extends Vue {
|
class CommonHelpPage extends Vue {
|
||||||
created() {
|
created() {
|
||||||
this.config = this.$store.state.config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get automationHtml() {
|
get mode() {
|
||||||
if (this.config.mode == 'omnireader') {
|
return this.$store.state.config.mode;
|
||||||
return `<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
}
|
||||||
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
|
|
||||||
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`;
|
async copyText(text, mes) {
|
||||||
} else {
|
const result = await copyTextToClipboard(text);
|
||||||
return '';
|
const msg = (result ? mes : 'Копирование не удалось');
|
||||||
}
|
if (result)
|
||||||
|
this.$notify.success({message: msg});
|
||||||
|
else
|
||||||
|
this.$notify.error({message: msg});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
@@ -63,4 +80,10 @@ class CommonHelpPage extends Vue {
|
|||||||
h4 {
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -53,11 +53,10 @@ class DonateHelpPage extends Vue {
|
|||||||
|
|
||||||
async copyAddress(address, prefix) {
|
async copyAddress(address, prefix) {
|
||||||
const result = await copyTextToClipboard(address);
|
const result = await copyTextToClipboard(address);
|
||||||
const msg = (result ? `${prefix}-адрес ${address} успешно скопирован в буфер обмена` : 'Копирование не удалось');
|
|
||||||
if (result)
|
if (result)
|
||||||
this.$notify.success({message: msg});
|
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
|
||||||
else
|
else
|
||||||
this.$notify.error({message: msg});
|
this.$notify.error({message: 'Копирование не удалось'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
<template slot="header">
|
||||||
<Window @close="close">
|
Справка
|
||||||
<template slot="header">
|
</template>
|
||||||
Справка
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-tabs type="border-card" v-model="selectedTab">
|
<el-tabs type="border-card" v-model="selectedTab">
|
||||||
<el-tab-pane class="tab" label="Общее">
|
<el-tab-pane class="tab" label="Общее">
|
||||||
<CommonHelpPage></CommonHelpPage>
|
<CommonHelpPage></CommonHelpPage>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="Клавиатура">
|
<el-tab-pane label="Клавиатура">
|
||||||
<HotkeysHelpPage></HotkeysHelpPage>
|
<HotkeysHelpPage></HotkeysHelpPage>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="Мышь/тачпад">
|
<el-tab-pane label="Мышь/тачпад">
|
||||||
<MouseHelpPage></MouseHelpPage>
|
<MouseHelpPage></MouseHelpPage>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="Помочь проекту" name="donate">
|
<el-tab-pane label="История версий" name="releases">
|
||||||
<DonateHelpPage></DonateHelpPage>
|
<VersionHistoryPage></VersionHistoryPage>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Помочь проекту" name="donate">
|
||||||
</el-tabs>
|
<DonateHelpPage></DonateHelpPage>
|
||||||
</Window>
|
</el-tab-pane>
|
||||||
</div>
|
</el-tabs>
|
||||||
</div>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -36,6 +34,7 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
|||||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||||
|
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -44,6 +43,7 @@ export default @Component({
|
|||||||
HotkeysHelpPage,
|
HotkeysHelpPage,
|
||||||
MouseHelpPage,
|
MouseHelpPage,
|
||||||
DonateHelpPage,
|
DonateHelpPage,
|
||||||
|
VersionHistoryPage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
class HelpPage extends Vue {
|
class HelpPage extends Vue {
|
||||||
@@ -57,6 +57,10 @@ class HelpPage extends Vue {
|
|||||||
this.selectedTab = 'donate';
|
this.selectedTab = 'donate';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activateVersionHistoryHelpPage() {
|
||||||
|
this.selectedTab = 'releases';
|
||||||
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||||
this.close();
|
this.close();
|
||||||
@@ -68,23 +72,6 @@ class HelpPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs {
|
.el-tabs {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
||||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
||||||
<li><b>X</b> - открыть недавние</li>
|
<li><b>X</b> - открыть недавние</li>
|
||||||
|
<li><b>O</b> - автономный режим</li>
|
||||||
<li><b>S</b> - открыть окно настроек</li>
|
<li><b>S</b> - открыть окно настроек</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div id="versionHistoryPage" class="page">
|
||||||
|
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
|
||||||
|
<p>
|
||||||
|
{{ item }}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<h4>История версий:</h4>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div v-for="item in versionContent" :id="item.key" :key="item.key">
|
||||||
|
<span v-html="item.content"></span>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import {versionHistory} from '../../versionHistory';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
})
|
||||||
|
class VersionHistoryPage extends Vue {
|
||||||
|
versionHeader = [];
|
||||||
|
versionContent = [];
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
let vh = [];
|
||||||
|
for (const version of versionHistory) {
|
||||||
|
vh.push(version.header);
|
||||||
|
}
|
||||||
|
this.versionHeader = vh;
|
||||||
|
|
||||||
|
let vc = [];
|
||||||
|
for (const version of versionHistory) {
|
||||||
|
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
|
||||||
|
}
|
||||||
|
this.versionContent = vc;
|
||||||
|
}
|
||||||
|
|
||||||
|
showRelease(id) {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
document.getElementById('versionHistoryPage').scrollTop = el.offsetTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 120%;
|
||||||
|
line-height: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
|
||||||
Последние 100 открытых книг
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-table
|
|
||||||
:data="tableData"
|
|
||||||
style="width: 100%"
|
|
||||||
size="mini"
|
|
||||||
height="1px"
|
|
||||||
stripe
|
|
||||||
border
|
|
||||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
|
||||||
:header-cell-style = "headerCellStyle"
|
|
||||||
:row-key = "rowKey"
|
|
||||||
>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
type="index"
|
|
||||||
width="35px"
|
|
||||||
>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
prop="touchDateTime"
|
|
||||||
min-width="90px"
|
|
||||||
sortable
|
|
||||||
>
|
|
||||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
||||||
<span style="font-size: 90%">Время<br>просм.</span>
|
|
||||||
</template>
|
|
||||||
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
||||||
<div class="desc" @click="loadBook(scope.row.url)">
|
|
||||||
{{ scope.row.touchDate }}<br>
|
|
||||||
{{ scope.row.touchTime }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
>
|
|
||||||
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
||||||
<!--el-input ref="input"
|
|
||||||
:value="search" @input="search = $event"
|
|
||||||
size="mini"
|
|
||||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
|
|
||||||
placeholder="Найти"/-->
|
|
||||||
<div class="el-input el-input--mini">
|
|
||||||
<input class="el-input__inner"
|
|
||||||
ref="input"
|
|
||||||
placeholder="Найти"
|
|
||||||
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
|
||||||
:value="search" @input="search = $event.target.value"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
min-width="300px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<div class="desc" @click="loadBook(scope.row.url)">
|
|
||||||
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
|
||||||
<span>{{ scope.row.desc.title }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
min-width="100px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
|
||||||
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column
|
|
||||||
width="60px"
|
|
||||||
>
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button
|
|
||||||
size="mini"
|
|
||||||
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
|
||||||
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
</el-table>
|
|
||||||
</Window>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
import Vue from 'vue';
|
|
||||||
import Component from 'vue-class-component';
|
|
||||||
import path from 'path';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import {formatDate} from '../../../share/utils';
|
|
||||||
import Window from '../../share/Window.vue';
|
|
||||||
import bookManager from '../share/bookManager';
|
|
||||||
|
|
||||||
export default @Component({
|
|
||||||
components: {
|
|
||||||
Window,
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search: function() {
|
|
||||||
this.updateTableData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
class HistoryPage extends Vue {
|
|
||||||
search = null;
|
|
||||||
tableData = null;
|
|
||||||
|
|
||||||
created() {
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.updateTableData();
|
|
||||||
this.mostRecentBook = bookManager.mostRecentBook();
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.input.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rowKey(row) {
|
|
||||||
return row.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTableData() {
|
|
||||||
let result = [];
|
|
||||||
|
|
||||||
const sorted = bookManager.getSortedRecent();
|
|
||||||
const len = (sorted.length < 100 ? sorted.length : 100);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const book = sorted[i];
|
|
||||||
let d = new Date();
|
|
||||||
d.setTime(book.touchTime);
|
|
||||||
const t = formatDate(d).split(' ');
|
|
||||||
|
|
||||||
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)}%]`;
|
|
||||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fb2 = (book.fb2 ? book.fb2 : {});
|
|
||||||
|
|
||||||
let title = fb2.bookTitle;
|
|
||||||
if (title)
|
|
||||||
title = `"${title}"`;
|
|
||||||
else
|
|
||||||
title = '';
|
|
||||||
|
|
||||||
let author = _.compact([
|
|
||||||
fb2.lastName,
|
|
||||||
fb2.firstName,
|
|
||||||
fb2.middleName
|
|
||||||
]).join(' ');
|
|
||||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
touchDateTime: book.touchTime,
|
|
||||||
touchDate: t[0],
|
|
||||||
touchTime: t[1],
|
|
||||||
desc: {
|
|
||||||
title: `${title}${perc}${textLen}`,
|
|
||||||
author,
|
|
||||||
},
|
|
||||||
url: book.url,
|
|
||||||
path: book.path,
|
|
||||||
key: book.key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = this.search;
|
|
||||||
result = result.filter(item => {
|
|
||||||
return !search ||
|
|
||||||
item.touchTime.includes(search) ||
|
|
||||||
item.touchDate.includes(search) ||
|
|
||||||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
|
||||||
});
|
|
||||||
|
|
||||||
this.tableData = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
headerCellStyle(cell) {
|
|
||||||
let result = {margin: 0, padding: 0};
|
|
||||||
if (cell.columnIndex > 0) {
|
|
||||||
result['border-bottom'] = 0;
|
|
||||||
}
|
|
||||||
if (cell.rowIndex > 0) {
|
|
||||||
result.height = '0px';
|
|
||||||
result['border-right'] = 0;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileNameFromPath(fb2Path) {
|
|
||||||
return path.basename(fb2Path).substr(0, 10) + '.fb2';
|
|
||||||
}
|
|
||||||
|
|
||||||
openOriginal(url) {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
openFb2(path) {
|
|
||||||
window.open(path, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleDel(key) {
|
|
||||||
await bookManager.delRecentBook({key});
|
|
||||||
this.updateTableData();
|
|
||||||
|
|
||||||
const newRecent = bookManager.mostRecentBook();
|
|
||||||
|
|
||||||
if (!(this.mostRecentBook && newRecent && this.mostRecentBook.key == newRecent.key))
|
|
||||||
this.$emit('load-book', newRecent);
|
|
||||||
|
|
||||||
this.mostRecentBook = newRecent;
|
|
||||||
if (!this.mostRecentBook)
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBook(url) {
|
|
||||||
this.$emit('load-book', {url});
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
isUrl(url) {
|
|
||||||
return (url.indexOf('file://') != 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.$emit('history-toggle');
|
|
||||||
}
|
|
||||||
|
|
||||||
keyHook(event) {
|
|
||||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
<div ref="main" class="main">
|
<div ref="main" class="main">
|
||||||
<div class="part">
|
<div class="part">
|
||||||
<span class="greeting bold-font">{{ title }}</span>
|
<span class="greeting bold-font">{{ title }}</span>
|
||||||
|
<div class="space"></div>
|
||||||
<span class="greeting">Добро пожаловать!</span>
|
<span class="greeting">Добро пожаловать!</span>
|
||||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b></span>
|
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
||||||
<span v-if="isExternalConverter" class="greeting">...а также: <b>rtf, doc, docx</b>, и вскоре: pdf, epub, mobi</span>
|
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
||||||
<span class="greeting">...и распознается сжатие: <b>zip, bz2, gz</b></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="part center">
|
<div class="part center">
|
||||||
@@ -18,14 +18,31 @@
|
|||||||
Загрузить файл с диска
|
Загрузить файл с диска
|
||||||
</el-button>
|
</el-button>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Комментарии</span>
|
<el-button size="mini" @click="loadBufferClick">
|
||||||
|
Из буфера обмена
|
||||||
|
</el-button>
|
||||||
|
<div class="space"></div>
|
||||||
|
<div class="space"></div>
|
||||||
|
<div v-if="mode == 'omnireader'" ref="yaShare2" class="ya-share2"
|
||||||
|
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
||||||
|
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
||||||
|
data-title="Omni Reader - браузерная онлайн-читалка"
|
||||||
|
data-url="https://omnireader.ru">
|
||||||
|
</div>
|
||||||
|
<div class="space"></div>
|
||||||
|
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
|
||||||
|
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="part bottom">
|
<div class="part bottom">
|
||||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||||
<span class="bottom-span">{{ version }}</span>
|
|
||||||
|
<span v-if="version == clientVersion" class="bottom-span">v{{ version }}</span>
|
||||||
|
<span v-else class="bottom-span">Версия сервера {{ version }}, версия клиента {{ clientVersion }}, необходимо обновить страницу</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -33,12 +50,18 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||||
|
import {versionHistory} from '../versionHistory';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
PasteTextPage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
class LoaderPage extends Vue {
|
class LoaderPage extends Vue {
|
||||||
bookUrl = null;
|
bookUrl = null;
|
||||||
loadPercent = 0;
|
loadPercent = 0;
|
||||||
|
pasteTextActive = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -46,6 +69,8 @@ class LoaderPage extends Vue {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.progress = this.$refs.progress;
|
this.progress = this.$refs.progress;
|
||||||
|
if (this.mode == 'omnireader')
|
||||||
|
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
|
||||||
}
|
}
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
@@ -53,7 +78,7 @@ class LoaderPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
if (this.$store.state.config.mode == 'omnireader')
|
if (this.mode == 'omnireader')
|
||||||
return 'Omni Reader - браузерная онлайн-читалка.';
|
return 'Omni Reader - браузерная онлайн-читалка.';
|
||||||
return 'Универсальная читалка книг и ресурсов интернета.';
|
return 'Универсальная читалка книг и ресурсов интернета.';
|
||||||
|
|
||||||
@@ -64,13 +89,19 @@ class LoaderPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get version() {
|
get version() {
|
||||||
return `v${this.$store.state.config.version}`;
|
return this.$store.state.config.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isExternalConverter() {
|
get isExternalConverter() {
|
||||||
return this.$store.state.config.useExternalBookConverter;
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clientVersion() {
|
||||||
|
let v = versionHistory[0].header;
|
||||||
|
v = v.split(' ')[0];
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
submitUrl() {
|
submitUrl() {
|
||||||
if (this.bookUrl) {
|
if (this.bookUrl) {
|
||||||
this.$emit('load-book', {url: this.bookUrl});
|
this.$emit('load-book', {url: this.bookUrl});
|
||||||
@@ -83,12 +114,27 @@ class LoaderPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadFile() {
|
loadFile() {
|
||||||
const file = this.$refs.file.files[0];
|
const file = this.$refs.file.files[0];
|
||||||
this.$refs.file.value = '';
|
this.$refs.file.value = '';
|
||||||
if (file)
|
if (file)
|
||||||
this.$emit('load-file', {file});
|
this.$emit('load-file', {file});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadBufferClick() {
|
||||||
|
this.pasteTextToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBuffer(opts) {
|
||||||
|
if (opts.buffer.length) {
|
||||||
|
const file = new File([opts.buffer], 'dummyName-PasteFromClipboard');
|
||||||
|
this.$emit('load-file', {file});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pasteTextToggle() {
|
||||||
|
this.pasteTextActive = !this.pasteTextActive;
|
||||||
|
}
|
||||||
|
|
||||||
openHelp() {
|
openHelp() {
|
||||||
this.$emit('help-toggle');
|
this.$emit('help-toggle');
|
||||||
}
|
}
|
||||||
@@ -101,7 +147,15 @@ class LoaderPage extends Vue {
|
|||||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openOldVersion() {
|
||||||
|
window.open('http://old.omnireader.ru', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
|
if (this.pasteTextActive) {
|
||||||
|
return this.$refs.pasteTextPage.keyHook(event);
|
||||||
|
}
|
||||||
|
|
||||||
//недостатки сторонних ui
|
//недостатки сторонних ui
|
||||||
const input = this.$refs.input.$refs.input;
|
const input = this.$refs.input.$refs.input;
|
||||||
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
||||||
@@ -130,6 +184,7 @@ class LoaderPage extends Vue {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.part {
|
.part {
|
||||||
@@ -141,8 +196,8 @@ class LoaderPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.greeting {
|
.greeting {
|
||||||
font-size: 130%;
|
font-size: 120%;
|
||||||
line-height: 170%;
|
line-height: 160%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bold-font {
|
.bold-font {
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<Window @close="close">
|
||||||
|
<template slot="header">
|
||||||
|
<span style="position: relative; top: -3px">
|
||||||
|
Вставьте текст и нажмите
|
||||||
|
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||||
|
или F2
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
|
import Window from '../../../share/Window.vue';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import * as utils from '../../../../share/utils';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class PasteTextPage extends Vue {
|
||||||
|
bookTitle = '';
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs.textArea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNonEmptyLine3words(text, count) {
|
||||||
|
let result = '';
|
||||||
|
const lines = text.split("\n");
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i].trim() != '') {
|
||||||
|
count--;
|
||||||
|
if (count <= 0) {
|
||||||
|
result = lines[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.trim().split(' ');
|
||||||
|
return result.slice(0, 3).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
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(' - ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBuffer() {
|
||||||
|
this.$emit('load-buffer', {buffer: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('paste-text-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (event.type == 'keydown') {
|
||||||
|
switch (event.code) {
|
||||||
|
case 'F2':
|
||||||
|
this.loadBuffer();
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
this.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.text {
|
||||||
|
flex: 1;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 10px 0 10px;
|
||||||
|
position: relative;
|
||||||
|
font-size: 120%;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -94,6 +94,6 @@ class ProgressPage extends Vue {
|
|||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
.el-progress__text {
|
.el-progress__text {
|
||||||
color: lightgreen;
|
color: lightgreen !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header v-show="toolBarActive" height='50px'>
|
<el-header v-show="toolBarActive" height='50px'>
|
||||||
<div class="header">
|
<div ref="header" class="header">
|
||||||
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
|
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
|
||||||
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
|
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<el-tooltip content="Действие назад" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
|
||||||
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
|
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Действие вперед" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
|
||||||
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
|
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<el-tooltip content="На весь экран" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
|
||||||
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
|
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Плавный скроллинг" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
|
||||||
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
|
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Перелистнуть" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
|
||||||
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
|
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Найти в тексте" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
|
||||||
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
|
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Скопировать текст со страницы" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
|
||||||
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
|
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
|
||||||
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
|
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
|
||||||
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
|
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<el-tooltip content="Открыть недавние" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['offlineMode']" content="Автономный режим (без интернета)" :open-delay="1000" effect="light">
|
||||||
<el-button ref="history" class="tool-button" :class="buttonActiveClass('history')" @click="buttonClick('history')"><i class="el-icon-document"></i></el-button>
|
<el-button ref="offlineMode" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"><i class="el-icon-connection"></i></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
|
||||||
|
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,12 +71,108 @@
|
|||||||
@start-text-search="startTextSearch"
|
@start-text-search="startTextSearch"
|
||||||
@stop-text-search="stopTextSearch">
|
@stop-text-search="stopTextSearch">
|
||||||
</SearchPage>
|
</SearchPage>
|
||||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
||||||
<HistoryPage v-show="historyActive" ref="historyPage" @load-book="loadBook" @history-toggle="historyToggle"></HistoryPage>
|
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
|
||||||
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
||||||
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
||||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||||
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="Что нового:"
|
||||||
|
:visible.sync="whatsNewVisible"
|
||||||
|
width="80%">
|
||||||
|
<div style="line-height: 20px" v-html="whatsNewContent"></div>
|
||||||
|
|
||||||
|
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="whatsNewDisable">Больше не показывать</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="Внимание!"
|
||||||
|
:visible.sync="migrationVisible1"
|
||||||
|
width="90%">
|
||||||
|
<div>
|
||||||
|
Появилась httpS-версия сайта по адресу <a href="https://omnireader.ru" target="_blank">https://omnireader.ru</a><br>
|
||||||
|
Работа по httpS-протоколу, помимо безопасности соединения, позволяет воспользоваться всеми возможностями
|
||||||
|
современных браузеров, а именно, применительно к нашему ресурсу:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>возможность автономной работы с читалкой (без доступа к интернету), кеширование сайта через appcache</li>
|
||||||
|
<li>безопасная передача на сервер данных о настройках и читаемых книгах при включенной синхронизации; все данные шифруются на стороне
|
||||||
|
браузера ключом доступа и никто (в т.ч. администратор) не имеет возможности их прочитать
|
||||||
|
<li>использование встроенных в JS функций шифрования и других</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Для того, чтобы перейти на новую версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
|
||||||
|
<ul>
|
||||||
|
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
|
||||||
|
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
после этого все данные будут автоматически сохранены на сервер
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
произойдет переход на https-версию читалки и откроется окно для ввода ключа
|
||||||
|
</span><br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
Старая http-версия сайта будет доступна до конца 2019 года.<br>
|
||||||
|
Приносим извинения за доставленные неудобства.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
|
||||||
|
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="Внимание!"
|
||||||
|
:visible.sync="migrationVisible2"
|
||||||
|
width="90%">
|
||||||
|
<div>
|
||||||
|
Информация для пользователей старой версии читалки по адресу <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a><br>
|
||||||
|
Для того, чтобы перейти на новую httpS-версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
|
||||||
|
<ul>
|
||||||
|
<li>перейти на старую версию ресурса <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a></li>
|
||||||
|
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
|
||||||
|
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
после этого все данные будут автоматически сохранены на сервер
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
произойдет переход на https-версию читалки и откроется окно для ввода ключа
|
||||||
|
</span><br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
Старая http-версия сайта будет доступна до конца 2019 года.<br>
|
||||||
|
Приносим извинения за доставленные неудобства.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
|
||||||
|
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
</el-main>
|
</el-main>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -81,6 +180,9 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {Buffer} from 'safe-buffer';
|
||||||
|
|
||||||
import LoaderPage from './LoaderPage/LoaderPage.vue';
|
import LoaderPage from './LoaderPage/LoaderPage.vue';
|
||||||
import TextPage from './TextPage/TextPage.vue';
|
import TextPage from './TextPage/TextPage.vue';
|
||||||
import ProgressPage from './ProgressPage/ProgressPage.vue';
|
import ProgressPage from './ProgressPage/ProgressPage.vue';
|
||||||
@@ -88,16 +190,16 @@ import ProgressPage from './ProgressPage/ProgressPage.vue';
|
|||||||
import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
|
import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
|
||||||
import SearchPage from './SearchPage/SearchPage.vue';
|
import SearchPage from './SearchPage/SearchPage.vue';
|
||||||
import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
|
import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
|
||||||
import HistoryPage from './HistoryPage/HistoryPage.vue';
|
import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
|
||||||
import SettingsPage from './SettingsPage/SettingsPage.vue';
|
import SettingsPage from './SettingsPage/SettingsPage.vue';
|
||||||
import HelpPage from './HelpPage/HelpPage.vue';
|
import HelpPage from './HelpPage/HelpPage.vue';
|
||||||
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
||||||
|
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
||||||
|
|
||||||
import bookManager from './share/bookManager';
|
import bookManager from './share/bookManager';
|
||||||
import readerApi from '../../api/reader';
|
import readerApi from '../../api/reader';
|
||||||
import _ from 'lodash';
|
import * as utils from '../../share/utils';
|
||||||
import {sleep} from '../../share/utils';
|
import {versionHistory} from './versionHistory';
|
||||||
import restoreOldSettings from './share/restoreOldSettings';
|
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -108,10 +210,11 @@ export default @Component({
|
|||||||
SetPositionPage,
|
SetPositionPage,
|
||||||
SearchPage,
|
SearchPage,
|
||||||
CopyTextPage,
|
CopyTextPage,
|
||||||
HistoryPage,
|
RecentBooksPage,
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
HelpPage,
|
HelpPage,
|
||||||
ClickMapPage,
|
ClickMapPage,
|
||||||
|
ServerStorage,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
bookPos: function(newValue) {
|
bookPos: function(newValue) {
|
||||||
@@ -138,10 +241,12 @@ export default @Component({
|
|||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
},
|
},
|
||||||
loaderActive: function(newValue) {
|
loaderActive: function(newValue) {
|
||||||
const recent = this.mostRecentBook();
|
(async() => {
|
||||||
if (!newValue && !this.loading && recent && !bookManager.hasBookParsed(recent)) {
|
const recent = this.mostRecentBook();
|
||||||
this.loadBook(recent);
|
if (!newValue && !this.loading && recent && !await bookManager.hasBookParsed(recent)) {
|
||||||
}
|
this.loadBook(recent);
|
||||||
|
}
|
||||||
|
})();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -154,7 +259,8 @@ class Reader extends Vue {
|
|||||||
setPositionActive = false;
|
setPositionActive = false;
|
||||||
searchActive = false;
|
searchActive = false;
|
||||||
copyTextActive = false;
|
copyTextActive = false;
|
||||||
historyActive = false;
|
recentBooksActive = false;
|
||||||
|
offlineModeActive = false;
|
||||||
settingsActive = false;
|
settingsActive = false;
|
||||||
helpActive = false;
|
helpActive = false;
|
||||||
clickMapActive = false;
|
clickMapActive = false;
|
||||||
@@ -163,9 +269,16 @@ class Reader extends Vue {
|
|||||||
allowUrlParamBookPos = false;
|
allowUrlParamBookPos = false;
|
||||||
showRefreshIcon = true;
|
showRefreshIcon = true;
|
||||||
mostRecentBookReactive = null;
|
mostRecentBookReactive = null;
|
||||||
|
showToolButton = {};
|
||||||
|
|
||||||
actionList = [];
|
actionList = [];
|
||||||
actionCur = -1;
|
actionCur = -1;
|
||||||
|
hidden = false;
|
||||||
|
|
||||||
|
whatsNewVisible = false;
|
||||||
|
whatsNewContent = '';
|
||||||
|
migrationVisible1 = false;
|
||||||
|
migrationVisible2 = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -197,21 +310,39 @@ class Reader extends Vue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
|
|
||||||
|
//TODO: убрать в будущем
|
||||||
|
if (this.showToolButton['history']) {
|
||||||
|
const newShowToolButton = Object.assign({}, this.showToolButton);
|
||||||
|
newShowToolButton['recentBooks'] = true;
|
||||||
|
delete newShowToolButton['history'];
|
||||||
|
const newSettings = Object.assign({}, this.settings, { showToolButton: newShowToolButton });
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.updateHeaderMinWidth();
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
await restoreOldSettings(this.settings, bookManager, this.commit);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
|
|
||||||
if (this.$root.rootRoute == '/reader') {
|
if (this.$root.rootRoute == '/reader') {
|
||||||
if (this.routeParamUrl) {
|
if (this.routeParamUrl) {
|
||||||
this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos});
|
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
|
||||||
} else {
|
} else {
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.$refs.serverStorage.init();
|
||||||
|
this.checkSetStorageAccessKey();
|
||||||
|
this.checkActivateDonateHelpPage();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
await this.showWhatsNew();
|
||||||
|
await this.showMigration();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +353,109 @@ class Reader extends Vue {
|
|||||||
this.showClickMapPage = settings.showClickMapPage;
|
this.showClickMapPage = settings.showClickMapPage;
|
||||||
this.clickControl = settings.clickControl;
|
this.clickControl = settings.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
|
this.showMigrationDialog = settings.showMigrationDialog;
|
||||||
|
this.showToolButton = settings.showToolButton;
|
||||||
|
this.enableSitesFilter = settings.enableSitesFilter;
|
||||||
|
|
||||||
|
this.updateHeaderMinWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeaderMinWidth() {
|
||||||
|
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
||||||
|
if (this.$refs.header)
|
||||||
|
this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
checkSetStorageAccessKey() {
|
||||||
|
const q = this.$route.query;
|
||||||
|
|
||||||
|
if (q['setStorageAccessKey']) {
|
||||||
|
this.$router.replace(`/reader`);
|
||||||
|
this.settingsToggle();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.settingsPage.enterServerStorageKey(
|
||||||
|
Buffer.from(utils.fromBase58(q['setStorageAccessKey'])).toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkActivateDonateHelpPage() {
|
||||||
|
const q = this.$route.query;
|
||||||
|
|
||||||
|
if (q['donate']) {
|
||||||
|
this.$router.replace(`/reader`);
|
||||||
|
this.helpToggle();
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.helpPage.activateDonateHelpPage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBookPosPercent() {
|
||||||
|
const q = this.$route.query;
|
||||||
|
if (q['__pp']) {
|
||||||
|
let pp = q['__pp'];
|
||||||
|
if (pp) {
|
||||||
|
pp = parseFloat(pp) || 0;
|
||||||
|
const recent = this.mostRecentBook();
|
||||||
|
(async() => {
|
||||||
|
await utils.sleep(100);
|
||||||
|
this.bookPos = Math.floor(recent.textLength*pp/100);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showWhatsNew() {
|
||||||
|
await utils.sleep(2000);
|
||||||
|
|
||||||
|
const whatsNew = versionHistory[0];
|
||||||
|
if (this.showWhatsNewDialog &&
|
||||||
|
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
|
||||||
|
whatsNew.header != this.whatsNewContentHash) {
|
||||||
|
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
|
||||||
|
this.whatsNewVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showMigration() {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
if (!this.settingsActive &&
|
||||||
|
this.mode == 'omnireader' && this.showMigrationDialog && this.migrationRemindDate != utils.formatDate(new Date(), 'coDate')) {
|
||||||
|
if (window.location.protocol == 'http:') {
|
||||||
|
this.migrationVisible1 = true;
|
||||||
|
} else if (window.location.protocol == 'https:') {
|
||||||
|
this.migrationVisible2 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationDialogDisable() {
|
||||||
|
this.migrationVisible1 = false;
|
||||||
|
this.migrationVisible2 = false;
|
||||||
|
if (this.showMigrationDialog) {
|
||||||
|
const newSettings = Object.assign({}, this.settings, { showMigrationDialog: false });
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationDialogRemind() {
|
||||||
|
this.migrationVisible1 = false;
|
||||||
|
this.migrationVisible2 = false;
|
||||||
|
this.commit('reader/setMigrationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||||
|
}
|
||||||
|
|
||||||
|
openVersionHistory() {
|
||||||
|
this.whatsNewVisible = false;
|
||||||
|
this.versionHistoryToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
whatsNewDisable() {
|
||||||
|
this.whatsNewVisible = false;
|
||||||
|
const whatsNew = versionHistory[0];
|
||||||
|
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
|
||||||
}
|
}
|
||||||
|
|
||||||
get routeParamPos() {
|
get routeParamPos() {
|
||||||
@@ -237,16 +471,22 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateRoute(isNewRoute) {
|
updateRoute(isNewRoute) {
|
||||||
|
if (this.loading)
|
||||||
|
return;
|
||||||
const recent = this.mostRecentBook();
|
const recent = this.mostRecentBook();
|
||||||
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
||||||
const url = (recent ? `url=${recent.url}` : '');
|
const url = (recent ? `url=${recent.url}` : '');
|
||||||
if (isNewRoute)
|
if (isNewRoute)
|
||||||
this.$router.push(`/reader?${pos}${url}`);
|
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
|
||||||
else
|
else
|
||||||
this.$router.replace(`/reader?${pos}${url}`);
|
this.$router.replace(`/reader?${pos}${url}`).catch(() => {});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
let result = '';
|
let result = '';
|
||||||
const path = this.$route.fullPath;
|
const path = this.$route.fullPath;
|
||||||
@@ -258,6 +498,11 @@ class Reader extends Vue {
|
|||||||
return decodeURIComponent(result);
|
return decodeURIComponent(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get routeParamRefresh() {
|
||||||
|
const q = this.$route.query;
|
||||||
|
return !!q['__refresh'];
|
||||||
|
}
|
||||||
|
|
||||||
bookPosChanged(event) {
|
bookPosChanged(event) {
|
||||||
if (event.bookPosSeen !== undefined)
|
if (event.bookPosSeen !== undefined)
|
||||||
this.bookPosSeen = event.bookPosSeen;
|
this.bookPosSeen = event.bookPosSeen;
|
||||||
@@ -265,6 +510,44 @@ class Reader extends Vue {
|
|||||||
this.debouncedUpdateRoute();
|
this.debouncedUpdateRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bookManagerEvent(eventName, value) {
|
||||||
|
if (eventName == 'set-recent' || eventName == 'recent-deleted') {
|
||||||
|
const oldBook = (this.textPage ? this.textPage.lastBook : null);
|
||||||
|
const oldPos = (this.textPage ? this.textPage.bookPos : null);
|
||||||
|
const newBook = bookManager.mostRecentBook();
|
||||||
|
|
||||||
|
if (!(oldBook && newBook && oldBook.key == newBook.key)) {
|
||||||
|
this.mostRecentBook();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldBook && newBook) {
|
||||||
|
if (oldBook.key != newBook.key || oldBook.path != newBook.path) {
|
||||||
|
this.loadingBook = true;
|
||||||
|
try {
|
||||||
|
await this.loadBook(newBook);
|
||||||
|
} finally {
|
||||||
|
this.loadingBook = false;
|
||||||
|
}
|
||||||
|
} else if (oldPos != newBook.bookPos) {
|
||||||
|
while (this.loadingBook) await utils.sleep(100);
|
||||||
|
this.bookPosChanged({bookPos: newBook.bookPos});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventName == 'recent-changed') {
|
||||||
|
if (this.recentBooksActive) {
|
||||||
|
await this.$refs.recentBooksPage.updateTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохранение в serverStorage
|
||||||
|
if (value) {
|
||||||
|
await utils.sleep(500);
|
||||||
|
await this.$refs.serverStorage.saveRecent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get toolBarActive() {
|
get toolBarActive() {
|
||||||
return this.reader.toolBarActive;
|
return this.reader.toolBarActive;
|
||||||
}
|
}
|
||||||
@@ -279,6 +562,14 @@ class Reader extends Vue {
|
|||||||
return this.$store.state.reader.settings;
|
return this.$store.state.reader.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get whatsNewContentHash() {
|
||||||
|
return this.$store.state.reader.whatsNewContentHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
get migrationRemindDate() {
|
||||||
|
return this.$store.state.reader.migrationRemindDate;
|
||||||
|
}
|
||||||
|
|
||||||
addAction(pos) {
|
addAction(pos) {
|
||||||
let a = this.actionList;
|
let a = this.actionList;
|
||||||
if (!a.length || a[a.length - 1] != pos) {
|
if (!a.length || a[a.length - 1] != pos) {
|
||||||
@@ -319,7 +610,7 @@ class Reader extends Vue {
|
|||||||
closeAllTextPages() {
|
closeAllTextPages() {
|
||||||
this.setPositionActive = false;
|
this.setPositionActive = false;
|
||||||
this.copyTextActive = false;
|
this.copyTextActive = false;
|
||||||
this.historyActive = false;
|
this.recentBooksActive = false;
|
||||||
this.settingsActive = false;
|
this.settingsActive = false;
|
||||||
this.stopScrolling();
|
this.stopScrolling();
|
||||||
this.stopSearch();
|
this.stopSearch();
|
||||||
@@ -340,8 +631,8 @@ class Reader extends Vue {
|
|||||||
this.setPositionActive = true;
|
this.setPositionActive = true;
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.setPositionPage.sliderMax = this.mostRecentBook().textLength - 1;
|
const recent = this.mostRecentBook();
|
||||||
this.$refs.setPositionPage.sliderValue = this.mostRecentBook().bookPos;
|
this.$refs.setPositionPage.init(recent.bookPos, recent.textLength - 1);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setPositionActive = false;
|
this.setPositionActive = false;
|
||||||
@@ -411,22 +702,31 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
historyToggle() {
|
recentBooksToggle() {
|
||||||
this.historyActive = !this.historyActive;
|
this.recentBooksActive = !this.recentBooksActive;
|
||||||
if (this.historyActive) {
|
if (this.recentBooksActive) {
|
||||||
this.closeAllTextPages();
|
this.closeAllTextPages();
|
||||||
this.$refs.historyPage.init();
|
this.$refs.recentBooksPage.init();
|
||||||
this.historyActive = true;
|
this.recentBooksActive = true;
|
||||||
} else {
|
} else {
|
||||||
this.historyActive = false;
|
this.recentBooksActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offlineModeToggle() {
|
||||||
|
this.offlineModeActive = !this.offlineModeActive;
|
||||||
|
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
|
||||||
|
}
|
||||||
|
|
||||||
settingsToggle() {
|
settingsToggle() {
|
||||||
this.settingsActive = !this.settingsActive;
|
this.settingsActive = !this.settingsActive;
|
||||||
if (this.settingsActive) {
|
if (this.settingsActive) {
|
||||||
this.closeAllTextPages();
|
this.closeAllTextPages();
|
||||||
this.settingsActive = true;
|
this.settingsActive = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.settingsPage.init();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.settingsActive = false;
|
this.settingsActive = false;
|
||||||
}
|
}
|
||||||
@@ -449,6 +749,15 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
versionHistoryToggle() {
|
||||||
|
this.helpToggle();
|
||||||
|
if (this.helpActive) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.helpPage.activateVersionHistoryHelpPage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
refreshBook() {
|
refreshBook() {
|
||||||
if (this.mostRecentBook()) {
|
if (this.mostRecentBook()) {
|
||||||
this.loadBook({url: this.mostRecentBook().url, force: true});
|
this.loadBook({url: this.mostRecentBook().url, force: true});
|
||||||
@@ -494,12 +803,15 @@ class Reader extends Vue {
|
|||||||
case 'copyText':
|
case 'copyText':
|
||||||
this.copyTextToggle();
|
this.copyTextToggle();
|
||||||
break;
|
break;
|
||||||
case 'history':
|
|
||||||
this.historyToggle();
|
|
||||||
break;
|
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
break;
|
break;
|
||||||
|
case 'recentBooks':
|
||||||
|
this.recentBooksToggle();
|
||||||
|
break;
|
||||||
|
case 'offlineMode':
|
||||||
|
this.offlineModeToggle();
|
||||||
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
this.settingsToggle();
|
this.settingsToggle();
|
||||||
break;
|
break;
|
||||||
@@ -518,7 +830,8 @@ class Reader extends Vue {
|
|||||||
case 'scrolling':
|
case 'scrolling':
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
case 'history':
|
case 'recentBooks':
|
||||||
|
case 'offlineMode':
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (this[`${button}Active`])
|
if (this[`${button}Active`])
|
||||||
classResult = classActive;
|
classResult = classActive;
|
||||||
@@ -536,7 +849,7 @@ class Reader extends Vue {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activePage == 'LoaderPage' || !this.mostRecentBook()) {
|
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case 'undoAction':
|
case 'undoAction':
|
||||||
case 'redoAction':
|
case 'redoAction':
|
||||||
@@ -546,9 +859,9 @@ class Reader extends Vue {
|
|||||||
case 'copyText':
|
case 'copyText':
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
case 'history':
|
case 'recentBooks':
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
if (!this.mostRecentBook())
|
if (!this.mostRecentBookReactive)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -584,11 +897,17 @@ class Reader extends Vue {
|
|||||||
this.$root.$emit('set-app-title');
|
this.$root.$emit('set-app-title');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// на LoaderPage всегда показываем toolBar
|
||||||
|
if (result == 'LoaderPage' && !this.toolBarActive) {
|
||||||
|
this.toolBarToggle();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastActivePage != result && result == 'TextPage') {
|
if (this.lastActivePage != result && result == 'TextPage') {
|
||||||
//акивируем страницу с текстом
|
//акивируем страницу с текстом
|
||||||
this.$nextTick(async() => {
|
this.$nextTick(async() => {
|
||||||
const last = this.mostRecentBookReactive;
|
const last = this.mostRecentBookReactive;
|
||||||
const isParsed = bookManager.hasBookParsed(last);
|
const isParsed = await bookManager.hasBookParsed(last);
|
||||||
|
|
||||||
if (!isParsed) {
|
if (!isParsed) {
|
||||||
this.$root.$emit('set-app-title');
|
this.$root.$emit('set-app-title');
|
||||||
return;
|
return;
|
||||||
@@ -597,6 +916,7 @@ class Reader extends Vue {
|
|||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
const textPage = this.$refs.page;
|
const textPage = this.$refs.page;
|
||||||
if (textPage.showBook) {
|
if (textPage.showBook) {
|
||||||
|
this.textPage = textPage;
|
||||||
textPage.lastBook = last;
|
textPage.lastBook = last;
|
||||||
textPage.bookPos = (last.bookPos !== undefined ? last.bookPos : 0);
|
textPage.bookPos = (last.bookPos !== undefined ? last.bookPos : 0);
|
||||||
|
|
||||||
@@ -609,7 +929,7 @@ class Reader extends Vue {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBook(opts) {
|
async loadBook(opts) {
|
||||||
if (!opts || !opts.url) {
|
if (!opts || !opts.url) {
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
return;
|
return;
|
||||||
@@ -621,126 +941,131 @@ class Reader extends Vue {
|
|||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
|
|
||||||
// уже просматривается сейчас
|
// уже просматривается сейчас
|
||||||
const lastBook = (this.$refs.page ? this.$refs.page.lastBook : null);
|
const lastBook = (this.textPage ? this.textPage.lastBook : null);
|
||||||
if (!opts.force && lastBook && lastBook.url == url && bookManager.hasBookParsed(lastBook)) {
|
if (!opts.force && lastBook && lastBook.url == url &&
|
||||||
|
(!opts.path || opts.path == lastBook.path) &&
|
||||||
|
await bookManager.hasBookParsed(lastBook)) {
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.progressActive = true;
|
this.progressActive = true;
|
||||||
this.$nextTick(async() => {
|
|
||||||
const progress = this.$refs.page;
|
|
||||||
|
|
||||||
this.actionList = [];
|
await this.$nextTick();
|
||||||
this.actionCur = -1;
|
|
||||||
|
|
||||||
try {
|
const progress = this.$refs.page;
|
||||||
progress.show();
|
|
||||||
progress.setState({state: 'parse'});
|
|
||||||
|
|
||||||
// есть ли среди недавних
|
this.actionList = [];
|
||||||
const key = bookManager.keyFromUrl(url);
|
this.actionCur = -1;
|
||||||
let wasOpened = await bookManager.getRecentBook({key});
|
|
||||||
wasOpened = (wasOpened ? wasOpened : {});
|
|
||||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
|
||||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
|
||||||
const bookPosPercent = wasOpened.bookPosPercent;
|
|
||||||
|
|
||||||
let book = null;
|
try {
|
||||||
|
progress.show();
|
||||||
|
progress.setState({state: 'parse'});
|
||||||
|
|
||||||
if (!opts.force) {
|
// есть ли среди недавних
|
||||||
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
const key = bookManager.keyFromUrl(url);
|
||||||
const bookParsed = await bookManager.getBook({url}, (prog) => {
|
let wasOpened = await bookManager.getRecentBook({key});
|
||||||
progress.setState({progress: prog});
|
wasOpened = (wasOpened ? wasOpened : {});
|
||||||
});
|
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||||
|
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||||
|
|
||||||
// если есть в локальном кэше
|
let book = null;
|
||||||
if (bookParsed) {
|
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, bookPosPercent}, bookParsed));
|
|
||||||
this.mostRecentBook();
|
|
||||||
this.addAction(bookPos);
|
|
||||||
this.loaderActive = false;
|
|
||||||
progress.hide(); this.progressActive = false;
|
|
||||||
this.blinkCachedLoadMessage();
|
|
||||||
|
|
||||||
await this.activateClickMapPage();
|
if (!opts.force) {
|
||||||
return;
|
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
|
||||||
}
|
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
|
||||||
|
|
||||||
// иначе идем на сервер
|
|
||||||
// пытаемся загрузить готовый файл с сервера
|
|
||||||
if (wasOpened.path) {
|
|
||||||
try {
|
|
||||||
const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => {
|
|
||||||
progress.setState(state);
|
|
||||||
});
|
|
||||||
book = Object.assign({}, wasOpened, {data: resp.data});
|
|
||||||
} catch (e) {
|
|
||||||
//молчим
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.setState({totalSteps: 5});
|
|
||||||
|
|
||||||
// не удалось, скачиваем книгу полностью с конвертацией
|
|
||||||
let loadCached = true;
|
|
||||||
if (!book) {
|
|
||||||
book = await readerApi.loadBook(url, (state) => {
|
|
||||||
progress.setState(state);
|
|
||||||
});
|
|
||||||
loadCached = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// добавляем в bookManager
|
|
||||||
progress.setState({state: 'parse', step: 5});
|
|
||||||
const addedBook = await bookManager.addBook(book, (prog) => {
|
|
||||||
progress.setState({progress: prog});
|
progress.setState({progress: prog});
|
||||||
});
|
});
|
||||||
|
|
||||||
// добавляем в историю
|
// если есть в локальном кэше
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, bookPosPercent}, addedBook));
|
if (bookParsed) {
|
||||||
this.mostRecentBook();
|
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
|
||||||
this.addAction(bookPos);
|
this.mostRecentBook();
|
||||||
this.updateRoute(true);
|
this.addAction(bookPos);
|
||||||
|
this.loaderActive = false;
|
||||||
this.loaderActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
progress.hide(); this.progressActive = false;
|
|
||||||
if (loadCached) {
|
|
||||||
this.blinkCachedLoadMessage();
|
this.blinkCachedLoadMessage();
|
||||||
} else
|
|
||||||
this.stopBlink = true;
|
|
||||||
|
|
||||||
await this.activateClickMapPage();
|
this.checkBookPosPercent();
|
||||||
} catch (e) {
|
await this.activateClickMapPage();
|
||||||
progress.hide(); this.progressActive = false;
|
return;
|
||||||
this.loaderActive = true;
|
}
|
||||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
|
||||||
|
// иначе идем на сервер
|
||||||
|
// пытаемся загрузить готовый файл с сервера
|
||||||
|
if (wasOpened.path) {
|
||||||
|
progress.setState({totalSteps: 5});
|
||||||
|
try {
|
||||||
|
const resp = await readerApi.loadCachedBook(wasOpened.path, (state) => {
|
||||||
|
progress.setState(state);
|
||||||
|
});
|
||||||
|
book = Object.assign({}, wasOpened, {data: resp.data});
|
||||||
|
} catch (e) {
|
||||||
|
//молчим
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFile(opts) {
|
progress.setState({totalSteps: 5});
|
||||||
this.progressActive = true;
|
// не удалось, скачиваем книгу полностью с конвертацией
|
||||||
this.$nextTick(async() => {
|
let loadCached = true;
|
||||||
const progress = this.$refs.page;
|
if (!book) {
|
||||||
try {
|
book = await readerApi.loadBook({url, enableSitesFilter: this.enableSitesFilter}, (state) => {
|
||||||
progress.show();
|
|
||||||
progress.setState({state: 'upload'});
|
|
||||||
|
|
||||||
const url = await readerApi.uploadFile(opts.file, this.config.maxUploadFileSize, (state) => {
|
|
||||||
progress.setState(state);
|
progress.setState(state);
|
||||||
});
|
});
|
||||||
|
loadCached = false;
|
||||||
progress.hide(); this.progressActive = false;
|
|
||||||
|
|
||||||
this.loadBook({url});
|
|
||||||
} catch (e) {
|
|
||||||
progress.hide(); this.progressActive = false;
|
|
||||||
this.loaderActive = true;
|
|
||||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// добавляем в bookManager
|
||||||
|
progress.setState({state: 'parse', step: 5});
|
||||||
|
const addedBook = await bookManager.addBook(book, (prog) => {
|
||||||
|
progress.setState({progress: prog});
|
||||||
|
});
|
||||||
|
|
||||||
|
// добавляем в историю
|
||||||
|
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook));
|
||||||
|
this.mostRecentBook();
|
||||||
|
this.addAction(bookPos);
|
||||||
|
this.updateRoute(true);
|
||||||
|
|
||||||
|
this.loaderActive = false;
|
||||||
|
progress.hide(); this.progressActive = false;
|
||||||
|
if (loadCached) {
|
||||||
|
this.blinkCachedLoadMessage();
|
||||||
|
} else
|
||||||
|
this.stopBlink = true;
|
||||||
|
|
||||||
|
this.checkBookPosPercent();
|
||||||
|
await this.activateClickMapPage();
|
||||||
|
} catch (e) {
|
||||||
|
progress.hide(); this.progressActive = false;
|
||||||
|
this.loaderActive = true;
|
||||||
|
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFile(opts) {
|
||||||
|
this.progressActive = true;
|
||||||
|
|
||||||
|
await this.$nextTick();
|
||||||
|
|
||||||
|
const progress = this.$refs.page;
|
||||||
|
try {
|
||||||
|
progress.show();
|
||||||
|
progress.setState({state: 'upload'});
|
||||||
|
|
||||||
|
const url = await readerApi.uploadFile(opts.file, this.config.maxUploadFileSize, (state) => {
|
||||||
|
progress.setState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
progress.hide(); this.progressActive = false;
|
||||||
|
|
||||||
|
await this.loadBook({url});
|
||||||
|
} catch (e) {
|
||||||
|
progress.hide(); this.progressActive = false;
|
||||||
|
this.loaderActive = true;
|
||||||
|
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blinkCachedLoadMessage() {
|
blinkCachedLoadMessage() {
|
||||||
@@ -755,9 +1080,9 @@ class Reader extends Vue {
|
|||||||
let page = this.$refs.page;
|
let page = this.$refs.page;
|
||||||
while (this.blinkCount) {
|
while (this.blinkCount) {
|
||||||
this.showRefreshIcon = !this.showRefreshIcon;
|
this.showRefreshIcon = !this.showRefreshIcon;
|
||||||
if (page.blinkCachedLoadMessage)
|
if (page && page.blinkCachedLoadMessage)
|
||||||
page.blinkCachedLoadMessage(this.showRefreshIcon);
|
page.blinkCachedLoadMessage(this.showRefreshIcon);
|
||||||
await sleep(500);
|
await utils.sleep(500);
|
||||||
if (this.stopBlink)
|
if (this.stopBlink)
|
||||||
break;
|
break;
|
||||||
this.blinkCount--;
|
this.blinkCount--;
|
||||||
@@ -765,7 +1090,7 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
this.showRefreshIcon = true;
|
this.showRefreshIcon = true;
|
||||||
this.inBlink = false;
|
this.inBlink = false;
|
||||||
if (page.blinkCachedLoadMessage)
|
if (page && page.blinkCachedLoadMessage)
|
||||||
page.blinkCachedLoadMessage('finish');
|
page.blinkCachedLoadMessage('finish');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -780,8 +1105,8 @@ class Reader extends Vue {
|
|||||||
if (!handled && this.settingsActive)
|
if (!handled && this.settingsActive)
|
||||||
handled = this.$refs.settingsPage.keyHook(event);
|
handled = this.$refs.settingsPage.keyHook(event);
|
||||||
|
|
||||||
if (!handled && this.historyActive)
|
if (!handled && this.recentBooksActive)
|
||||||
handled = this.$refs.historyPage.keyHook(event);
|
handled = this.$refs.recentBooksPage.keyHook(event);
|
||||||
|
|
||||||
if (!handled && this.setPositionActive)
|
if (!handled && this.setPositionActive)
|
||||||
handled = this.$refs.setPositionPage.keyHook(event);
|
handled = this.$refs.setPositionPage.keyHook(event);
|
||||||
@@ -831,10 +1156,13 @@ class Reader extends Vue {
|
|||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
break;
|
break;
|
||||||
case 'KeyX':
|
case 'KeyX':
|
||||||
this.historyToggle();
|
this.recentBooksToggle();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
|
case 'KeyO':
|
||||||
|
this.offlineModeToggle();
|
||||||
|
break;
|
||||||
case 'KeyS':
|
case 'KeyS':
|
||||||
this.settingsToggle();
|
this.settingsToggle();
|
||||||
break;
|
break;
|
||||||
@@ -862,11 +1190,10 @@ class Reader extends Vue {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-width: 550px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-main {
|
.el-main {
|
||||||
@@ -890,6 +1217,10 @@ class Reader extends Vue {
|
|||||||
box-shadow: 3px 3px 5px black;
|
box-shadow: 3px 3px 5px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-button + .tool-button {
|
||||||
|
margin: 0 2px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-button:hover {
|
.tool-button:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
@@ -930,4 +1261,10 @@ i {
|
|||||||
.clear {
|
.clear {
|
||||||
color: rgba(0,0,0,0);
|
color: rgba(0,0,0,0);
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
321
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
321
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
<template>
|
||||||
|
<Window width="600px" ref="window" @close="close">
|
||||||
|
<template slot="header">
|
||||||
|
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
|
||||||
|
<span v-show="loading"><i class="el-icon-loading" style="font-size: 25px"></i> <span style="position: relative; top: -4px">Список загружается</span></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="tableData"
|
||||||
|
style="width: 570px"
|
||||||
|
size="mini"
|
||||||
|
height="1px"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
||||||
|
:header-cell-style = "headerCellStyle"
|
||||||
|
:row-key = "rowKey"
|
||||||
|
>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
type="index"
|
||||||
|
width="35px"
|
||||||
|
>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="touchDateTime"
|
||||||
|
min-width="85px"
|
||||||
|
sortable
|
||||||
|
>
|
||||||
|
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||||
|
<span style="font-size: 90%">Время<br>просм.</span>
|
||||||
|
</template>
|
||||||
|
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||||
|
<div class="desc" @click="loadBook(scope.row.url)">
|
||||||
|
{{ scope.row.touchDate }}<br>
|
||||||
|
{{ scope.row.touchTime }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
>
|
||||||
|
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
||||||
|
<!--el-input ref="input"
|
||||||
|
:value="search" @input="search = $event"
|
||||||
|
size="mini"
|
||||||
|
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
|
||||||
|
placeholder="Найти"/-->
|
||||||
|
<div class="el-input el-input--mini">
|
||||||
|
<input class="el-input__inner"
|
||||||
|
ref="input"
|
||||||
|
placeholder="Найти"
|
||||||
|
style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
||||||
|
:value="search" @input="search = $event.target.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
min-width="280px"
|
||||||
|
>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div class="desc" @click="loadBook(scope.row.url)">
|
||||||
|
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
||||||
|
<span>{{ scope.row.desc.title }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
min-width="90px"
|
||||||
|
>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
||||||
|
<a :href="scope.row.path" :download="getFileNameFromPath(scope.row.path)">Скачать FB2</a>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
width="60px"
|
||||||
|
>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||||
|
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
</el-table>
|
||||||
|
</Window>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import path from 'path';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
import Window from '../../share/Window.vue';
|
||||||
|
import bookManager from '../share/bookManager';
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
components: {
|
||||||
|
Window,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search: function() {
|
||||||
|
this.updateTableData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class RecentBooksPage extends Vue {
|
||||||
|
loading = false;
|
||||||
|
search = null;
|
||||||
|
tableData = [];
|
||||||
|
|
||||||
|
created() {
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
//this.$refs.input.focus();
|
||||||
|
});
|
||||||
|
(async() => {//отбражение подгрузки списка, иначе тормозит
|
||||||
|
if (this.initing)
|
||||||
|
return;
|
||||||
|
this.initing = true;
|
||||||
|
|
||||||
|
await this.updateTableData(3);
|
||||||
|
await utils.sleep(200);
|
||||||
|
|
||||||
|
if (bookManager.loaded) {
|
||||||
|
const t = Date.now();
|
||||||
|
await this.updateTableData(10);
|
||||||
|
if (bookManager.getSortedRecent().length > 10)
|
||||||
|
await utils.sleep(10*(Date.now() - t));
|
||||||
|
} else {
|
||||||
|
let i = 0;
|
||||||
|
let j = 5;
|
||||||
|
while (i < 500 && !bookManager.loaded) {
|
||||||
|
if (i % j == 0) {
|
||||||
|
bookManager.sortedRecentCached = null;
|
||||||
|
await this.updateTableData(100);
|
||||||
|
j *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(100);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.updateTableData();
|
||||||
|
this.initing = false;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
rowKey(row) {
|
||||||
|
return row.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTableData(limit) {
|
||||||
|
while (this.updating) await utils.sleep(100);
|
||||||
|
this.updating = true;
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
this.loading = !!limit;
|
||||||
|
const sorted = bookManager.getSortedRecent();
|
||||||
|
|
||||||
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
|
const book = sorted[i];
|
||||||
|
if (book.deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (limit && result.length >= limit)
|
||||||
|
break;
|
||||||
|
|
||||||
|
let d = new Date();
|
||||||
|
d.setTime(book.touchTime);
|
||||||
|
const t = utils.formatDate(d).split(' ');
|
||||||
|
|
||||||
|
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)}%]`;
|
||||||
|
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fb2 = (book.fb2 ? book.fb2 : {});
|
||||||
|
|
||||||
|
let title = fb2.bookTitle;
|
||||||
|
if (title)
|
||||||
|
title = `"${title}"`;
|
||||||
|
else
|
||||||
|
title = '';
|
||||||
|
|
||||||
|
let author = '';
|
||||||
|
if (fb2.author) {
|
||||||
|
const authorNames = fb2.author.map(a => _.compact([
|
||||||
|
a.lastName,
|
||||||
|
a.firstName,
|
||||||
|
a.middleName
|
||||||
|
]).join(' '));
|
||||||
|
author = authorNames.join(', ');
|
||||||
|
} else {
|
||||||
|
author = _.compact([
|
||||||
|
fb2.lastName,
|
||||||
|
fb2.firstName,
|
||||||
|
fb2.middleName
|
||||||
|
]).join(' ');
|
||||||
|
}
|
||||||
|
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
touchDateTime: book.touchTime,
|
||||||
|
touchDate: t[0],
|
||||||
|
touchTime: t[1],
|
||||||
|
desc: {
|
||||||
|
title: `${title}${perc}${textLen}`,
|
||||||
|
author,
|
||||||
|
},
|
||||||
|
url: book.url,
|
||||||
|
path: book.path,
|
||||||
|
key: book.key,
|
||||||
|
});
|
||||||
|
if (result.length >= 100)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = this.search;
|
||||||
|
result = result.filter(item => {
|
||||||
|
return !search ||
|
||||||
|
item.touchTime.includes(search) ||
|
||||||
|
item.touchDate.includes(search) ||
|
||||||
|
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
||||||
|
});
|
||||||
|
|
||||||
|
/*for (let i = 0; i < result.length; i++) {
|
||||||
|
if (!_.isEqual(this.tableData[i], result[i])) {
|
||||||
|
this.$set(this.tableData, i, result[i]);
|
||||||
|
await utils.sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.tableData.length > result.length)
|
||||||
|
this.tableData.splice(result.length);*/
|
||||||
|
|
||||||
|
this.tableData = result;
|
||||||
|
this.updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerCellStyle(cell) {
|
||||||
|
let result = {margin: 0, padding: 0};
|
||||||
|
if (cell.columnIndex > 0) {
|
||||||
|
result['border-bottom'] = 0;
|
||||||
|
}
|
||||||
|
if (cell.rowIndex > 0) {
|
||||||
|
result.height = '0px';
|
||||||
|
result['border-right'] = 0;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileNameFromPath(fb2Path) {
|
||||||
|
return path.basename(fb2Path).substr(0, 10) + '.fb2';
|
||||||
|
}
|
||||||
|
|
||||||
|
openOriginal(url) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
openFb2(path) {
|
||||||
|
window.open(path, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDel(key) {
|
||||||
|
await bookManager.delRecentBook({key});
|
||||||
|
this.updateTableData();
|
||||||
|
|
||||||
|
if (!bookManager.mostRecentBook())
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBook(url) {
|
||||||
|
this.$emit('load-book', {url});
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
isUrl(url) {
|
||||||
|
if (url)
|
||||||
|
return (url.indexOf('file://') != 0);
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.$emit('recent-books-toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
keyHook(event) {
|
||||||
|
if (event.type == 'keydown' && event.code == 'Escape') {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.desc {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,28 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
<template slot="header">
|
||||||
<Window @close="close">
|
{{ header }}
|
||||||
<template slot="header">
|
</template>
|
||||||
{{ header }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||||
|
|
||||||
<div v-show="!initStep" class="input">
|
<div v-show="!initStep" class="input">
|
||||||
<input ref="input" class="el-input__inner"
|
<input ref="input" class="el-input__inner"
|
||||||
placeholder="что ищем"
|
placeholder="что ищем"
|
||||||
:value="needle" @input="needle = $event.target.value"/>
|
:value="needle" @input="needle = $event.target.value"/>
|
||||||
<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>
|
</div>
|
||||||
<el-button-group v-show="!initStep" class="button-group">
|
<el-button-group v-show="!initStep" class="button-group">
|
||||||
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
||||||
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
||||||
</el-button-group>
|
</el-button-group>
|
||||||
</div>
|
|
||||||
</Window>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -61,6 +57,8 @@ class SearchPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(parsed) {
|
async init(parsed) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
if (this.parsed != parsed) {
|
if (this.parsed != parsed) {
|
||||||
this.initStep = true;
|
this.initStep = true;
|
||||||
this.stopInit = false;
|
this.stopInit = false;
|
||||||
@@ -178,32 +176,13 @@ class SearchPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
height: 125px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
min-width: 430px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|||||||
628
client/components/Reader/ServerStorage/ServerStorage.vue
Normal file
628
client/components/Reader/ServerStorage/ServerStorage.vue
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Component from 'vue-class-component';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import bookManager from '../share/bookManager';
|
||||||
|
import readerApi from '../../../api/reader';
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||||
|
|
||||||
|
import localForage from 'localforage';
|
||||||
|
const ssCacheStore = localForage.createInstance({
|
||||||
|
name: 'ssCacheStore'
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @Component({
|
||||||
|
watch: {
|
||||||
|
serverSyncEnabled: function() {
|
||||||
|
this.serverSyncEnabledChanged();
|
||||||
|
},
|
||||||
|
serverStorageKey: function() {
|
||||||
|
this.serverStorageKeyChanged(true);
|
||||||
|
},
|
||||||
|
settings: function() {
|
||||||
|
this.debouncedSaveSettings();
|
||||||
|
},
|
||||||
|
profiles: function() {
|
||||||
|
this.saveProfiles();
|
||||||
|
},
|
||||||
|
currentProfile: function() {
|
||||||
|
this.currentProfileChanged(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class ServerStorage extends Vue {
|
||||||
|
created() {
|
||||||
|
this.inited = false;
|
||||||
|
this.keyInited = false;
|
||||||
|
this.commit = this.$store.commit;
|
||||||
|
this.prevServerStorageKey = null;
|
||||||
|
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
|
||||||
|
|
||||||
|
this.debouncedSaveSettings = _.debounce(() => {
|
||||||
|
this.saveSettings();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
this.debouncedNotifySuccess = _.debounce(() => {
|
||||||
|
this.success('Данные синхронизированы с сервером');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
this.oldProfiles = {};
|
||||||
|
this.oldSettings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
this.cachedRecent = await ssCacheStore.getItem('recent');
|
||||||
|
if (!this.cachedRecent)
|
||||||
|
await this.setCachedRecent({rev: 0, data: {}});
|
||||||
|
|
||||||
|
this.cachedRecentPatch = await ssCacheStore.getItem('recent-patch');
|
||||||
|
if (!this.cachedRecentPatch)
|
||||||
|
await this.setCachedRecentPatch({rev: 0, data: {}});
|
||||||
|
|
||||||
|
this.cachedRecentMod = await ssCacheStore.getItem('recent-mod');
|
||||||
|
if (!this.cachedRecentMod)
|
||||||
|
await this.setCachedRecentMod({rev: 0, data: {}});
|
||||||
|
|
||||||
|
if (!this.serverStorageKey) {
|
||||||
|
//генерируем новый ключ
|
||||||
|
await this.generateNewServerStorageKey();
|
||||||
|
} else {
|
||||||
|
await this.serverStorageKeyChanged();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedRecent(value) {
|
||||||
|
await ssCacheStore.setItem('recent', value);
|
||||||
|
this.cachedRecent = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedRecentPatch(value) {
|
||||||
|
await ssCacheStore.setItem('recent-patch', value);
|
||||||
|
this.cachedRecentPatch = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCachedRecentMod(value) {
|
||||||
|
await ssCacheStore.setItem('recent-mod', value);
|
||||||
|
this.cachedRecentMod = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateNewServerStorageKey() {
|
||||||
|
const key = utils.toBase58(utils.randomArray(32));
|
||||||
|
this.commit('reader/setServerStorageKey', key);
|
||||||
|
await this.serverStorageKeyChanged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async serverSyncEnabledChanged() {
|
||||||
|
if (this.serverSyncEnabled) {
|
||||||
|
this.prevServerStorageKey = null;
|
||||||
|
if (!this.serverStorageKey) {
|
||||||
|
//генерируем новый ключ
|
||||||
|
await this.generateNewServerStorageKey();
|
||||||
|
} else {
|
||||||
|
await this.serverStorageKeyChanged(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async serverStorageKeyChanged(force) {
|
||||||
|
if (this.prevServerStorageKey != this.serverStorageKey) {
|
||||||
|
this.prevServerStorageKey = this.serverStorageKey;
|
||||||
|
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
|
||||||
|
this.keyInited = true;
|
||||||
|
|
||||||
|
await this.loadProfiles(force);
|
||||||
|
this.checkCurrentProfile();
|
||||||
|
await this.currentProfileChanged(force);
|
||||||
|
const loadSuccess = await this.loadRecent();
|
||||||
|
if (loadSuccess && force)
|
||||||
|
await this.saveRecent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async currentProfileChanged(force) {
|
||||||
|
if (!this.currentProfile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.loadSettings(force);
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverSyncEnabled() {
|
||||||
|
return this.$store.state.reader.serverSyncEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settings() {
|
||||||
|
return this.$store.state.reader.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
get settingsRev() {
|
||||||
|
return this.$store.state.reader.settingsRev;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serverStorageKey() {
|
||||||
|
return this.$store.state.reader.serverStorageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get profiles() {
|
||||||
|
return this.$store.state.reader.profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
get profilesRev() {
|
||||||
|
return this.$store.state.reader.profilesRev;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentProfile() {
|
||||||
|
return this.$store.state.reader.currentProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showServerStorageMessages() {
|
||||||
|
return this.settings.showServerStorageMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCurrentProfile() {
|
||||||
|
if (!this.profiles[this.currentProfile]) {
|
||||||
|
this.commit('reader/setCurrentProfile', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message) {
|
||||||
|
if (this.showServerStorageMessages)
|
||||||
|
this.$notify.success({message});
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(message) {
|
||||||
|
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||||
|
this.$notify.warning({message});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message) {
|
||||||
|
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||||
|
this.$notify.error({message});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings(force = false, doNotifySuccess = true) {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const setsId = `settings-${this.currentProfile}`;
|
||||||
|
const oldRev = this.settingsRev[setsId] || 0;
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
if (!force) {
|
||||||
|
try {
|
||||||
|
const revs = await this.storageCheck({[setsId]: {}});
|
||||||
|
if (revs.state == 'success' && revs.items[setsId].rev == oldRev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sets = null;
|
||||||
|
try {
|
||||||
|
sets = await this.storageGet({[setsId]: {}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sets.state == 'success') {
|
||||||
|
sets = sets.items[setsId];
|
||||||
|
|
||||||
|
if (sets.rev == 0)
|
||||||
|
sets.data = {};
|
||||||
|
|
||||||
|
this.oldSettings = _.cloneDeep(sets.data);
|
||||||
|
this.commit('reader/setSettings', sets.data);
|
||||||
|
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${sets.state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const diff = utils.getObjDiff(this.oldSettings, this.settings);
|
||||||
|
if (utils.isEmptyObjDiff(diff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.savingSettings = true;
|
||||||
|
try {
|
||||||
|
const setsId = `settings-${this.currentProfile}`;
|
||||||
|
let result = {state: ''};
|
||||||
|
|
||||||
|
const oldRev = this.settingsRev[setsId] || 0;
|
||||||
|
try {
|
||||||
|
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
await this.loadSettings(true, false);
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
this.oldSettings = _.cloneDeep(this.settings);
|
||||||
|
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.savingSettings = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProfiles(force = false, doNotifySuccess = true) {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const oldRev = this.profilesRev;
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
if (!force) {
|
||||||
|
try {
|
||||||
|
const revs = await this.storageCheck({profiles: {}});
|
||||||
|
if (revs.state == 'success' && revs.items.profiles.rev == oldRev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prof = null;
|
||||||
|
try {
|
||||||
|
prof = await this.storageGet({profiles: {}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prof.state == 'success') {
|
||||||
|
prof = prof.items.profiles;
|
||||||
|
|
||||||
|
if (prof.rev == 0)
|
||||||
|
prof.data = {};
|
||||||
|
|
||||||
|
this.oldProfiles = _.cloneDeep(prof.data);
|
||||||
|
this.commit('reader/setProfiles', prof.data);
|
||||||
|
this.commit('reader/setProfilesRev', prof.rev);
|
||||||
|
this.checkCurrentProfile();
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${prof.state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveProfiles() {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
|
||||||
|
if (utils.isEmptyObjDiff(diff))
|
||||||
|
return;
|
||||||
|
|
||||||
|
//обнуляются профили во время разработки при hotReload, подстраховка
|
||||||
|
if (!this.$store.state.reader.allowProfilesSave) {
|
||||||
|
console.error('Сохранение профилей не санкционировано');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savingProfiles = true;
|
||||||
|
try {
|
||||||
|
let result = {state: ''};
|
||||||
|
try {
|
||||||
|
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
await this.loadProfiles(true, false);
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
this.oldProfiles = _.cloneDeep(this.profiles);
|
||||||
|
this.commit('reader/setProfilesRev', this.profilesRev + 1);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.savingProfiles = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadRecent(skipRevCheck = false, doNotifySuccess = true) {
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
|
||||||
|
return;
|
||||||
|
this.loadingRecent = true;
|
||||||
|
try {
|
||||||
|
//проверим ревизию на сервере
|
||||||
|
let query = {recent: {}, recentPatch: {}, recentMod: {}};
|
||||||
|
let revs = null;
|
||||||
|
if (!skipRevCheck) {
|
||||||
|
try {
|
||||||
|
revs = await this.storageCheck(query);
|
||||||
|
if (revs.state == 'success') {
|
||||||
|
if (revs.items.recent.rev != this.cachedRecent.rev) {
|
||||||
|
//no changes
|
||||||
|
} else if (revs.items.recentPatch.rev != this.cachedRecentPatch.rev) {
|
||||||
|
query = {recentPatch: {}, recentMod: {}};
|
||||||
|
} else if (revs.items.recentMod.rev != this.cachedRecentMod.rev) {
|
||||||
|
query = {recentMod: {}};
|
||||||
|
} else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let recent = null;
|
||||||
|
try {
|
||||||
|
recent = await this.storageGet(query);
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recent.state == 'success') {
|
||||||
|
let newRecent = recent.items.recent;
|
||||||
|
let newRecentPatch = recent.items.recentPatch;
|
||||||
|
let newRecentMod = recent.items.recentMod;
|
||||||
|
|
||||||
|
if (!newRecent) {
|
||||||
|
newRecent = _.cloneDeep(this.cachedRecent);
|
||||||
|
}
|
||||||
|
if (!newRecentPatch) {
|
||||||
|
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||||
|
}
|
||||||
|
if (!newRecentMod) {
|
||||||
|
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRecent.rev == 0) newRecent.data = {};
|
||||||
|
if (newRecentPatch.rev == 0) newRecentPatch.data = {};
|
||||||
|
if (newRecentMod.rev == 0) newRecentMod.data = {};
|
||||||
|
|
||||||
|
let result = Object.assign({}, newRecent.data, newRecentPatch.data);
|
||||||
|
|
||||||
|
const md = newRecentMod.data;
|
||||||
|
if (md.key && result[md.key])
|
||||||
|
result[md.key] = utils.applyObjDiff(result[md.key], md.mod);
|
||||||
|
|
||||||
|
if (!bookManager.loaded) {
|
||||||
|
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||||
|
while (!bookManager.loaded) await utils.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRecent.rev != this.cachedRecent.rev)
|
||||||
|
await this.setCachedRecent(newRecent);
|
||||||
|
if (newRecentPatch.rev != this.cachedRecentPatch.rev)
|
||||||
|
await this.setCachedRecentPatch(newRecentPatch);
|
||||||
|
if (newRecentMod.rev != this.cachedRecentMod.rev)
|
||||||
|
await this.setCachedRecentMod(newRecentMod);
|
||||||
|
|
||||||
|
await bookManager.setRecent(result);
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
|
} finally {
|
||||||
|
this.loadingRecent = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveRecent(itemKey, recurse) {
|
||||||
|
while (!this.inited || this.savingRecent)
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
|
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.savingRecent = true;
|
||||||
|
try {
|
||||||
|
const bm = bookManager;
|
||||||
|
|
||||||
|
let needSaveRecent = false;
|
||||||
|
let needSaveRecentPatch = false;
|
||||||
|
let needSaveRecentMod = false;
|
||||||
|
|
||||||
|
//newRecentMod
|
||||||
|
let newRecentMod = {};
|
||||||
|
if (itemKey && this.cachedRecentPatch.data[itemKey] && this.prevItemKey == itemKey) {
|
||||||
|
newRecentMod = _.cloneDeep(this.cachedRecentMod);
|
||||||
|
newRecentMod.rev++;
|
||||||
|
|
||||||
|
newRecentMod.data.key = itemKey;
|
||||||
|
newRecentMod.data.mod = utils.getObjDiff(this.cachedRecentPatch.data[itemKey], bm.recent[itemKey]);
|
||||||
|
needSaveRecentMod = true;
|
||||||
|
}
|
||||||
|
this.prevItemKey = itemKey;
|
||||||
|
|
||||||
|
//newRecentPatch
|
||||||
|
let newRecentPatch = {};
|
||||||
|
if (itemKey && !needSaveRecentMod) {
|
||||||
|
newRecentPatch = _.cloneDeep(this.cachedRecentPatch);
|
||||||
|
newRecentPatch.rev++;
|
||||||
|
newRecentPatch.data[itemKey] = bm.recent[itemKey];
|
||||||
|
|
||||||
|
let applyMod = this.cachedRecentMod.data;
|
||||||
|
if (applyMod && applyMod.key && newRecentPatch.data[applyMod.key])
|
||||||
|
newRecentPatch.data[applyMod.key] = utils.applyObjDiff(newRecentPatch.data[applyMod.key], applyMod.mod);
|
||||||
|
|
||||||
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
|
needSaveRecentPatch = true;
|
||||||
|
needSaveRecentMod = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//newRecent
|
||||||
|
let newRecent = {};
|
||||||
|
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
|
//ждем весь bm.recent
|
||||||
|
while (!bookManager.loaded)
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
|
newRecent = {rev: this.cachedRecent.rev + 1, data: bm.recent};
|
||||||
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
|
newRecentMod = {rev: this.cachedRecentMod.rev + 1, data: {}};
|
||||||
|
needSaveRecent = true;
|
||||||
|
needSaveRecentPatch = true;
|
||||||
|
needSaveRecentMod = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//query
|
||||||
|
let query = {};
|
||||||
|
if (needSaveRecent) {
|
||||||
|
query = {recent: newRecent, recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||||
|
} else if (needSaveRecentPatch) {
|
||||||
|
query = {recentPatch: newRecentPatch, recentMod: newRecentMod};
|
||||||
|
} else {
|
||||||
|
query = {recentMod: newRecentMod};
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохранение
|
||||||
|
let result = {state: ''};
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await this.storageSet(query);
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
|
||||||
|
await this.loadRecent(false, false);
|
||||||
|
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
if (!recurse && itemKey) {
|
||||||
|
this.savingRecent = false;
|
||||||
|
this.saveRecent(itemKey, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
if (needSaveRecent && newRecent.rev)
|
||||||
|
await this.setCachedRecent(newRecent);
|
||||||
|
if (needSaveRecentPatch && newRecentPatch.rev)
|
||||||
|
await this.setCachedRecentPatch(newRecentPatch);
|
||||||
|
if (needSaveRecentMod && newRecentMod.rev)
|
||||||
|
await this.setCachedRecentMod(newRecentMod);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.savingRecent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageCheck(items) {
|
||||||
|
return await this.storageApi('check', items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageGet(items) {
|
||||||
|
return await this.storageApi('get', items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageSet(items, force) {
|
||||||
|
return await this.storageApi('set', items, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storageApi(action, items, force) {
|
||||||
|
const request = {action, items};
|
||||||
|
if (force)
|
||||||
|
request.force = true;
|
||||||
|
const encodedRequest = await this.encodeStorageItems(request);
|
||||||
|
return await this.decodeStorageItems(await readerApi.storage(encodedRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
async encodeStorageItems(request) {
|
||||||
|
if (!this.hashedStorageKey)
|
||||||
|
throw new Error('hashedStorageKey is empty');
|
||||||
|
|
||||||
|
if (!_.isObject(request.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
|
let result = Object.assign({}, request);
|
||||||
|
let items = {};
|
||||||
|
for (const id of Object.keys(request.items)) {
|
||||||
|
const item = request.items[id];
|
||||||
|
if (request.action == 'set' && !_.isObject(item.data))
|
||||||
|
throw new Error('encodeStorageItems: data is not an object');
|
||||||
|
|
||||||
|
let encoded = Object.assign({}, item);
|
||||||
|
|
||||||
|
if (item.data) {
|
||||||
|
const comp = utils.pako.deflate(JSON.stringify(item.data), {level: 1});
|
||||||
|
let encrypted = null;
|
||||||
|
try {
|
||||||
|
encrypted = cryptoUtils.aesEncrypt(comp, this.serverStorageKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('encrypt failed');
|
||||||
|
}
|
||||||
|
encoded.data = '1' + utils.toBase64(encrypted);
|
||||||
|
}
|
||||||
|
items[`${this.hashedStorageKey}.${utils.toBase58(id)}`] = encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.items = items;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decodeStorageItems(response) {
|
||||||
|
if (!this.hashedStorageKey)
|
||||||
|
throw new Error('hashedStorageKey is empty');
|
||||||
|
|
||||||
|
let result = Object.assign({}, response);
|
||||||
|
let items = {};
|
||||||
|
if (response.items) {
|
||||||
|
if (!_.isObject(response.items))
|
||||||
|
throw new Error('items is not an object');
|
||||||
|
|
||||||
|
for (const id of Object.keys(response.items)) {
|
||||||
|
const item = response.items[id];
|
||||||
|
let decoded = Object.assign({}, item);
|
||||||
|
if (item.data) {
|
||||||
|
if (!_.isString(item.data) || !item.data.length)
|
||||||
|
throw new Error('decodeStorageItems: data is not a string');
|
||||||
|
if (item.data[0] !== '1')
|
||||||
|
throw new Error('decodeStorageItems: unknown data format');
|
||||||
|
|
||||||
|
const a = utils.fromBase64(item.data.substr(1));
|
||||||
|
let decrypted = null;
|
||||||
|
try {
|
||||||
|
decrypted = cryptoUtils.aesDecrypt(a, this.serverStorageKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('decrypt failed');
|
||||||
|
}
|
||||||
|
decoded.data = JSON.parse(utils.pako.inflate(decrypted, {to: 'string'}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = id.split('.');
|
||||||
|
if (!(ids.length == 2) || !(ids[0] == this.hashedStorageKey))
|
||||||
|
throw new Error(`decodeStorageItems: bad id - ${id}`);
|
||||||
|
items[utils.fromBase58(ids[1])] = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.items = items;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
</script>
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
<template slot="header">
|
||||||
<Window @close="close">
|
Установить позицию
|
||||||
<template slot="header">
|
</template>
|
||||||
Установить позицию
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="slider">
|
<div class="slider">
|
||||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||||
</div>
|
|
||||||
</Window>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Window>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
|
|
||||||
@@ -28,7 +23,8 @@ export default @Component({
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
sliderValue: function(newValue) {
|
sliderValue: function(newValue) {
|
||||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
if (this.initialized)
|
||||||
|
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -39,6 +35,15 @@ class SetPositionPage extends Vue {
|
|||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.reader = this.$store.state.reader;
|
this.reader = this.$store.state.reader;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(sliderValue, sliderMax) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
|
this.sliderMax = sliderMax;
|
||||||
|
this.sliderValue = sliderValue;
|
||||||
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTooltip(val) {
|
formatTooltip(val) {
|
||||||
@@ -63,26 +68,6 @@ class SetPositionPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
height: 140px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ export default class DrawHelper {
|
|||||||
|
|
||||||
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` +
|
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` +
|
||||||
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
||||||
` line-height: ${this.lineHeight}px; white-space: nowrap; overflow: hidden;">`;
|
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
|
||||||
|
|
||||||
let imageDrawn = new Set();
|
let imageDrawn = new Set();
|
||||||
let len = lines.length;
|
let len = lines.length;
|
||||||
@@ -317,4 +317,56 @@ export default class DrawHelper {
|
|||||||
await animation1Finish(duration);
|
await animation1Finish(duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async doPageAnimationRotate(page1, page2, duration, isDown, animation1Finish, animation2Finish) {
|
||||||
|
if (isDown) {
|
||||||
|
page1.style.transform = `rotateY(90deg)`;
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration/2}ms ease-in`;
|
||||||
|
page2.style.transform = `rotateY(-90deg)`;
|
||||||
|
|
||||||
|
await animation2Finish(duration/2);
|
||||||
|
|
||||||
|
page1.style.transition = `${duration/2}ms ease-out`;
|
||||||
|
page1.style.transform = `rotateY(0deg)`;
|
||||||
|
await animation1Finish(duration/2);
|
||||||
|
} else {
|
||||||
|
page1.style.transform = `rotateY(-90deg)`;
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration/2}ms ease-in`;
|
||||||
|
page2.style.transform = `rotateY(90deg)`;
|
||||||
|
|
||||||
|
await animation2Finish(duration/2);
|
||||||
|
|
||||||
|
page1.style.transition = `${duration/2}ms ease-out`;
|
||||||
|
page1.style.transform = `rotateY(0deg)`;
|
||||||
|
await animation1Finish(duration/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doPageAnimationFlip(page1, page2, duration, isDown, animation1Finish, animation2Finish, backgroundColor) {
|
||||||
|
page2.style.background = backgroundColor;
|
||||||
|
|
||||||
|
if (isDown) {
|
||||||
|
page2.style.transformOrigin = '5%';
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration}ms ease-in-out`;
|
||||||
|
page2.style.transform = `rotateY(-120deg) translateX(${this.w/4}px)`;
|
||||||
|
await animation2Finish(duration);
|
||||||
|
} else {
|
||||||
|
page2.style.transformOrigin = '95%';
|
||||||
|
await sleep(30);
|
||||||
|
|
||||||
|
page2.style.transition = `${duration}ms ease-in-out`;
|
||||||
|
page2.style.transform = `rotateY(120deg) translateX(-${this.w/4}px)`;
|
||||||
|
await animation2Finish(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
page2.style.transformOrigin = 'center';
|
||||||
|
page2.style.background = '';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,13 @@
|
|||||||
<div v-html="background"></div>
|
<div v-html="background"></div>
|
||||||
<!-- img -->
|
<!-- img -->
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox1" class="layout" style="overflow: hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage1" class="layout" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||||
<div v-html="page1"></div>
|
<div v-html="page1"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox2" class="layout" style="overflow: hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage2" class="layout" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
||||||
<div v-html="page2"></div>
|
<div v-html="page2"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
oncontextmenu="return false;">
|
oncontextmenu="return false;">
|
||||||
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"></div>
|
@click.prevent.stop="onStatusBarClick"></div>
|
||||||
<div v-show="fontsLoading" ref="fontsLoading"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick"></div>
|
@click.prevent.stop="onStatusBarClick"></div>
|
||||||
@@ -77,7 +76,6 @@ class TextPage extends Vue {
|
|||||||
page2 = null;
|
page2 = null;
|
||||||
statusBar = null;
|
statusBar = null;
|
||||||
statusBarClickable = null;
|
statusBarClickable = null;
|
||||||
fontsLoading = null;
|
|
||||||
|
|
||||||
lastBook = null;
|
lastBook = null;
|
||||||
bookPos = 0;
|
bookPos = 0;
|
||||||
@@ -133,7 +131,6 @@ class TextPage extends Vue {
|
|||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
||||||
this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -157,9 +154,10 @@ class TextPage extends Vue {
|
|||||||
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
|
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
|
||||||
|
|
||||||
this.w = this.realWidth - 2*this.indentLR;
|
this.w = this.realWidth - 2*this.indentLR;
|
||||||
this.h = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0) - 2*this.indentTB;
|
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
|
||||||
|
this.h = this.scrollHeight - 2*this.indentTB;
|
||||||
this.lineHeight = this.fontSize + this.lineInterval;
|
this.lineHeight = this.fontSize + this.lineInterval;
|
||||||
this.pageLineCount = 1 + Math.floor((this.h - this.fontSize)/this.lineHeight);
|
this.pageLineCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
|
||||||
|
|
||||||
this.$refs.scrollingPage1.style.width = this.w + 'px';
|
this.$refs.scrollingPage1.style.width = this.w + 'px';
|
||||||
this.$refs.scrollingPage2.style.width = this.w + 'px';
|
this.$refs.scrollingPage2.style.width = this.w + 'px';
|
||||||
@@ -170,6 +168,12 @@ class TextPage extends Vue {
|
|||||||
this.fontShift = this.fontVertShift/100;
|
this.fontShift = this.fontVertShift/100;
|
||||||
this.textShift = this.textVertShift/100 + this.fontShift;
|
this.textShift = this.textVertShift/100 + this.fontShift;
|
||||||
|
|
||||||
|
//statusBar
|
||||||
|
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);
|
||||||
|
|
||||||
//drawHelper
|
//drawHelper
|
||||||
this.drawHelper.realWidth = this.realWidth;
|
this.drawHelper.realWidth = this.realWidth;
|
||||||
this.drawHelper.realHeight = this.realHeight;
|
this.drawHelper.realHeight = this.realHeight;
|
||||||
@@ -195,14 +199,8 @@ class TextPage extends Vue {
|
|||||||
this.drawHelper.lineHeight = this.lineHeight;
|
this.drawHelper.lineHeight = this.lineHeight;
|
||||||
this.drawHelper.context = this.context;
|
this.drawHelper.context = this.context;
|
||||||
|
|
||||||
//сообщение "Загрузка шрифтов..."
|
//statusBar
|
||||||
const flText = 'Загрузка шрифта...';
|
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
||||||
this.$refs.fontsLoading.innerHTML = flText;
|
|
||||||
const fontsLoadingStyle = this.$refs.fontsLoading.style;
|
|
||||||
fontsLoadingStyle.position = 'absolute';
|
|
||||||
fontsLoadingStyle.fontSize = this.fontSize + 'px';
|
|
||||||
fontsLoadingStyle.top = (this.realHeight/2 - 2*this.fontSize) + 'px';
|
|
||||||
fontsLoadingStyle.left = (this.realWidth - this.drawHelper.measureText(flText, {}))/2 + 'px';
|
|
||||||
|
|
||||||
//parsed
|
//parsed
|
||||||
if (this.parsed) {
|
if (this.parsed) {
|
||||||
@@ -222,30 +220,35 @@ class TextPage extends Vue {
|
|||||||
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
||||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||||
|
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||||
}
|
}
|
||||||
|
|
||||||
//statusBar
|
|
||||||
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);
|
|
||||||
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
|
||||||
|
|
||||||
//scrolling page
|
//scrolling page
|
||||||
const pageDelta = this.h - (this.pageLineCount*this.lineHeight - this.lineInterval);
|
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
|
||||||
let y = this.indentTB + pageDelta/2;
|
let y = pageSpace/2;
|
||||||
if (this.showStatusBar)
|
if (this.showStatusBar)
|
||||||
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||||
const page1 = this.$refs.scrollBox1;
|
let page1 = this.$refs.scrollBox1;
|
||||||
const page2 = this.$refs.scrollBox2;
|
let page2 = this.$refs.scrollBox2;
|
||||||
page1.style.width = this.w + 'px';
|
|
||||||
page2.style.width = this.w + 'px';
|
page1.style.perspective = '3072px';
|
||||||
page1.style.height = (this.h - pageDelta) + 'px';
|
page2.style.perspective = '3072px';
|
||||||
page2.style.height = (this.h - pageDelta) + 'px';
|
|
||||||
|
page1.style.width = this.w + this.indentLR + 'px';
|
||||||
|
page2.style.width = this.w + this.indentLR + 'px';
|
||||||
|
page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||||
|
page2.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||||
page1.style.top = y + 'px';
|
page1.style.top = y + 'px';
|
||||||
page2.style.top = y + 'px';
|
page2.style.top = y + 'px';
|
||||||
page1.style.left = this.indentLR + 'px';
|
page1.style.left = this.indentLR + 'px';
|
||||||
page2.style.left = this.indentLR + 'px';
|
page2.style.left = this.indentLR + 'px';
|
||||||
|
|
||||||
|
page1 = this.$refs.scrollingPage1;
|
||||||
|
page2 = this.$refs.scrollingPage2;
|
||||||
|
page1.style.width = this.w + this.indentLR + 'px';
|
||||||
|
page2.style.width = this.w + this.indentLR + 'px';
|
||||||
|
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||||
|
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkLoadedFonts() {
|
async checkLoadedFonts() {
|
||||||
@@ -260,6 +263,18 @@ class TextPage extends Vue {
|
|||||||
async loadFonts() {
|
async loadFonts() {
|
||||||
this.fontsLoading = true;
|
this.fontsLoading = true;
|
||||||
|
|
||||||
|
let inst = null;
|
||||||
|
(async() => {
|
||||||
|
await sleep(500);
|
||||||
|
if (this.fontsLoading)
|
||||||
|
inst = this.$notify({
|
||||||
|
title: '',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
message: 'Загрузка шрифта <i class="el-icon-loading"></i>',
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
if (!this.fontsLoaded)
|
if (!this.fontsLoaded)
|
||||||
this.fontsLoaded = {};
|
this.fontsLoaded = {};
|
||||||
//загрузка дин.шрифта
|
//загрузка дин.шрифта
|
||||||
@@ -290,6 +305,8 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.fontsLoading = false;
|
this.fontsLoading = false;
|
||||||
|
if (inst)
|
||||||
|
inst.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings() {
|
getSettings() {
|
||||||
@@ -369,13 +386,17 @@ class TextPage extends Vue {
|
|||||||
this.meta = bookManager.metaOnly(this.book);
|
this.meta = bookManager.metaOnly(this.book);
|
||||||
this.fb2 = this.meta.fb2;
|
this.fb2 = this.meta.fb2;
|
||||||
|
|
||||||
const authorName = _.compact([
|
let authorNames = [];
|
||||||
this.fb2.lastName,
|
if (this.fb2.author) {
|
||||||
this.fb2.firstName,
|
authorNames = this.fb2.author.map(a => _.compact([
|
||||||
this.fb2.middleName
|
a.lastName,
|
||||||
]).join(' ');
|
a.firstName,
|
||||||
|
a.middleName
|
||||||
|
]).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
this.title = _.compact([
|
this.title = _.compact([
|
||||||
authorName,
|
authorNames.join(', '),
|
||||||
this.fb2.bookTitle
|
this.fb2.bookTitle
|
||||||
]).join(' - ');
|
]).join(' - ');
|
||||||
|
|
||||||
@@ -613,7 +634,7 @@ class TextPage extends Vue {
|
|||||||
const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
|
const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
|
||||||
const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation');
|
const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation');
|
||||||
const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation');
|
const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation');
|
||||||
//const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
|
const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
|
||||||
|
|
||||||
const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100));
|
const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100));
|
||||||
let page1 = this.$refs.scrollingPage1;
|
let page1 = this.$refs.scrollingPage1;
|
||||||
@@ -633,8 +654,22 @@ class TextPage extends Vue {
|
|||||||
duration, this.pageChangeDirectionDown, transition1Finish);
|
duration, this.pageChangeDirectionDown, transition1Finish);
|
||||||
break;
|
break;
|
||||||
case 'downShift':
|
case 'downShift':
|
||||||
|
page1.style.height = this.scrollHeight + 'px';
|
||||||
|
page2.style.height = this.scrollHeight + 'px';
|
||||||
|
|
||||||
await this.drawHelper.doPageAnimationDownShift(page1, page2,
|
await this.drawHelper.doPageAnimationDownShift(page1, page2,
|
||||||
duration, this.pageChangeDirectionDown, transition1Finish);
|
duration, this.pageChangeDirectionDown, transition1Finish);
|
||||||
|
|
||||||
|
page1.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||||
|
page2.style.height = this.scrollHeight + this.lineHeight + 'px';
|
||||||
|
break;
|
||||||
|
case 'rotate':
|
||||||
|
await this.drawHelper.doPageAnimationRotate(page1, page2,
|
||||||
|
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish);
|
||||||
|
break;
|
||||||
|
case 'flip':
|
||||||
|
await this.drawHelper.doPageAnimationFlip(page1, page2,
|
||||||
|
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish, this.backgroundColor);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,7 +1006,7 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTouchStart(event) {
|
onTouchStart(event) {
|
||||||
if (!this.mobile)
|
if (!this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
if (event.touches.length == 1) {
|
if (event.touches.length == 1) {
|
||||||
@@ -987,19 +1022,19 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTouchEnd() {
|
onTouchEnd() {
|
||||||
if (!this.mobile)
|
if (!this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
onTouchCancel() {
|
onTouchCancel() {
|
||||||
if (!this.mobile)
|
if (!this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown(event) {
|
onMouseDown(event) {
|
||||||
if (this.mobile)
|
if (this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
if (event.button == 0) {
|
if (event.button == 0) {
|
||||||
@@ -1015,13 +1050,13 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMouseUp() {
|
onMouseUp() {
|
||||||
if (this.mobile)
|
if (this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel(event) {
|
onMouseWheel(event) {
|
||||||
if (this.mobile)
|
if (this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
if (event.deltaY > 0) {
|
if (event.deltaY > 0) {
|
||||||
this.doDown();
|
this.doDown();
|
||||||
@@ -1098,6 +1133,14 @@ class TextPage extends Vue {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.over-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.on-top {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
.back {
|
.back {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
@@ -1163,4 +1206,5 @@ class TextPage extends Vue {
|
|||||||
0% { opacity: 1; }
|
0% { opacity: 1; }
|
||||||
100% { opacity: 0; }
|
100% { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export default class BookParser {
|
|||||||
if (tag == 'binary') {
|
if (tag == 'binary') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
||||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +201,12 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elemName == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
||||||
|
if (!fb2.author)
|
||||||
|
fb2.author = [];
|
||||||
|
fb2.author.push({});
|
||||||
|
}
|
||||||
|
|
||||||
if (path.indexOf('/fictionbook/body') == 0) {
|
if (path.indexOf('/fictionbook/body') == 0) {
|
||||||
if (tag == 'body') {
|
if (tag == 'body') {
|
||||||
if (!isFirstBody)
|
if (!isFirstBody)
|
||||||
@@ -319,15 +325,19 @@ export default class BookParser {
|
|||||||
|
|
||||||
text = text.replace(/[\t\n\r\xa0]/g, ' ');
|
text = text.replace(/[\t\n\r\xa0]/g, ' ');
|
||||||
|
|
||||||
|
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case '/fictionbook/description/title-info/author/first-name':
|
case '/fictionbook/description/title-info/author/first-name':
|
||||||
fb2.firstName = text;
|
if (authorLength)
|
||||||
|
fb2.author[authorLength - 1].firstName = text;
|
||||||
break;
|
break;
|
||||||
case '/fictionbook/description/title-info/author/middle-name':
|
case '/fictionbook/description/title-info/author/middle-name':
|
||||||
fb2.middleName = text;
|
if (authorLength)
|
||||||
|
fb2.author[authorLength - 1].middleName = text;
|
||||||
break;
|
break;
|
||||||
case '/fictionbook/description/title-info/author/last-name':
|
case '/fictionbook/description/title-info/author/last-name':
|
||||||
fb2.lastName = text;
|
if (authorLength)
|
||||||
|
fb2.author[authorLength - 1].lastName = text;
|
||||||
break;
|
break;
|
||||||
case '/fictionbook/description/title-info/genre':
|
case '/fictionbook/description/title-info/genre':
|
||||||
fb2.genre = text;
|
fb2.genre = text;
|
||||||
@@ -610,7 +620,8 @@ export default class BookParser {
|
|||||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
||||||
para.parsed.showImages === this.showImages &&
|
para.parsed.showImages === this.showImages &&
|
||||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
para.parsed.imageHeightLines === this.imageHeightLines &&
|
||||||
para.parsed.imageFitWidth === this.imageFitWidth
|
para.parsed.imageFitWidth === this.imageFitWidth &&
|
||||||
|
para.parsed.compactTextPerc === this.compactTextPerc
|
||||||
)
|
)
|
||||||
return para.parsed;
|
return para.parsed;
|
||||||
|
|
||||||
@@ -625,6 +636,7 @@ export default class BookParser {
|
|||||||
showImages: this.showImages,
|
showImages: this.showImages,
|
||||||
imageHeightLines: this.imageHeightLines,
|
imageHeightLines: this.imageHeightLines,
|
||||||
imageFitWidth: this.imageFitWidth,
|
imageFitWidth: this.imageFitWidth,
|
||||||
|
compactTextPerc: this.compactTextPerc,
|
||||||
visible: !(
|
visible: !(
|
||||||
(this.cutEmptyParagraphs && para.cut) ||
|
(this.cutEmptyParagraphs && para.cut) ||
|
||||||
(para.addIndex > this.addEmptyParagraphs)
|
(para.addIndex > this.addEmptyParagraphs)
|
||||||
@@ -655,6 +667,7 @@ export default class BookParser {
|
|||||||
let style = {};
|
let style = {};
|
||||||
let ofs = 0;//смещение от начала параграфа para.offset
|
let ofs = 0;//смещение от начала параграфа para.offset
|
||||||
let imgW = 0;
|
let imgW = 0;
|
||||||
|
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
style = part.style;
|
style = part.style;
|
||||||
@@ -739,7 +752,7 @@ export default class BookParser {
|
|||||||
p = (style.space ? p + parsed.p*style.space : p);
|
p = (style.space ? p + parsed.p*style.space : p);
|
||||||
let w = this.measureText(str, style) + p;
|
let w = this.measureText(str, style) + p;
|
||||||
let wordTail = word;
|
let wordTail = word;
|
||||||
if (w > parsed.w && prevStr != '') {
|
if (w > parsed.w + compactWidth && prevStr != '') {
|
||||||
if (parsed.wordWrap) {//по слогам
|
if (parsed.wordWrap) {//по слогам
|
||||||
let slogi = this.splitToSlogi(word);
|
let slogi = this.splitToSlogi(word);
|
||||||
|
|
||||||
@@ -752,7 +765,7 @@ export default class BookParser {
|
|||||||
for (let k = 0; k < slogiLen - 1; k++) {
|
for (let k = 0; k < slogiLen - 1; k++) {
|
||||||
let slog = slogi[0];
|
let slog = slogi[0];
|
||||||
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
||||||
if (ww <= parsed.w) {
|
if (ww <= parsed.w + compactWidth) {
|
||||||
s += slog;
|
s += slog;
|
||||||
ss += slog;
|
ss += slog;
|
||||||
} else
|
} else
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
|
||||||
const maxDataSize = 500*1024*1024;//chars, not bytes
|
const maxDataSize = 300*1024*1024;//compressed bytes
|
||||||
|
|
||||||
const bmCacheStore = localForage.createInstance({
|
|
||||||
name: 'bmCacheStore'
|
|
||||||
});
|
|
||||||
|
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
name: 'bmMetaStore'
|
name: 'bmMetaStore'
|
||||||
@@ -23,58 +20,84 @@ const bmRecentStore = localForage.createInstance({
|
|||||||
|
|
||||||
class BookManager {
|
class BookManager {
|
||||||
async init(settings) {
|
async init(settings) {
|
||||||
|
this.loaded = false;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
|
||||||
//this.booksCached нужен только для ускорения загрузки читалки
|
this.eventListeners = [];
|
||||||
this.booksCached = await bmCacheStore.getItem('books');
|
this.books = {};
|
||||||
if (!this.booksCached)
|
this.recent = {};
|
||||||
this.booksCached = {};
|
|
||||||
this.recent = await bmCacheStore.getItem('recent');
|
|
||||||
this.books = Object.assign({}, this.booksCached);
|
|
||||||
|
|
||||||
this.recentChanged1 = true;
|
this.recentLast = await bmRecentStore.getItem('recent-last');
|
||||||
this.recentChanged2 = true;
|
if (this.recentLast) {
|
||||||
|
this.recent[this.recentLast.key] = this.recentLast;
|
||||||
if (!this.books || !this.recent) {
|
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||||
this.books = {};
|
if (_.isObject(meta)) {
|
||||||
this.recent = {};
|
this.books[meta.key] = meta;
|
||||||
await this.loadMeta(true);
|
}
|
||||||
} else {
|
|
||||||
this.loadMeta(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.recentChanged = true;
|
||||||
|
|
||||||
|
this.loadStored();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Долгая асинхронная загрузка из хранилища.
|
||||||
async loadMeta(immediate) {
|
//Хранение в отдельных записях дает относительно
|
||||||
if (!immediate)
|
//нормальное поведение при нескольких вкладках с читалкой в браузере.
|
||||||
await utils.sleep(2000);
|
async loadStored() {
|
||||||
|
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||||
|
await utils.sleep(2000);
|
||||||
|
|
||||||
let len = await bmMetaStore.length();
|
let len = await bmMetaStore.length();
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
const key = await bmMetaStore.key(i);
|
const key = await bmMetaStore.key(i);
|
||||||
const keySplit = key.split('-');
|
const keySplit = key.split('-');
|
||||||
|
|
||||||
if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
|
if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
|
||||||
let meta = await bmMetaStore.getItem(key);
|
let meta = await bmMetaStore.getItem(key);
|
||||||
|
|
||||||
this.books[meta.key] = meta;
|
if (_.isObject(meta)) {
|
||||||
|
//уже может быть распарсена книга
|
||||||
|
const oldBook = this.books[meta.key];
|
||||||
|
this.books[meta.key] = meta;
|
||||||
|
|
||||||
|
if (oldBook && oldBook.parsed) {
|
||||||
|
this.books[meta.key].parsed = oldBook.parsed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await bmMetaStore.removeItem(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let key = null;
|
||||||
len = await bmRecentStore.length();
|
len = await bmRecentStore.length();
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
const key = await bmRecentStore.key(i);
|
key = await bmRecentStore.key(i);
|
||||||
let r = await bmRecentStore.getItem(key);
|
if (key) {
|
||||||
this.recent[r.key] = r;
|
let r = await bmRecentStore.getItem(key);
|
||||||
|
if (_.isObject(r) && r.key) {
|
||||||
|
this.recent[r.key] = r;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await bmRecentStore.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 this.cleanBooks();
|
await this.cleanBooks();
|
||||||
|
await this.cleanRecentBooks();
|
||||||
|
|
||||||
this.booksCached = {};
|
this.recentChanged = true;
|
||||||
for (const key in this.books) {
|
this.loaded = true;
|
||||||
this.booksCached[key] = this.metaOnly(this.books[key]);
|
this.emit('load-stored-finish');
|
||||||
}
|
|
||||||
await bmCacheStore.setItem('books', this.booksCached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanBooks() {
|
async cleanBooks() {
|
||||||
@@ -84,7 +107,8 @@ class BookManager {
|
|||||||
let toDel = null;
|
let toDel = null;
|
||||||
for (let key in this.books) {
|
for (let key in this.books) {
|
||||||
let book = this.books[key];
|
let book = this.books[key];
|
||||||
size += (book.length ? book.length : 0);
|
const bookLength = (book.length ? book.length : 0);
|
||||||
|
size += (book.dataCompressedLength ? book.dataCompressedLength : bookLength);
|
||||||
|
|
||||||
if (book.addTime < min) {
|
if (book.addTime < min) {
|
||||||
toDel = book;
|
toDel = book;
|
||||||
@@ -100,51 +124,173 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addBook(newBook, callback) {
|
async deflateWithProgress(data, callback) {
|
||||||
if (!this.books)
|
const chunkSize = 128*1024;
|
||||||
await this.init();
|
const deflator = new utils.pako.Deflate({level: 5});
|
||||||
|
|
||||||
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
|
let chunkNum = 0;
|
||||||
|
let perc = 0;
|
||||||
|
let prevPerc = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i += chunkSize) {
|
||||||
|
if ((i + chunkSize) >= data.length) {
|
||||||
|
deflator.push(data.substring(i, i + chunkSize), true);
|
||||||
|
} else {
|
||||||
|
deflator.push(data.substring(i, i + chunkSize), false);
|
||||||
|
}
|
||||||
|
chunkNum++;
|
||||||
|
|
||||||
|
perc = Math.round(chunkNum/chunkTotal*100);
|
||||||
|
if (perc != prevPerc) {
|
||||||
|
callback(perc);
|
||||||
|
await utils.sleep(1);
|
||||||
|
prevPerc = perc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deflator.err) {
|
||||||
|
throw new Error(deflator.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(100);
|
||||||
|
|
||||||
|
return deflator.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async inflateWithProgress(data, callback) {
|
||||||
|
const chunkSize = 64*1024;
|
||||||
|
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||||
|
|
||||||
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
|
let chunkNum = 0;
|
||||||
|
let perc = 0;
|
||||||
|
let prevPerc = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i += chunkSize) {
|
||||||
|
if ((i + chunkSize) >= data.length) {
|
||||||
|
inflator.push(data.subarray(i, i + chunkSize), true);
|
||||||
|
} else {
|
||||||
|
inflator.push(data.subarray(i, i + chunkSize), false);
|
||||||
|
}
|
||||||
|
chunkNum++;
|
||||||
|
|
||||||
|
perc = Math.round(chunkNum/chunkTotal*100);
|
||||||
|
if (perc != prevPerc) {
|
||||||
|
callback(perc);
|
||||||
|
await utils.sleep(1);
|
||||||
|
prevPerc = perc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inflator.err) {
|
||||||
|
throw new Error(inflator.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(100);
|
||||||
|
|
||||||
|
return inflator.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addBook(newBook, callback) {
|
||||||
let meta = {url: newBook.url, path: newBook.path};
|
let meta = {url: newBook.url, path: newBook.path};
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromUrl(meta.url);
|
||||||
meta.addTime = Date.now();
|
meta.addTime = Date.now();
|
||||||
|
|
||||||
const result = await this.parseBook(meta, newBook.data, callback);
|
const cb = (perc) => {
|
||||||
|
const p = Math.round(30*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cb2 = (perc) => {
|
||||||
|
const p = Math.round(30 + 65*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await this.parseBook(meta, newBook.data, cb);
|
||||||
|
result.dataCompressed = true;
|
||||||
|
|
||||||
|
let data = newBook.data;
|
||||||
|
if (result.dataCompressed) {
|
||||||
|
//data = utils.pako.deflate(data, {level: 5});
|
||||||
|
data = await this.deflateWithProgress(data, cb2);
|
||||||
|
result.dataCompressedLength = data.byteLength;
|
||||||
|
}
|
||||||
|
callback(95);
|
||||||
|
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
this.booksCached[meta.key] = this.metaOnly(result);
|
|
||||||
|
|
||||||
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
||||||
await bmDataStore.setItem(`bmData-${meta.key}`, result.data);
|
await bmDataStore.setItem(`bmData-${meta.key}`, data);
|
||||||
await bmCacheStore.setItem('books', this.booksCached);
|
|
||||||
|
|
||||||
|
callback(100);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBookParsed(meta) {
|
async hasBookParsed(meta) {
|
||||||
if (!this.books)
|
if (!this.books)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.url)
|
if (!meta.url)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromUrl(meta.url);
|
||||||
|
|
||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
|
if (!book && !this.loaded) {
|
||||||
|
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||||
|
if (book)
|
||||||
|
this.books[meta.key] = book;
|
||||||
|
}
|
||||||
|
|
||||||
return !!(book && book.parsed);
|
return !!(book && book.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBook(meta, callback) {
|
async getBook(meta, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromUrl(meta.url);
|
||||||
|
|
||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
if (result && !result.data) {
|
if (!result) {
|
||||||
result.data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||||
this.books[meta.key] = result;
|
if (result)
|
||||||
|
this.books[meta.key] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Если файл на сервере изменился, считаем, что в кеше его нету
|
||||||
|
if (meta.path && result && meta.path != result.path) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result && !result.parsed) {
|
if (result && !result.parsed) {
|
||||||
result = await this.parseBook(result, result.data, callback);
|
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||||
|
callback(5);
|
||||||
|
await utils.sleep(10);
|
||||||
|
|
||||||
|
let cb = (perc) => {
|
||||||
|
const p = 5 + Math.round(15*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.dataCompressed) {
|
||||||
|
try {
|
||||||
|
//data = utils.pako.inflate(data, {to: 'string'});
|
||||||
|
data = await this.inflateWithProgress(data, cb);
|
||||||
|
} catch (e) {
|
||||||
|
this.delBook(meta);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(20);
|
||||||
|
|
||||||
|
cb = (perc) => {
|
||||||
|
const p = 20 + Math.round(80*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
|
result = await this.parseBook(result, data, cb);
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,29 +298,19 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delBook(meta) {
|
async delBook(meta) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
||||||
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
||||||
|
|
||||||
delete this.books[meta.key];
|
delete this.books[meta.key];
|
||||||
delete this.booksCached[meta.key];
|
|
||||||
|
|
||||||
await bmCacheStore.setItem('books', this.booksCached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseBook(meta, data, callback) {
|
async parseBook(meta, data, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
const parsed = new BookParser(this.settings);
|
const parsed = new BookParser(this.settings);
|
||||||
|
|
||||||
const parsedMeta = await parsed.parse(data, callback);
|
const parsedMeta = await parsed.parse(data, callback);
|
||||||
const result = Object.assign({}, meta, parsedMeta, {
|
const result = Object.assign({}, meta, parsedMeta, {
|
||||||
length: data.length,
|
length: data.length,
|
||||||
textLength: parsed.textLength,
|
textLength: parsed.textLength,
|
||||||
data,
|
|
||||||
parsed
|
parsed
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -183,7 +319,7 @@ class BookManager {
|
|||||||
|
|
||||||
metaOnly(book) {
|
metaOnly(book) {
|
||||||
let result = Object.assign({}, book);
|
let result = Object.assign({}, book);
|
||||||
delete result.data;
|
delete result.data;//можно будет убрать эту строку со временем
|
||||||
delete result.parsed;
|
delete result.parsed;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -192,88 +328,98 @@ class BookManager {
|
|||||||
return utils.stringToHex(url);
|
return utils.stringToHex(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRecentBook(value, noTouch) {
|
//-- recent --------------------------------------------------------------
|
||||||
if (!this.recent)
|
async setRecentBook(value) {
|
||||||
await this.init();
|
|
||||||
const result = this.metaOnly(value);
|
const result = this.metaOnly(value);
|
||||||
if (!noTouch)
|
result.touchTime = Date.now();
|
||||||
Object.assign(result, {touchTime: Date.now()});
|
result.deleted = 0;
|
||||||
|
|
||||||
if (result.textLength && !result.bookPos && result.bookPosPercent)
|
if (this.recent[result.key] && this.recent[result.key].deleted) {
|
||||||
result.bookPos = Math.round(result.bookPosPercent*result.textLength);
|
//восстановим из небытия пользовательские данные
|
||||||
|
if (!result.bookPos)
|
||||||
|
result.bookPos = this.recent[result.key].bookPos;
|
||||||
|
if (!result.bookPosSeen)
|
||||||
|
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
||||||
|
}
|
||||||
|
|
||||||
this.recent[result.key] = result;
|
this.recent[result.key] = result;
|
||||||
|
|
||||||
await bmRecentStore.setItem(result.key, result);
|
await bmRecentStore.setItem(result.key, result);
|
||||||
await this.cleanRecentBooks();
|
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
|
|
||||||
this.recentChanged1 = true;
|
this.recentLast = result;
|
||||||
this.recentChanged2 = true;
|
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||||
|
|
||||||
|
this.recentChanged = true;
|
||||||
|
this.emit('recent-changed', result.key);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentBook(value) {
|
async getRecentBook(value) {
|
||||||
if (!this.recent)
|
let result = this.recent[value.key];
|
||||||
await this.init();
|
if (!result) {
|
||||||
return this.recent[value.key];
|
result = await bmRecentStore.getItem(value.key);
|
||||||
|
if (result)
|
||||||
|
this.recent[value.key] = result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value) {
|
||||||
if (!this.recent)
|
this.recent[value.key].deleted = 1;
|
||||||
await this.init();
|
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
||||||
|
|
||||||
await bmRecentStore.removeItem(value.key);
|
if (this.recentLast.key == value.key) {
|
||||||
delete this.recent[value.key];
|
this.recentLast = null;
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||||
|
}
|
||||||
this.recentChanged1 = true;
|
this.emit('recent-deleted', value.key);
|
||||||
this.recentChanged2 = true;
|
this.emit('recent-changed', value.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
if (!this.recent)
|
const sorted = this.getSortedRecent();
|
||||||
await this.init();
|
|
||||||
|
|
||||||
if (Object.keys(this.recent).length > 1000) {
|
let isDel = false;
|
||||||
let min = Date.now();
|
for (let i = 1000; i < sorted.length; i++) {
|
||||||
let found = null;
|
await bmRecentStore.removeItem(sorted[i].key);
|
||||||
for (let key in this.recent) {
|
delete this.recent[sorted[i].key];
|
||||||
const book = this.recent[key];
|
await bmRecentStore.removeItem(sorted[i].key);
|
||||||
if (book.touchTime < min) {
|
isDel = true;
|
||||||
min = book.touchTime;
|
|
||||||
found = book;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
await this.delRecentBook(found);
|
|
||||||
await this.cleanRecentBooks();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sortedRecentCached = null;
|
||||||
|
|
||||||
|
if (isDel)
|
||||||
|
this.emit('recent-changed');
|
||||||
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
if (!this.recentChanged1 && this.mostRecentCached) {
|
if (this.recentLast) {
|
||||||
return this.mostRecentCached;
|
return this.recentLast;
|
||||||
}
|
}
|
||||||
|
const oldRecentLast = this.recentLast;
|
||||||
|
|
||||||
let max = 0;
|
let max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
for (let key in this.recent) {
|
for (let key in this.recent) {
|
||||||
const book = this.recent[key];
|
const book = this.recent[key];
|
||||||
if (book.touchTime > max) {
|
if (!book.deleted && book.touchTime > max) {
|
||||||
max = book.touchTime;
|
max = book.touchTime;
|
||||||
result = book;
|
result = book;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.mostRecentCached = result;
|
this.recentLast = result;
|
||||||
this.recentChanged1 = false;
|
bmRecentStore.setItem('recent-last', this.recentLast);//no await
|
||||||
|
|
||||||
|
if (this.recentLast !== oldRecentLast)
|
||||||
|
this.emit('recent-changed');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedRecent() {
|
getSortedRecent() {
|
||||||
if (!this.recentChanged2 && this.sortedRecentCached) {
|
if (!this.recentChanged && this.sortedRecentCached) {
|
||||||
return this.sortedRecentCached;
|
return this.sortedRecentCached;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,10 +428,60 @@ class BookManager {
|
|||||||
result.sort((a, b) => b.touchTime - a.touchTime);
|
result.sort((a, b) => b.touchTime - a.touchTime);
|
||||||
|
|
||||||
this.sortedRecentCached = result;
|
this.sortedRecentCached = result;
|
||||||
this.recentChanged2 = false;
|
this.recentChanged = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setRecent(value) {
|
||||||
|
const mergedRecent = _.cloneDeep(this.recent);
|
||||||
|
|
||||||
|
Object.assign(mergedRecent, value);
|
||||||
|
|
||||||
|
//подстраховка от hotReload
|
||||||
|
for (let i of Object.keys(mergedRecent)) {
|
||||||
|
if (!mergedRecent[i].key || mergedRecent[i].key !== i)
|
||||||
|
delete mergedRecent[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
//"ленивое" обновление хранилища
|
||||||
|
(async() => {
|
||||||
|
for (const rec of Object.values(mergedRecent)) {
|
||||||
|
if (rec.key) {
|
||||||
|
await bmRecentStore.setItem(rec.key, rec);
|
||||||
|
await utils.sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.recent = mergedRecent;
|
||||||
|
|
||||||
|
this.recentLast = null;
|
||||||
|
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||||
|
|
||||||
|
this.recentChanged = true;
|
||||||
|
this.emit('set-recent');
|
||||||
|
this.emit('recent-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(listener) {
|
||||||
|
if (this.eventListeners.indexOf(listener) < 0)
|
||||||
|
this.eventListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(listener) {
|
||||||
|
const i = this.eventListeners.indexOf(listener);
|
||||||
|
if (i >= 0)
|
||||||
|
this.eventListeners.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventName, value) {
|
||||||
|
if (this.eventListeners) {
|
||||||
|
for (const listener of this.eventListeners) {
|
||||||
|
//console.log(eventName);
|
||||||
|
listener(eventName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
export default async function restoreOldSettings(settings, bookManager, commit) {
|
|
||||||
const oldSets = localStorage['colorSetting'];
|
|
||||||
let isOld = false;
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
let key = unescape(localStorage.key(i));
|
|
||||||
if (key.indexOf('bpr-book-') == 0)
|
|
||||||
isOld = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOld || oldSets) {
|
|
||||||
let newSettings = null;
|
|
||||||
if (oldSets) {
|
|
||||||
const [textColor, backgroundColor, lineStep, , , statusBarHeight, scInt] = unescape(oldSets).split('|');
|
|
||||||
|
|
||||||
const fontSize = Math.round(lineStep*0.8);
|
|
||||||
const scrollingDelay = fontSize*scInt;
|
|
||||||
|
|
||||||
newSettings = Object.assign({}, settings, {
|
|
||||||
textColor,
|
|
||||||
backgroundColor,
|
|
||||||
fontSize,
|
|
||||||
statusBarHeight: statusBarHeight*1,
|
|
||||||
scrollingDelay,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
let key = localStorage.key(i);
|
|
||||||
if (key.indexOf('bpr-') == 0) {
|
|
||||||
let v = unescape(localStorage[key]);
|
|
||||||
key = unescape(key);
|
|
||||||
|
|
||||||
if (key.lastIndexOf('=timestamp') == key.length - 10) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.indexOf('bpr-book-') == 0) {
|
|
||||||
const url = key.substr(9);
|
|
||||||
const [scrollTop, scrollHeight, ] = v.split('|');
|
|
||||||
|
|
||||||
const bookPosPercent = scrollTop*1/(scrollHeight*1 + 1);
|
|
||||||
const title = unescape(localStorage[`bpr-title-${escape(url)}`]);
|
|
||||||
const author = unescape(localStorage[`bpr-author-${escape(url)}`]);
|
|
||||||
const time = unescape(localStorage[`bpr-book-${escape(url)}=timestamp`]).split(';')[0];
|
|
||||||
const touchTime = Date.parse(time);
|
|
||||||
|
|
||||||
const bookKey = bookManager.keyFromUrl(url);
|
|
||||||
const recent = await bookManager.getRecentBook({key: bookKey});
|
|
||||||
|
|
||||||
if (!recent) {
|
|
||||||
await bookManager.setRecentBook({
|
|
||||||
key: bookKey,
|
|
||||||
touchTime,
|
|
||||||
bookPosPercent,
|
|
||||||
url,
|
|
||||||
fb2: {
|
|
||||||
bookTitle: title,
|
|
||||||
lastName: author,
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.clear();
|
|
||||||
if (oldSets)
|
|
||||||
commit('reader/setSettings', newSettings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
178
client/components/Reader/versionHistory.js
Normal file
178
client/components/Reader/versionHistory.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
showUntil: '2019-10-21',
|
||||||
|
header: '0.7.5 (2019-10-22)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-10-17',
|
||||||
|
header: '0.7.3 (2019-10-18)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>внутренние переделки механизма синхронизации с сервером</li>
|
||||||
|
<li>добавлен html-фильтр для сайтов www.fanfiction.net, archiveofourown.org</li>
|
||||||
|
<li>добавлен параметр "Включить html-фильтр для сайтов" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-09-19',
|
||||||
|
header: '0.7.1 (2019-09-20)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
<li>на панель управления добавлена кнопка "Автономный режим"</li>
|
||||||
|
<li>актуализирована справка</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-10-01',
|
||||||
|
header: '0.7.0 (2019-09-07)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>налажена работа https-версии сайта, рекомендуется плавный переход</li>
|
||||||
|
<li>добавлена возможность загрузки и работы https-версии читалки в оффлайн-режиме (при отсутствии интернета)</li>
|
||||||
|
<li>упрощение механизма серверной синхронизации с целью повышения надежности и избавления от багов</li>
|
||||||
|
<li>окна теперь можно перемещать за заголовок</li>
|
||||||
|
<li>немного улучшен внешний вид и управление на смартфонах</li>
|
||||||
|
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-07-20',
|
||||||
|
header: '0.6.10 (2019-07-21)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-06-22',
|
||||||
|
header: '0.6.9 (2019-06-23)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправлен баг - падение сервера при распаковке битых архивов книг</li>
|
||||||
|
<li>исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8</li>
|
||||||
|
<li>добавлены новые варианты анимации перелистывания</li>
|
||||||
|
<li>на страницу загрузки добавлен блок "Поделиться"</li>
|
||||||
|
<li>улучшены прогрессбары</li>
|
||||||
|
<li>исправления недочетов, небольшие оптимизации</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-06-05',
|
||||||
|
header: '0.6.7 (2019-05-30)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлен диалог "Что нового"</li>
|
||||||
|
<li>в справку добавлена история версий проекта</li>
|
||||||
|
<li>добавлена возможность настройки отображаемых кнопок на панели управления</li>
|
||||||
|
<li>некоторые кнопки на панели управления были скрыты по умолчанию</li>
|
||||||
|
<li>на страницу загрузки добавлена возможность загрузки книги из буфера обмена</li>
|
||||||
|
<li>добавлен GET-параметр вида "/reader?__refresh=1&url=..." для принудительного обновления загружаемого текста</li>
|
||||||
|
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
|
||||||
|
<li>исправления багов и недочетов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-03-29',
|
||||||
|
header: '0.6.6 (2019-03-29)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
|
||||||
|
<li>оптимизации процесса синхронизации, внутренние переделки</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-03-24',
|
||||||
|
header: '0.6.4 (2019-03-24)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>исправления багов, оптимизации</li>
|
||||||
|
<li>добавлена возможность синхронизации данных между устройствами</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-03-04',
|
||||||
|
header: '0.5.4 (2019-03-04)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена поддержка форматов pdf, epub, mobi</li>
|
||||||
|
<li>(0.5.2) добавлена поддержка форматов rtf, doc, docx</li>
|
||||||
|
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
|
||||||
|
<li>(0.4.0) добавлено отображение картинок в fb2</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-02-17',
|
||||||
|
header: '0.3.0 (2019-02-17)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>поправки багов</li>
|
||||||
|
<li>улучшено распознавание текста</li>
|
||||||
|
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-02-14',
|
||||||
|
header: '0.1.7 (2019-02-14)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>увеличены верхние границы отступов и др.размеров</li>
|
||||||
|
<li>добавлена настройка для удаления/вставки пустых параграфов</li>
|
||||||
|
<li>добавлена настройка включения/отключения управления кликом</li>
|
||||||
|
<li>добавлена возможность сброса настроек</li>
|
||||||
|
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2019-02-12',
|
||||||
|
header: '0.1.0 (2019-02-12)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="window">
|
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||||
<div class="header">
|
<div ref="windowBox" class="windowBox" @click.stop>
|
||||||
<span class="header-text"><slot name="header"></slot></span>
|
<div class="window">
|
||||||
<span class="close-button" @click="close"><i class="el-icon-close"></i></span>
|
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
|
||||||
|
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
|
||||||
|
<span class="header-text"><slot name="header"></slot></span>
|
||||||
|
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -14,17 +19,116 @@ import Vue from 'vue';
|
|||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
|
props: {
|
||||||
|
height: { type: String, default: '100%' },
|
||||||
|
width: { type: String, default: '100%' },
|
||||||
|
maxWidth: { type: String, default: '' },
|
||||||
|
topShift: { type: Number, default: 0 },
|
||||||
|
}
|
||||||
})
|
})
|
||||||
class Window extends Vue {
|
class Window extends Vue {
|
||||||
close() {
|
init() {
|
||||||
this.$emit('close');
|
this.$nextTick(() => {
|
||||||
|
this.$refs.windowBox.style.height = this.height;
|
||||||
|
this.$refs.windowBox.style.width = this.width;
|
||||||
|
if (this.maxWidth)
|
||||||
|
this.$refs.windowBox.style.maxWidth = this.maxWidth;
|
||||||
|
|
||||||
|
const left = (this.$refs.main.offsetWidth - this.$refs.windowBox.offsetWidth)/2;
|
||||||
|
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
|
||||||
|
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMouseDown(event) {
|
||||||
|
if (this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.$refs.header.style.cursor = 'move';
|
||||||
|
this.startX = event.screenX;
|
||||||
|
this.startY = event.screenY;
|
||||||
|
this.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(event) {
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.$refs.header.style.cursor = 'default';
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event) {
|
||||||
|
if (this.moving) {
|
||||||
|
const deltaX = event.screenX - this.startX;
|
||||||
|
const deltaY = event.screenY - this.startY;
|
||||||
|
this.startX = event.screenX;
|
||||||
|
this.startY = event.screenY;
|
||||||
|
|
||||||
|
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart(event) {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
this.$refs.header.style.cursor = 'move';
|
||||||
|
this.startX = touch.screenX;
|
||||||
|
this.startY = touch.screenY;
|
||||||
|
this.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchMove(event) {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1 && this.moving) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const deltaX = touch.screenX - this.startX;
|
||||||
|
const deltaY = touch.screenY - this.startY;
|
||||||
|
this.startX = touch.screenX;
|
||||||
|
this.startY = touch.screenY;
|
||||||
|
|
||||||
|
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchEnd() {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
this.$refs.header.style.cursor = 'default';
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.moving)
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.main {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.windowBox {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.window {
|
.window {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -39,9 +143,9 @@ class Window extends Vue {
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
background-color: #e5e7ea;
|
background-color: #59B04F;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
@@ -54,8 +158,12 @@ class Window extends Vue {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 40px;
|
width: 30px;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background-color: #69C05F;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -9,98 +9,44 @@ Vue.use(ElementUI, { locale });
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
//------------------------------------------------------
|
//------------------------------------------------------
|
||||||
//import './theme/index.css';
|
import './theme/index.css';
|
||||||
|
|
||||||
import './theme/icon.css';
|
|
||||||
import './theme/tooltip.css';
|
|
||||||
|
|
||||||
import ElMenu from 'element-ui/lib/menu';
|
import ElMenu from 'element-ui/lib/menu';
|
||||||
import './theme/menu.css';
|
|
||||||
|
|
||||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||||
import './theme/menu-item.css';
|
|
||||||
|
|
||||||
import ElButton from 'element-ui/lib/button';
|
import ElButton from 'element-ui/lib/button';
|
||||||
import './theme/button.css';
|
|
||||||
|
|
||||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
import ElButtonGroup from 'element-ui/lib/button-group';
|
||||||
import './theme/button-group.css';
|
|
||||||
|
|
||||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
import ElCheckbox from 'element-ui/lib/checkbox';
|
||||||
import './theme/checkbox.css';
|
|
||||||
|
|
||||||
import ElTabs from 'element-ui/lib/tabs';
|
import ElTabs from 'element-ui/lib/tabs';
|
||||||
import './theme/tabs.css';
|
|
||||||
|
|
||||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
import ElTabPane from 'element-ui/lib/tab-pane';
|
||||||
import './theme/tab-pane.css';
|
|
||||||
|
|
||||||
import ElTooltip from 'element-ui/lib/tooltip';
|
import ElTooltip from 'element-ui/lib/tooltip';
|
||||||
import './theme/tooltip.css';
|
|
||||||
|
|
||||||
import ElCol from 'element-ui/lib/col';
|
import ElCol from 'element-ui/lib/col';
|
||||||
import './theme/col.css';
|
|
||||||
|
|
||||||
import ElContainer from 'element-ui/lib/container';
|
import ElContainer from 'element-ui/lib/container';
|
||||||
import './theme/container.css';
|
|
||||||
|
|
||||||
import ElAside from 'element-ui/lib/aside';
|
import ElAside from 'element-ui/lib/aside';
|
||||||
import './theme/aside.css';
|
|
||||||
|
|
||||||
import ElHeader from 'element-ui/lib/header';
|
import ElHeader from 'element-ui/lib/header';
|
||||||
import './theme/header.css';
|
|
||||||
|
|
||||||
import ElMain from 'element-ui/lib/main';
|
import ElMain from 'element-ui/lib/main';
|
||||||
import './theme/main.css';
|
|
||||||
|
|
||||||
import ElInput from 'element-ui/lib/input';
|
import ElInput from 'element-ui/lib/input';
|
||||||
import './theme/input.css';
|
|
||||||
|
|
||||||
import ElInputNumber from 'element-ui/lib/input-number';
|
import ElInputNumber from 'element-ui/lib/input-number';
|
||||||
import './theme/input-number.css';
|
|
||||||
|
|
||||||
import ElSelect from 'element-ui/lib/select';
|
import ElSelect from 'element-ui/lib/select';
|
||||||
import './theme/select.css';
|
|
||||||
|
|
||||||
import ElOption from 'element-ui/lib/option';
|
import ElOption from 'element-ui/lib/option';
|
||||||
import './theme/option.css';
|
|
||||||
|
|
||||||
import ElTable from 'element-ui/lib/table';
|
import ElTable from 'element-ui/lib/table';
|
||||||
import './theme/table.css';
|
|
||||||
|
|
||||||
import ElTableColumn from 'element-ui/lib/table-column';
|
import ElTableColumn from 'element-ui/lib/table-column';
|
||||||
import './theme/table-column.css';
|
|
||||||
|
|
||||||
import ElProgress from 'element-ui/lib/progress';
|
import ElProgress from 'element-ui/lib/progress';
|
||||||
import './theme/progress.css';
|
|
||||||
|
|
||||||
import ElSlider from 'element-ui/lib/slider';
|
import ElSlider from 'element-ui/lib/slider';
|
||||||
import './theme/slider.css';
|
|
||||||
|
|
||||||
import ElForm from 'element-ui/lib/form';
|
import ElForm from 'element-ui/lib/form';
|
||||||
import './theme/form.css';
|
|
||||||
|
|
||||||
import ElFormItem from 'element-ui/lib/form-item';
|
import ElFormItem from 'element-ui/lib/form-item';
|
||||||
import './theme/form-item.css';
|
|
||||||
|
|
||||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
import ElColorPicker from 'element-ui/lib/color-picker';
|
||||||
import './theme/color-picker.css';
|
import ElDialog from 'element-ui/lib/dialog';
|
||||||
|
|
||||||
import Notification from 'element-ui/lib/notification';
|
import Notification from 'element-ui/lib/notification';
|
||||||
import './theme/notification.css';
|
|
||||||
|
|
||||||
import Loading from 'element-ui/lib/loading';
|
import Loading from 'element-ui/lib/loading';
|
||||||
import './theme/loading.css';
|
|
||||||
|
|
||||||
import MessageBox from 'element-ui/lib/message-box';
|
import MessageBox from 'element-ui/lib/message-box';
|
||||||
import './theme/message-box.css';
|
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
||||||
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
||||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
||||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
ElProgress, ElSlider, ElForm, ElFormItem,
|
||||||
ElColorPicker,
|
ElColorPicker, ElDialog,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let name in components) {
|
for (let name in components) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html manifest="/app/manifest.appcache">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
@@ -9,5 +9,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script src="https://yastatic.net/share2/share.js" async="async"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './components/App.vue';
|
|
||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './element';
|
import './element';
|
||||||
|
|
||||||
|
import App from './components/App.vue';
|
||||||
//Vue.config.productionTip = false;
|
//Vue.config.productionTip = false;
|
||||||
|
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
|
|||||||
@@ -2,21 +2,25 @@ import Vue from 'vue';
|
|||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import App from './components/App.vue';
|
//немедленная загрузка
|
||||||
|
import CardIndex from './components/CardIndex/CardIndex.vue';
|
||||||
|
//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
||||||
|
|
||||||
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
|
||||||
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
||||||
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
||||||
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
||||||
const History = () => import('./components/CardIndex/History/History.vue');
|
const History = () => import('./components/CardIndex/History/History.vue');
|
||||||
|
|
||||||
const Reader = () => import('./components/Reader/Reader.vue');
|
//немедленная загрузка
|
||||||
|
//const Reader = () => import('./components/Reader/Reader.vue');
|
||||||
|
import Reader from './components/Reader/Reader.vue';
|
||||||
|
|
||||||
//const Forum = () => import('./components/Forum/Forum.vue');
|
//const Forum = () => import('./components/Forum/Forum.vue');
|
||||||
const Income = () => import('./components/Income/Income.vue');
|
const Income = () => import('./components/Income/Income.vue');
|
||||||
const Sources = () => import('./components/Sources/Sources.vue');
|
const Sources = () => import('./components/Sources/Sources.vue');
|
||||||
const Settings = () => import('./components/Settings/Settings.vue');
|
const Settings = () => import('./components/Settings/Settings.vue');
|
||||||
const Help = () => import('./components/Help/Help.vue');
|
const Help = () => import('./components/Help/Help.vue');
|
||||||
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||||
|
|
||||||
const myRoutes = [
|
const myRoutes = [
|
||||||
['/', null, null, '/cardindex'],
|
['/', null, null, '/cardindex'],
|
||||||
|
|||||||
26
client/share/cryptoUtils.js
Normal file
26
client/share/cryptoUtils.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//WebCrypto API (crypto.subtle) не работает без https, поэтому приходится извращаться через sjcl
|
||||||
|
import sjclWrapper from './sjclWrapper';
|
||||||
|
|
||||||
|
//не менять
|
||||||
|
const iv = 'B6E2XejNh2dS';
|
||||||
|
const salt = 'Liberama project is awesome';
|
||||||
|
|
||||||
|
export function aesEncrypt(data, password) {
|
||||||
|
return sjclWrapper.codec.bytes.fromBits(
|
||||||
|
sjclWrapper.encryptArray(
|
||||||
|
password, sjclWrapper.codec.bytes.toBits(data), {iv, salt}
|
||||||
|
).ct
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesDecrypt(data, password) {
|
||||||
|
return sjclWrapper.codec.bytes.fromBits(
|
||||||
|
sjclWrapper.decryptArray(
|
||||||
|
password, {ct: sjclWrapper.codec.bytes.toBits(data)}, {iv, salt}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha256(str) {
|
||||||
|
return sjclWrapper.codec.bytes.fromBits(sjclWrapper.hash.sha256.hash(str));
|
||||||
|
}
|
||||||
60
client/share/sjcl.js
Normal file
60
client/share/sjcl.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
|
||||||
|
sjcl.cipher.aes=function(a){this.s[0][0][0]||this.O();var b,c,d,e,f=this.s[0][4],g=this.s[1];b=a.length;var h=1;if(4!==b&&6!==b&&8!==b)throw new sjcl.exception.invalid("invalid aes key size");this.b=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(0===a%b||8===b&&4===a%b)c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255],0===a%b&&(c=c<<8^c>>>24^h<<24,h=h<<1^283*(h>>7));d[a]=d[a-b]^c}for(b=0;a;b++,a--)c=d[b&3?a:a-4],e[b]=4>=a||4>b?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^g[3][f[c&
|
||||||
|
255]]};
|
||||||
|
sjcl.cipher.aes.prototype={encrypt:function(a){return t(this,a,0)},decrypt:function(a){return t(this,a,1)},s:[[[],[],[],[],[]],[[],[],[],[],[]]],O:function(){var a=this.s[0],b=this.s[1],c=a[4],d=b[4],e,f,g,h=[],k=[],l,n,m,p;for(e=0;0x100>e;e++)k[(h[e]=e<<1^283*(e>>7))^e]=e;for(f=g=0;!c[f];f^=l||1,g=k[g]||1)for(m=g^g<<1^g<<2^g<<3^g<<4,m=m>>8^m&255^99,c[f]=m,d[m]=f,n=h[e=h[l=h[f]]],p=0x1010101*n^0x10001*e^0x101*l^0x1010100*f,n=0x101*h[m]^0x1010100*m,e=0;4>e;e++)a[e][f]=n=n<<24^n>>>8,b[e][m]=p=p<<24^p>>>8;for(e=
|
||||||
|
0;5>e;e++)a[e]=a[e].slice(0),b[e]=b[e].slice(0)}};
|
||||||
|
function t(a,b,c){if(4!==b.length)throw new sjcl.exception.invalid("invalid aes block size");var d=a.b[c],e=b[0]^d[0],f=b[c?3:1]^d[1],g=b[2]^d[2];b=b[c?1:3]^d[3];var h,k,l,n=d.length/4-2,m,p=4,r=[0,0,0,0];h=a.s[c];a=h[0];var q=h[1],v=h[2],w=h[3],x=h[4];for(m=0;m<n;m++)h=a[e>>>24]^q[f>>16&255]^v[g>>8&255]^w[b&255]^d[p],k=a[f>>>24]^q[g>>16&255]^v[b>>8&255]^w[e&255]^d[p+1],l=a[g>>>24]^q[b>>16&255]^v[e>>8&255]^w[f&255]^d[p+2],b=a[b>>>24]^q[e>>16&255]^v[f>>8&255]^w[g&255]^d[p+3],p+=4,e=h,f=k,g=l;for(m=
|
||||||
|
0;4>m;m++)r[c?3&-m:m]=x[e>>>24]<<24^x[f>>16&255]<<16^x[g>>8&255]<<8^x[b&255]^d[p++],h=e,e=f,f=g,g=b,b=h;return r}
|
||||||
|
sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.$(a.slice(b/32),32-(b&31)).slice(1);return void 0===c?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<<c)-1},concat:function(a,b){if(0===a.length||0===b.length)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return 32===d?a.concat(b):sjcl.bitArray.$(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;return 0===
|
||||||
|
b?0:32*(b-1)+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(32*a.length<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b=b&31;0<c&&b&&(a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1));return a},partial:function(a,b,c){return 32===a?b:(c?b|0:b<<32-a)+0x10000000000*a},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return!1;var c=0,d;for(d=0;d<a.length;d++)c|=a[d]^b[d];return 0===
|
||||||
|
c},$:function(a,b,c,d){var e;e=0;for(void 0===d&&(d=[]);32<=b;b-=32)d.push(c),c=0;if(0===b)return d.concat(a);for(e=0;e<a.length;e++)d.push(c|a[e]>>>b),c=a[e]<<32-b;e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,32<b+a?c:d.pop(),1));return d},i:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]},byteswapM:function(a){var b,c;for(b=0;b<a.length;++b)c=a[b],a[b]=c>>>24|c>>>8&0xff00|(c&0xff00)<<8|c<<24;return a}};
|
||||||
|
sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d<c/8;d++)0===(d&3)&&(e=a[d/4]),b+=String.fromCharCode(e>>>8>>>8>>>8),e<<=8;return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++)d=d<<8|a.charCodeAt(c),3===(c&3)&&(b.push(d),d=0);c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
|
||||||
|
sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a=a+"00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,4*d)}};
|
||||||
|
sjcl.codec.base32={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",X:"0123456789ABCDEFGHIJKLMNOPQRSTUV",BITS:32,BASE:5,REMAINING:27,fromBits:function(a,b,c){var d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f="",g=0,h=sjcl.codec.base32.B,k=0,l=sjcl.bitArray.bitLength(a);c&&(h=sjcl.codec.base32.X);for(c=0;f.length*d<l;)f+=h.charAt((k^a[c]>>>g)>>>e),g<d?(k=a[c]<<d-g,g+=e,c++):(k<<=d,g-=d);for(;f.length&7&&!b;)f+="=";return f},toBits:function(a,b){a=a.replace(/\s|=/g,"").toUpperCase();var c=sjcl.codec.base32.BITS,
|
||||||
|
d=sjcl.codec.base32.BASE,e=sjcl.codec.base32.REMAINING,f=[],g,h=0,k=sjcl.codec.base32.B,l=0,n,m="base32";b&&(k=sjcl.codec.base32.X,m="base32hex");for(g=0;g<a.length;g++){n=k.indexOf(a.charAt(g));if(0>n){if(!b)try{return sjcl.codec.base32hex.toBits(a)}catch(p){}throw new sjcl.exception.invalid("this isn't "+m+"!");}h>e?(h-=e,f.push(l^n>>>h),l=n<<c-h):(h+=d,l^=n<<c-h)}h&56&&f.push(sjcl.bitArray.partial(h&56,l,1));return f}};
|
||||||
|
sjcl.codec.base32hex={fromBits:function(a,b){return sjcl.codec.base32.fromBits(a,b,1)},toBits:function(a){return sjcl.codec.base32.toBits(a,1)}};
|
||||||
|
sjcl.codec.base64={B:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.B,g=0,h=sjcl.bitArray.bitLength(a);c&&(f=f.substr(0,62)+"-_");for(c=0;6*d.length<h;)d+=f.charAt((g^a[c]>>>e)>>>26),6>e?(g=a[c]<<6-e,e+=26,c++):(g<<=6,e-=6);for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d,e=0,f=sjcl.codec.base64.B,g=0,h;b&&(f=f.substr(0,62)+"-_");for(d=0;d<a.length;d++){h=f.indexOf(a.charAt(d));
|
||||||
|
if(0>h)throw new sjcl.exception.invalid("this isn't base64!");26<e?(e-=26,c.push(g^h>>>e),g=h<<32-e):(e+=6,g^=h<<32-e)}e&56&&c.push(sjcl.bitArray.partial(e&56,g,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.b[0]||this.O();a?(this.F=a.F.slice(0),this.A=a.A.slice(0),this.l=a.l):this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
|
||||||
|
sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.F=this.Y.slice(0);this.A=[];this.l=0;return this},update:function(a){"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));var b,c=this.A=sjcl.bitArray.concat(this.A,a);b=this.l;a=this.l=b+sjcl.bitArray.bitLength(a);if(0x1fffffffffffff<a)throw new sjcl.exception.invalid("Cannot hash more than 2^53 - 1 bits");if("undefined"!==typeof Uint32Array){var d=new Uint32Array(c),e=0;for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,d.subarray(16*e,
|
||||||
|
16*(e+1))),e+=1;c.splice(0,16*e)}else for(b=512+b-(512+b&0x1ff);b<=a;b+=512)u(this,c.splice(0,16));return this},finalize:function(){var a,b=this.A,c=this.F,b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.l/0x100000000));for(b.push(this.l|0);b.length;)u(this,b.splice(0,16));this.reset();return c},Y:[],b:[],O:function(){function a(a){return 0x100000000*(a-Math.floor(a))|0}for(var b=0,c=2,d,e;64>b;c++){e=!0;for(d=2;d*d<=c;d++)if(0===c%d){e=
|
||||||
|
!1;break}e&&(8>b&&(this.Y[b]=a(Math.pow(c,.5))),this.b[b]=a(Math.pow(c,1/3)),b++)}}};
|
||||||
|
function u(a,b){var c,d,e,f=a.F,g=a.b,h=f[0],k=f[1],l=f[2],n=f[3],m=f[4],p=f[5],r=f[6],q=f[7];for(c=0;64>c;c++)16>c?d=b[c]:(d=b[c+1&15],e=b[c+14&15],d=b[c&15]=(d>>>7^d>>>18^d>>>3^d<<25^d<<14)+(e>>>17^e>>>19^e>>>10^e<<15^e<<13)+b[c&15]+b[c+9&15]|0),d=d+q+(m>>>6^m>>>11^m>>>25^m<<26^m<<21^m<<7)+(r^m&(p^r))+g[c],q=r,r=p,p=m,m=n+d|0,n=l,l=k,k=h,h=d+(k&l^n&(k^l))+(k>>>2^k>>>13^k>>>22^k<<30^k<<19^k<<10)|0;f[0]=f[0]+h|0;f[1]=f[1]+k|0;f[2]=f[2]+l|0;f[3]=f[3]+n|0;f[4]=f[4]+m|0;f[5]=f[5]+p|0;f[6]=f[6]+r|0;f[7]=
|
||||||
|
f[7]+q|0}
|
||||||
|
sjcl.mode.ccm={name:"ccm",G:[],listenProgress:function(a){sjcl.mode.ccm.G.push(a)},unListenProgress:function(a){a=sjcl.mode.ccm.G.indexOf(a);-1<a&&sjcl.mode.ccm.G.splice(a,1)},fa:function(a){var b=sjcl.mode.ccm.G.slice(),c;for(c=0;c<b.length;c+=1)b[c](a)},encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,k=h.bitLength(c)/8,l=h.bitLength(g)/8;e=e||64;d=d||[];if(7>k)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;4>f&&l>>>8*f;f++);f<15-k&&(f=15-k);c=h.clamp(c,
|
||||||
|
8*(15-f));b=sjcl.mode.ccm.V(a,b,c,d,e,f);g=sjcl.mode.ccm.C(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),k=f.clamp(b,h-e),l=f.bitSlice(b,h-e),h=(h-e)/8;if(7>g)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;4>b&&h>>>8*b;b++);b<15-g&&(b=15-g);c=f.clamp(c,8*(15-b));k=sjcl.mode.ccm.C(a,k,c,l,e,b);a=sjcl.mode.ccm.V(a,k.data,c,d,e,b);if(!f.equal(k.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");
|
||||||
|
return k.data},na:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,k=h.i;d=[h.partial(8,(b.length?64:0)|d-2<<2|f-1)];d=h.concat(d,c);d[3]|=e;d=a.encrypt(d);if(b.length)for(c=h.bitLength(b)/8,65279>=c?g=[h.partial(16,c)]:0xffffffff>=c&&(g=h.concat([h.partial(16,65534)],[c])),g=h.concat(g,b),b=0;b<g.length;b+=4)d=a.encrypt(k(d,g.slice(b,b+4).concat([0,0,0])));return d},V:function(a,b,c,d,e,f){var g=sjcl.bitArray,h=g.i;e/=8;if(e%2||4>e||16<e)throw new sjcl.exception.invalid("ccm: invalid tag length");
|
||||||
|
if(0xffffffff<d.length||0xffffffff<b.length)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");c=sjcl.mode.ccm.na(a,d,c,e,g.bitLength(b)/8,f);for(d=0;d<b.length;d+=4)c=a.encrypt(h(c,b.slice(d,d+4).concat([0,0,0])));return g.clamp(c,8*e)},C:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.i;var k=b.length,l=h.bitLength(b),n=k/50,m=n;c=h.concat([h.partial(8,f-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(g(d,a.encrypt(c)),0,e);if(!k)return{tag:d,data:[]};for(g=0;g<k;g+=4)g>n&&(sjcl.mode.ccm.fa(g/
|
||||||
|
k),n+=m),c[3]++,e=a.encrypt(c),b[g]^=e[0],b[g+1]^=e[1],b[g+2]^=e[2],b[g+3]^=e[3];return{tag:d,data:h.clamp(b,l)}}};
|
||||||
|
sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.S,k=sjcl.bitArray,l=k.i,n=[0,0,0,0];c=h(a.encrypt(c));var m,p=[];d=d||[];e=e||64;for(g=0;g+4<b.length;g+=4)m=b.slice(g,g+4),n=l(n,m),p=p.concat(l(c,a.encrypt(l(c,m)))),c=h(c);m=b.slice(g);b=k.bitLength(m);g=a.encrypt(l(c,[0,0,0,b]));m=k.clamp(l(m.concat([0,0,0]),g),b);n=l(n,l(m.concat([0,0,0]),g));n=a.encrypt(l(n,l(c,h(c))));
|
||||||
|
d.length&&(n=l(n,f?d:sjcl.mode.ocb2.pmac(a,d)));return p.concat(k.concat(m,k.clamp(n,e)))},decrypt:function(a,b,c,d,e,f){if(128!==sjcl.bitArray.bitLength(c))throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.S,h=sjcl.bitArray,k=h.i,l=[0,0,0,0],n=g(a.encrypt(c)),m,p,r=sjcl.bitArray.bitLength(b)-e,q=[];d=d||[];for(c=0;c+4<r/32;c+=4)m=k(n,a.decrypt(k(n,b.slice(c,c+4)))),l=k(l,m),q=q.concat(m),n=g(n);p=r-32*c;m=a.encrypt(k(n,[0,0,0,p]));m=k(m,h.clamp(b.slice(c),p).concat([0,
|
||||||
|
0,0]));l=k(l,m);l=a.encrypt(k(l,k(n,g(n))));d.length&&(l=k(l,f?d:sjcl.mode.ocb2.pmac(a,d)));if(!h.equal(h.clamp(l,e),h.bitSlice(b,r)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return q.concat(h.clamp(m,p))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.S,e=sjcl.bitArray,f=e.i,g=[0,0,0,0],h=a.encrypt([0,0,0,0]),h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4)h=d(h),g=f(g,a.encrypt(f(h,b.slice(c,c+4))));c=b.slice(c);128>e.bitLength(c)&&(h=f(h,d(h)),c=e.concat(c,[-2147483648,0,0,0]));g=f(g,c);
|
||||||
|
return a.encrypt(f(d(f(h,d(h))),g))},S:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^135*(a[0]>>>31)]}};
|
||||||
|
sjcl.mode.gcm={name:"gcm",encrypt:function(a,b,c,d,e){var f=b.slice(0);b=sjcl.bitArray;d=d||[];a=sjcl.mode.gcm.C(!0,a,f,d,c,e||128);return b.concat(a.data,a.tag)},decrypt:function(a,b,c,d,e){var f=b.slice(0),g=sjcl.bitArray,h=g.bitLength(f);e=e||128;d=d||[];e<=h?(b=g.bitSlice(f,h-e),f=g.bitSlice(f,0,h-e)):(b=f,f=[]);a=sjcl.mode.gcm.C(!1,a,f,d,c,e);if(!g.equal(a.tag,b))throw new sjcl.exception.corrupt("gcm: tag doesn't match");return a.data},ka:function(a,b){var c,d,e,f,g,h=sjcl.bitArray.i;e=[0,0,
|
||||||
|
0,0];f=b.slice(0);for(c=0;128>c;c++){(d=0!==(a[Math.floor(c/32)]&1<<31-c%32))&&(e=h(e,f));g=0!==(f[3]&1);for(d=3;0<d;d--)f[d]=f[d]>>>1|(f[d-1]&1)<<31;f[0]>>>=1;g&&(f[0]^=-0x1f000000)}return e},j:function(a,b,c){var d,e=c.length;b=b.slice(0);for(d=0;d<e;d+=4)b[0]^=0xffffffff&c[d],b[1]^=0xffffffff&c[d+1],b[2]^=0xffffffff&c[d+2],b[3]^=0xffffffff&c[d+3],b=sjcl.mode.gcm.ka(b,a);return b},C:function(a,b,c,d,e,f){var g,h,k,l,n,m,p,r,q=sjcl.bitArray;m=c.length;p=q.bitLength(c);r=q.bitLength(d);h=q.bitLength(e);
|
||||||
|
g=b.encrypt([0,0,0,0]);96===h?(e=e.slice(0),e=q.concat(e,[1])):(e=sjcl.mode.gcm.j(g,[0,0,0,0],e),e=sjcl.mode.gcm.j(g,e,[0,0,Math.floor(h/0x100000000),h&0xffffffff]));h=sjcl.mode.gcm.j(g,[0,0,0,0],d);n=e.slice(0);d=h.slice(0);a||(d=sjcl.mode.gcm.j(g,h,c));for(l=0;l<m;l+=4)n[3]++,k=b.encrypt(n),c[l]^=k[0],c[l+1]^=k[1],c[l+2]^=k[2],c[l+3]^=k[3];c=q.clamp(c,p);a&&(d=sjcl.mode.gcm.j(g,h,c));a=[Math.floor(r/0x100000000),r&0xffffffff,Math.floor(p/0x100000000),p&0xffffffff];d=sjcl.mode.gcm.j(g,d,a);k=b.encrypt(e);
|
||||||
|
d[0]^=k[0];d[1]^=k[1];d[2]^=k[2];d[3]^=k[3];return{tag:q.bitSlice(d,0,f),data:c}}};sjcl.misc.hmac=function(a,b){this.W=b=b||sjcl.hash.sha256;var c=[[],[]],d,e=b.prototype.blockSize/32;this.w=[new b,new b];a.length>e&&(a=b.hash(a));for(d=0;d<e;d++)c[0][d]=a[d]^909522486,c[1][d]=a[d]^1549556828;this.w[0].update(c[0]);this.w[1].update(c[1]);this.R=new b(this.w[0])};
|
||||||
|
sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a){if(this.aa)throw new sjcl.exception.invalid("encrypt on already updated hmac called!");this.update(a);return this.digest(a)};sjcl.misc.hmac.prototype.reset=function(){this.R=new this.W(this.w[0]);this.aa=!1};sjcl.misc.hmac.prototype.update=function(a){this.aa=!0;this.R.update(a)};sjcl.misc.hmac.prototype.digest=function(){var a=this.R.finalize(),a=(new this.W(this.w[1])).update(a).finalize();this.reset();return a};
|
||||||
|
sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E4;if(0>d||0>c)throw new sjcl.exception.invalid("invalid params to pbkdf2");"string"===typeof a&&(a=sjcl.codec.utf8String.toBits(a));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,k,l=[],n=sjcl.bitArray;for(k=1;32*l.length<(d||1);k++){e=f=a.encrypt(n.concat(b,[k]));for(g=1;g<c;g++)for(f=a.encrypt(f),h=0;h<f.length;h++)e[h]^=f[h];l=l.concat(e)}d&&(l=n.clamp(l,d));return l};
|
||||||
|
sjcl.prng=function(a){this.c=[new sjcl.hash.sha256];this.m=[0];this.P=0;this.H={};this.N=0;this.U={};this.Z=this.f=this.o=this.ha=0;this.b=[0,0,0,0,0,0,0,0];this.h=[0,0,0,0];this.L=void 0;this.M=a;this.D=!1;this.K={progress:{},seeded:{}};this.u=this.ga=0;this.I=1;this.J=2;this.ca=0x10000;this.T=[0,48,64,96,128,192,0x100,384,512,768,1024];this.da=3E4;this.ba=80};
|
||||||
|
sjcl.prng.prototype={randomWords:function(a,b){var c=[],d;d=this.isReady(b);var e;if(d===this.u)throw new sjcl.exception.notReady("generator isn't seeded");if(d&this.J){d=!(d&this.I);e=[];var f=0,g;this.Z=e[0]=(new Date).valueOf()+this.da;for(g=0;16>g;g++)e.push(0x100000000*Math.random()|0);for(g=0;g<this.c.length&&(e=e.concat(this.c[g].finalize()),f+=this.m[g],this.m[g]=0,d||!(this.P&1<<g));g++);this.P>=1<<this.c.length&&(this.c.push(new sjcl.hash.sha256),this.m.push(0));this.f-=f;f>this.o&&(this.o=
|
||||||
|
f);this.P++;this.b=sjcl.hash.sha256.hash(this.b.concat(e));this.L=new sjcl.cipher.aes(this.b);for(d=0;4>d&&(this.h[d]=this.h[d]+1|0,!this.h[d]);d++);}for(d=0;d<a;d+=4)0===(d+1)%this.ca&&y(this),e=z(this),c.push(e[0],e[1],e[2],e[3]);y(this);return c.slice(0,a)},setDefaultParanoia:function(a,b){if(0===a&&"Setting paranoia=0 will ruin your security; use it only for testing"!==b)throw new sjcl.exception.invalid("Setting paranoia=0 will ruin your security; use it only for testing");this.M=a},addEntropy:function(a,
|
||||||
|
b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.H[c],h=this.isReady(),k=0;d=this.U[c];void 0===d&&(d=this.U[c]=this.ha++);void 0===g&&(g=this.H[c]=0);this.H[c]=(this.H[c]+1)%this.c.length;switch(typeof a){case "number":void 0===b&&(b=1);this.c[g].update([d,this.N++,1,b,f,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if("[object Uint32Array]"===c){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else for("[object Array]"!==c&&(k=1),c=0;c<a.length&&!k;c++)"number"!==typeof a[c]&&
|
||||||
|
(k=1);if(!k){if(void 0===b)for(c=b=0;c<a.length;c++)for(e=a[c];0<e;)b++,e=e>>>1;this.c[g].update([d,this.N++,2,b,f,a.length].concat(a))}break;case "string":void 0===b&&(b=a.length);this.c[g].update([d,this.N++,3,b,f,a.length]);this.c[g].update(a);break;default:k=1}if(k)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.m[g]+=b;this.f+=b;h===this.u&&(this.isReady()!==this.u&&A("seeded",Math.max(this.o,this.f)),A("progress",this.getProgress()))},
|
||||||
|
isReady:function(a){a=this.T[void 0!==a?a:this.M];return this.o&&this.o>=a?this.m[0]>this.ba&&(new Date).valueOf()>this.Z?this.J|this.I:this.I:this.f>=a?this.J|this.u:this.u},getProgress:function(a){a=this.T[a?a:this.M];return this.o>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.D){this.a={loadTimeCollector:B(this,this.ma),mouseCollector:B(this,this.oa),keyboardCollector:B(this,this.la),accelerometerCollector:B(this,this.ea),touchCollector:B(this,this.qa)};if(window.addEventListener)window.addEventListener("load",
|
||||||
|
this.a.loadTimeCollector,!1),window.addEventListener("mousemove",this.a.mouseCollector,!1),window.addEventListener("keypress",this.a.keyboardCollector,!1),window.addEventListener("devicemotion",this.a.accelerometerCollector,!1),window.addEventListener("touchmove",this.a.touchCollector,!1);else if(document.attachEvent)document.attachEvent("onload",this.a.loadTimeCollector),document.attachEvent("onmousemove",this.a.mouseCollector),document.attachEvent("keypress",this.a.keyboardCollector);else throw new sjcl.exception.bug("can't attach event");
|
||||||
|
this.D=!0}},stopCollectors:function(){this.D&&(window.removeEventListener?(window.removeEventListener("load",this.a.loadTimeCollector,!1),window.removeEventListener("mousemove",this.a.mouseCollector,!1),window.removeEventListener("keypress",this.a.keyboardCollector,!1),window.removeEventListener("devicemotion",this.a.accelerometerCollector,!1),window.removeEventListener("touchmove",this.a.touchCollector,!1)):document.detachEvent&&(document.detachEvent("onload",this.a.loadTimeCollector),document.detachEvent("onmousemove",
|
||||||
|
this.a.mouseCollector),document.detachEvent("keypress",this.a.keyboardCollector)),this.D=!1)},addEventListener:function(a,b){this.K[a][this.ga++]=b},removeEventListener:function(a,b){var c,d,e=this.K[a],f=[];for(d in e)e.hasOwnProperty(d)&&e[d]===b&&f.push(d);for(c=0;c<f.length;c++)d=f[c],delete e[d]},la:function(){C(this,1)},oa:function(a){var b,c;try{b=a.x||a.clientX||a.offsetX||0,c=a.y||a.clientY||a.offsetY||0}catch(d){c=b=0}0!=b&&0!=c&&this.addEntropy([b,c],2,"mouse");C(this,0)},qa:function(a){a=
|
||||||
|
a.touches[0]||a.changedTouches[0];this.addEntropy([a.pageX||a.clientX,a.pageY||a.clientY],1,"touch");C(this,0)},ma:function(){C(this,2)},ea:function(a){a=a.accelerationIncludingGravity.x||a.accelerationIncludingGravity.y||a.accelerationIncludingGravity.z;if(window.orientation){var b=window.orientation;"number"===typeof b&&this.addEntropy(b,1,"accelerometer")}a&&this.addEntropy(a,2,"accelerometer");C(this,0)}};
|
||||||
|
function A(a,b){var c,d=sjcl.random.K[a],e=[];for(c in d)d.hasOwnProperty(c)&&e.push(d[c]);for(c=0;c<e.length;c++)e[c](b)}function C(a,b){"undefined"!==typeof window&&window.performance&&"function"===typeof window.performance.now?a.addEntropy(window.performance.now(),b,"loadtime"):a.addEntropy((new Date).valueOf(),b,"loadtime")}function y(a){a.b=z(a).concat(z(a));a.L=new sjcl.cipher.aes(a.b)}function z(a){for(var b=0;4>b&&(a.h[b]=a.h[b]+1|0,!a.h[b]);b++);return a.L.encrypt(a.h)}
|
||||||
|
function B(a,b){return function(){b.apply(a,arguments)}}sjcl.random=new sjcl.prng(6);
|
||||||
|
a:try{var D,E,F,G;if(G="undefined"!==typeof module&&module.exports){var H;try{H=null}catch(a){H=null}G=E=H}if(G&&E.randomBytes)D=E.randomBytes(128),D=new Uint32Array((new Uint8Array(D)).buffer),sjcl.random.addEntropy(D,1024,"crypto['randomBytes']");else if("undefined"!==typeof window&&"undefined"!==typeof Uint32Array){F=new Uint32Array(32);if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(F);else if(window.msCrypto&&window.msCrypto.getRandomValues)window.msCrypto.getRandomValues(F);
|
||||||
|
else break a;sjcl.random.addEntropy(F,1024,"crypto['getRandomValues']")}}catch(a){"undefined"!==typeof window&&window.console&&(console.log("There was an error collecting entropy from the browser:"),console.log(a))}
|
||||||
|
sjcl.json={defaults:{v:1,iter:1E4,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},ja:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.g({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.g(f,c);c=f.adata;"string"===typeof f.salt&&(f.salt=sjcl.codec.base64.toBits(f.salt));"string"===typeof f.iv&&(f.iv=sjcl.codec.base64.toBits(f.iv));if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||"string"===typeof a&&100>=f.iter||64!==f.ts&&96!==f.ts&&128!==f.ts||128!==f.ks&&192!==f.ks&&0x100!==f.ks||2>f.iv.length||
|
||||||
|
4<f.iv.length)throw new sjcl.exception.invalid("json encrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,f),a=g.key.slice(0,f.ks/32),f.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.publicKey&&(g=a.kem(),f.kemtag=g.tag,a=g.key.slice(0,f.ks/32));"string"===typeof b&&(b=sjcl.codec.utf8String.toBits(b));"string"===typeof c&&(f.adata=c=sjcl.codec.utf8String.toBits(c));g=new sjcl.cipher[f.cipher](a);e.g(d,f);d.key=a;f.ct="ccm"===f.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&
|
||||||
|
b instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.encrypt(g,b,f.iv,c,f.ts):sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return f},encrypt:function(a,b,c,d){var e=sjcl.json,f=e.ja.apply(e,arguments);return e.encode(f)},ia:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.g(e.g(e.g({},e.defaults),b),c,!0);var f,g;f=b.adata;"string"===typeof b.salt&&(b.salt=sjcl.codec.base64.toBits(b.salt));"string"===typeof b.iv&&(b.iv=sjcl.codec.base64.toBits(b.iv));if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||"string"===
|
||||||
|
typeof a&&100>=b.iter||64!==b.ts&&96!==b.ts&&128!==b.ts||128!==b.ks&&192!==b.ks&&0x100!==b.ks||!b.iv||2>b.iv.length||4<b.iv.length)throw new sjcl.exception.invalid("json decrypt: invalid parameters");"string"===typeof a?(g=sjcl.misc.cachedPbkdf2(a,b),a=g.key.slice(0,b.ks/32),b.salt=g.salt):sjcl.ecc&&a instanceof sjcl.ecc.elGamal.secretKey&&(a=a.unkem(sjcl.codec.base64.toBits(b.kemtag)).slice(0,b.ks/32));"string"===typeof f&&(f=sjcl.codec.utf8String.toBits(f));g=new sjcl.cipher[b.cipher](a);f="ccm"===
|
||||||
|
b.mode&&sjcl.arrayBuffer&&sjcl.arrayBuffer.ccm&&b.ct instanceof ArrayBuffer?sjcl.arrayBuffer.ccm.decrypt(g,b.ct,b.iv,b.tag,f,b.ts):sjcl.mode[b.mode].decrypt(g,b.ct,b.iv,f,b.ts);e.g(d,b);d.key=a;return 1===c.raw?f:sjcl.codec.utf8String.fromBits(f)},decrypt:function(a,b,c,d){var e=sjcl.json;return e.ia(a,e.decode(b),c,d)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+
|
||||||
|
b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],0)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^\s*(?:(["']?)([a-z][a-z0-9]*)\1)\s*:\s*(?:(-?\d+)|"([a-z0-9+\/%*_.@=\-]*)"|(true|false))$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");
|
||||||
|
null!=d[3]?b[d[2]]=parseInt(d[3],10):null!=d[4]?b[d[2]]=d[2].match(/^(ct|adata|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4]):null!=d[5]&&(b[d[2]]="true"===d[5])}return b},g:function(a,b,c){void 0===a&&(a={});if(void 0===b)return a;for(var d in b)if(b.hasOwnProperty(d)){if(c&&void 0!==a[d]&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},sa:function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&a[d]!==b[d]&&(c[d]=a[d]);return c},ra:function(a,
|
||||||
|
b){var c={},d;for(d=0;d<b.length;d++)void 0!==a[b[d]]&&(c[b[d]]=a[b[d]]);return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.pa={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.pa,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=void 0===b.salt?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
|
||||||
|
"undefined"!==typeof module&&module.exports&&(module.exports=sjcl);"function"===typeof define&&define([],function(){return sjcl});
|
||||||
146
client/share/sjclWrapper.js
Normal file
146
client/share/sjclWrapper.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
//sjcl.js подправлен (убран лишний require, добавлявший +400kb к bundle) и скопирован локально
|
||||||
|
import sjcl from './sjcl';
|
||||||
|
|
||||||
|
//везде недоработки...
|
||||||
|
|
||||||
|
sjcl.codec.bytes = {
|
||||||
|
fromBits: function(arr) {
|
||||||
|
var out = [], bl = sjcl.bitArray.bitLength(arr), i, tmp;
|
||||||
|
for (i=0; i<bl/8; i++) {
|
||||||
|
if ((i&3) === 0) {
|
||||||
|
tmp = arr[i/4];
|
||||||
|
}
|
||||||
|
out.push(tmp >>> 24);
|
||||||
|
tmp <<= 8;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
toBits: function(bytes) {
|
||||||
|
var out = [], i, tmp=0;
|
||||||
|
for (i=0; i<bytes.length; i++) {
|
||||||
|
tmp = tmp << 8 | bytes[i];
|
||||||
|
if ((i&3) === 3) {
|
||||||
|
out.push(tmp);
|
||||||
|
tmp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i&3) {
|
||||||
|
out.push(sjcl.bitArray.partial(8*(i&3), tmp));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sjcl.json._add = function(target, src, requireSame) {
|
||||||
|
if (target === undefined) { target = {}; }
|
||||||
|
if (src === undefined) { return target; }
|
||||||
|
var i;
|
||||||
|
for (i in src) {
|
||||||
|
if (src.hasOwnProperty(i)) {
|
||||||
|
if (requireSame && target[i] !== undefined && target[i] !== src[i]) {
|
||||||
|
throw new sjcl.exception.invalid("required parameter overridden");
|
||||||
|
}
|
||||||
|
target[i] = src[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
sjcl.encryptArray = function(password, plaintext, params, rp) {
|
||||||
|
params = params || {};
|
||||||
|
rp = rp || {};
|
||||||
|
|
||||||
|
var j = sjcl.json, p = j._add({ iv: sjcl.random.randomWords(4,0) },
|
||||||
|
j.defaults), tmp, prp, adata;
|
||||||
|
j._add(p, params);
|
||||||
|
adata = p.adata;
|
||||||
|
if (typeof p.salt === "string") {
|
||||||
|
p.salt = sjcl.codec.base64.toBits(p.salt);
|
||||||
|
}
|
||||||
|
if (typeof p.iv === "string") {
|
||||||
|
p.iv = sjcl.codec.base64.toBits(p.iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sjcl.mode[p.mode] ||
|
||||||
|
!sjcl.cipher[p.cipher] ||
|
||||||
|
(typeof password === "string" && p.iter <= 100) ||
|
||||||
|
(p.ts !== 64 && p.ts !== 96 && p.ts !== 128) ||
|
||||||
|
(p.ks !== 128 && p.ks !== 192 && p.ks !== 256) ||
|
||||||
|
(p.iv.length < 2 || p.iv.length > 4)) {
|
||||||
|
throw new sjcl.exception.invalid("json encrypt: invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof password === "string") {
|
||||||
|
tmp = sjcl.misc.cachedPbkdf2(password, p);
|
||||||
|
password = tmp.key.slice(0,p.ks/32);
|
||||||
|
p.salt = tmp.salt;
|
||||||
|
} else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.publicKey) {
|
||||||
|
tmp = password.kem();
|
||||||
|
p.kemtag = tmp.tag;
|
||||||
|
password = tmp.key.slice(0,p.ks/32);
|
||||||
|
}
|
||||||
|
if (typeof plaintext === "string") {
|
||||||
|
plaintext = sjcl.codec.utf8String.toBits(plaintext);
|
||||||
|
}
|
||||||
|
if (typeof adata === "string") {
|
||||||
|
p.adata = adata = sjcl.codec.utf8String.toBits(adata);
|
||||||
|
}
|
||||||
|
prp = new sjcl.cipher[p.cipher](password);
|
||||||
|
|
||||||
|
j._add(rp, p);
|
||||||
|
rp.key = password;
|
||||||
|
|
||||||
|
/* do the encryption */
|
||||||
|
if (p.mode === "ccm" && sjcl.arrayBuffer && sjcl.arrayBuffer.ccm && plaintext instanceof ArrayBuffer) {
|
||||||
|
p.ct = sjcl.arrayBuffer.ccm.encrypt(prp, plaintext, p.iv, adata, p.ts);
|
||||||
|
} else {
|
||||||
|
p.ct = sjcl.mode[p.mode].encrypt(prp, plaintext, p.iv, adata, p.ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
sjcl.decryptArray = function(password, ciphertext, params) {
|
||||||
|
params = params || {};
|
||||||
|
|
||||||
|
var j = sjcl.json, p = j._add(j._add(j._add({},j.defaults),ciphertext), params, true), ct, tmp, prp, adata=p.adata;
|
||||||
|
if (typeof p.salt === "string") {
|
||||||
|
p.salt = sjcl.codec.base64.toBits(p.salt);
|
||||||
|
}
|
||||||
|
if (typeof p.iv === "string") {
|
||||||
|
p.iv = sjcl.codec.base64.toBits(p.iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sjcl.mode[p.mode] ||
|
||||||
|
!sjcl.cipher[p.cipher] ||
|
||||||
|
(typeof password === "string" && p.iter <= 100) ||
|
||||||
|
(p.ts !== 64 && p.ts !== 96 && p.ts !== 128) ||
|
||||||
|
(p.ks !== 128 && p.ks !== 192 && p.ks !== 256) ||
|
||||||
|
(!p.iv) ||
|
||||||
|
(p.iv.length < 2 || p.iv.length > 4)) {
|
||||||
|
throw new sjcl.exception.invalid("json decrypt: invalid parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof password === "string") {
|
||||||
|
tmp = sjcl.misc.cachedPbkdf2(password, p);
|
||||||
|
password = tmp.key.slice(0,p.ks/32);
|
||||||
|
p.salt = tmp.salt;
|
||||||
|
} else if (sjcl.ecc && password instanceof sjcl.ecc.elGamal.secretKey) {
|
||||||
|
password = password.unkem(sjcl.codec.base64.toBits(p.kemtag)).slice(0,p.ks/32);
|
||||||
|
}
|
||||||
|
if (typeof adata === "string") {
|
||||||
|
adata = sjcl.codec.utf8String.toBits(adata);
|
||||||
|
}
|
||||||
|
prp = new sjcl.cipher[p.cipher](password);
|
||||||
|
|
||||||
|
/* do the decryption */
|
||||||
|
if (p.mode === "ccm" && sjcl.arrayBuffer && sjcl.arrayBuffer.ccm && p.ct instanceof ArrayBuffer) {
|
||||||
|
ct = sjcl.arrayBuffer.ccm.decrypt(prp, p.ct, p.iv, p.tag, adata, p.ts);
|
||||||
|
} else {
|
||||||
|
ct = sjcl.mode[p.mode].decrypt(prp, p.ct, p.iv, adata, p.ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sjcl;
|
||||||
@@ -1,21 +1,34 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import baseX from 'base-x';
|
||||||
|
import PAKO from 'pako';
|
||||||
|
import {Buffer} from 'safe-buffer';
|
||||||
|
import sjclWrapper from './sjclWrapper';
|
||||||
|
|
||||||
|
export const pako = PAKO;
|
||||||
|
|
||||||
|
const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||||
|
const bs58 = baseX(BASE58);
|
||||||
|
|
||||||
export function sleep(ms) {
|
export function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringToHex(str) {
|
export function stringToHex(str) {
|
||||||
let result = '';
|
return Buffer.from(str).toString('hex');
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
result += str.charCodeAt(i).toString(16);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hexToString(str) {
|
export function hexToString(str) {
|
||||||
let result = '';
|
return Buffer.from(str, 'hex').toString();
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
}
|
||||||
result += String.fromCharCode(parseInt(str.substr(i, 2), 16));
|
|
||||||
}
|
export function randomArray(len) {
|
||||||
return result;
|
const a = new Uint8Array(len);
|
||||||
|
window.crypto.getRandomValues(a);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomHexString(len) {
|
||||||
|
return Buffer.from(randomArray(len)).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(d, format) {
|
export function formatDate(d, format) {
|
||||||
@@ -26,6 +39,10 @@ export function formatDate(d, format) {
|
|||||||
case 'normal':
|
case 'normal':
|
||||||
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` +
|
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` +
|
||||||
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
||||||
|
case 'coDate':
|
||||||
|
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
|
||||||
|
case 'noDate':
|
||||||
|
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -62,4 +79,118 @@ export async function copyTextToClipboard(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toBase58(data) {
|
||||||
|
return bs58.encode(Buffer.from(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromBase58(data) {
|
||||||
|
return bs58.decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
//base-x слишком тормозит, используем sjcl
|
||||||
|
export function toBase64(data) {
|
||||||
|
return sjclWrapper.codec.base64.fromBits(
|
||||||
|
sjclWrapper.codec.bytes.toBits(Buffer.from(data))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//base-x слишком тормозит, используем sjcl
|
||||||
|
export function fromBase64(data) {
|
||||||
|
return Buffer.from(sjclWrapper.codec.bytes.fromBits(
|
||||||
|
sjclWrapper.codec.base64.toBits(data)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getObjDiff(oldObj, newObj) {
|
||||||
|
const result = {__isDiff: true, change: {}, add: {}, del: []};
|
||||||
|
|
||||||
|
for (const key of Object.keys(oldObj)) {
|
||||||
|
if (newObj.hasOwnProperty(key)) {
|
||||||
|
if (!_.isEqual(oldObj[key], newObj[key])) {
|
||||||
|
if (_.isObject(oldObj[key]) && _.isObject(newObj[key])) {
|
||||||
|
result.change[key] = getObjDiff(oldObj[key], newObj[key]);
|
||||||
|
} else {
|
||||||
|
result.change[key] = _.cloneDeep(newObj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.del.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(newObj)) {
|
||||||
|
if (!oldObj.hasOwnProperty(key)) {
|
||||||
|
result.add[key] = _.cloneDeep(newObj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObjDiff(diff) {
|
||||||
|
return (_.isObject(diff) && diff.__isDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmptyObjDiff(diff) {
|
||||||
|
return (!_.isObject(diff) || !diff.__isDiff ||
|
||||||
|
(!Object.keys(diff.change).length &&
|
||||||
|
!Object.keys(diff.add).length &&
|
||||||
|
!diff.del.length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyObjDiff(obj, diff, isAddChanged) {
|
||||||
|
const result = _.cloneDeep(obj);
|
||||||
|
if (!diff.__isDiff)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
const change = diff.change;
|
||||||
|
for (const key of Object.keys(change)) {
|
||||||
|
if (result.hasOwnProperty(key)) {
|
||||||
|
if (_.isObject(change[key])) {
|
||||||
|
result[key] = applyObjDiff(result[key], change[key], isAddChanged);
|
||||||
|
} else {
|
||||||
|
result[key] = _.cloneDeep(change[key]);
|
||||||
|
}
|
||||||
|
} else if (isAddChanged) {
|
||||||
|
result[key] = _.cloneDeep(change[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff.add)) {
|
||||||
|
result[key] = _.cloneDeep(diff.add[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of diff.del) {
|
||||||
|
delete result[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseQuery(str) {
|
||||||
|
if (typeof str != 'string' || str.length == 0)
|
||||||
|
return {};
|
||||||
|
let s = str.split('&');
|
||||||
|
let s_length = s.length;
|
||||||
|
let bit, query = {}, first, second;
|
||||||
|
|
||||||
|
for (let i = 0; i < s_length; i++) {
|
||||||
|
bit = s[i].split('=');
|
||||||
|
first = decodeURIComponent(bit[0]);
|
||||||
|
if (first.length == 0)
|
||||||
|
continue;
|
||||||
|
second = decodeURIComponent(bit[1]);
|
||||||
|
if (typeof query[first] == 'undefined')
|
||||||
|
query[first] = second;
|
||||||
|
else
|
||||||
|
if (query[first] instanceof Array)
|
||||||
|
query[first].push(second);
|
||||||
|
else
|
||||||
|
query[first] = [query[first], second];
|
||||||
|
}
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
|
||||||
|
const toolButtons = [
|
||||||
|
{name: 'undoAction', show: true, text: 'Действие назад'},
|
||||||
|
{name: 'redoAction', show: true, text: 'Действие вперед'},
|
||||||
|
{name: 'fullScreen', show: true, text: 'На весь экран'},
|
||||||
|
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
|
||||||
|
{name: 'setPosition', show: true, text: 'На страницу'},
|
||||||
|
{name: 'search', show: true, text: 'Найти в тексте'},
|
||||||
|
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
||||||
|
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
||||||
|
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
|
||||||
|
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
||||||
|
];
|
||||||
|
|
||||||
const fonts = [
|
const fonts = [
|
||||||
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
|
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
|
||||||
{name: 'GEO_1', label: 'BPG Arial', fontVertShift: 10},
|
{name: 'GEO_1', label: 'BPG Arial', fontVertShift: 10},
|
||||||
@@ -123,61 +137,78 @@ const webFonts = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const settingDefaults = {
|
const settingDefaults = {
|
||||||
textColor: '#000000',
|
textColor: '#000000',
|
||||||
backgroundColor: '#EBE2C9',
|
backgroundColor: '#EBE2C9',
|
||||||
wallpaper: '',
|
wallpaper: '',
|
||||||
fontStyle: '',// 'italic'
|
fontStyle: '',// 'italic'
|
||||||
fontWeight: '',// 'bold'
|
fontWeight: '',// 'bold'
|
||||||
fontSize: 20,// px
|
fontSize: 20,// px
|
||||||
fontName: 'ReaderDefault',
|
fontName: 'ReaderDefault',
|
||||||
webFontName: '',
|
webFontName: '',
|
||||||
fontVertShift: 0,
|
fontVertShift: 0,
|
||||||
textVertShift: -20,
|
textVertShift: 0,
|
||||||
|
|
||||||
lineInterval: 3,// px, межстрочный интервал
|
lineInterval: 3,// px, межстрочный интервал
|
||||||
textAlignJustify: true,// выравнивание по ширине
|
textAlignJustify: true,// выравнивание по ширине
|
||||||
p: 25,// px, отступ параграфа
|
p: 25,// px, отступ параграфа
|
||||||
indentLR: 15,// px, отступ всего текста слева и справа
|
indentLR: 15,// px, отступ всего текста слева и справа
|
||||||
indentTB: 0,// px, отступ всего текста сверху и снизу
|
indentTB: 0,// px, отступ всего текста сверху и снизу
|
||||||
wordWrap: true,//перенос по слогам
|
wordWrap: true,//перенос по слогам
|
||||||
keepLastToFirst: true,// перенос последней строки в первую при листании
|
keepLastToFirst: false,// перенос последней строки в первую при листании
|
||||||
|
|
||||||
showStatusBar: true,
|
showStatusBar: true,
|
||||||
statusBarTop: false,// top, bottom
|
statusBarTop: false,// top, bottom
|
||||||
statusBarHeight: 19,// px
|
statusBarHeight: 19,// px
|
||||||
statusBarColorAlpha: 0.4,
|
statusBarColorAlpha: 0.4,
|
||||||
|
|
||||||
scrollingDelay: 3000,// замедление, ms
|
scrollingDelay: 3000,// замедление, ms
|
||||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||||
|
|
||||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание
|
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||||
pageChangeAnimationSpeed: 80, //0-100%
|
pageChangeAnimationSpeed: 80, //0-100%
|
||||||
|
|
||||||
allowUrlParamBookPos: false,
|
allowUrlParamBookPos: false,
|
||||||
lazyParseEnabled: false,
|
lazyParseEnabled: false,
|
||||||
copyFullText: false,
|
copyFullText: false,
|
||||||
showClickMapPage: true,
|
showClickMapPage: true,
|
||||||
clickControl: true,
|
clickControl: true,
|
||||||
cutEmptyParagraphs: false,
|
cutEmptyParagraphs: false,
|
||||||
addEmptyParagraphs: 0,
|
addEmptyParagraphs: 0,
|
||||||
blinkCachedLoad: true,
|
blinkCachedLoad: true,
|
||||||
showImages: true,
|
showImages: true,
|
||||||
showInlineImagesInCenter: true,
|
showInlineImagesInCenter: true,
|
||||||
imageHeightLines: 100,
|
compactTextPerc: 0,
|
||||||
imageFitWidth: true,
|
imageHeightLines: 100,
|
||||||
|
imageFitWidth: true,
|
||||||
|
showServerStorageMessages: true,
|
||||||
|
showWhatsNewDialog: true,
|
||||||
|
showMigrationDialog: true,
|
||||||
|
enableSitesFilter: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
|
showToolButton: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||||
for (const font of webFonts)
|
for (const font of webFonts)
|
||||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||||
|
for (const button of toolButtons)
|
||||||
|
settingDefaults.showToolButton[button.name] = button.show;
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
const state = {
|
const state = {
|
||||||
toolBarActive: true,
|
toolBarActive: true,
|
||||||
|
serverSyncEnabled: false,
|
||||||
|
serverStorageKey: '',
|
||||||
|
profiles: {},
|
||||||
|
profilesRev: 0,
|
||||||
|
allowProfilesSave: false,//подстраховка для разработки
|
||||||
|
whatsNewContentHash: '',
|
||||||
|
migrationRemindDate: '',
|
||||||
|
currentProfile: '',
|
||||||
settings: Object.assign({}, settingDefaults),
|
settings: Object.assign({}, settingDefaults),
|
||||||
|
settingsRev: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
@@ -191,12 +222,40 @@ const mutations = {
|
|||||||
setToolBarActive(state, value) {
|
setToolBarActive(state, value) {
|
||||||
state.toolBarActive = value;
|
state.toolBarActive = value;
|
||||||
},
|
},
|
||||||
|
setServerSyncEnabled(state, value) {
|
||||||
|
state.serverSyncEnabled = value;
|
||||||
|
},
|
||||||
|
setServerStorageKey(state, value) {
|
||||||
|
state.serverStorageKey = value;
|
||||||
|
},
|
||||||
|
setProfiles(state, value) {
|
||||||
|
state.profiles = value;
|
||||||
|
},
|
||||||
|
setProfilesRev(state, value) {
|
||||||
|
state.profilesRev = value;
|
||||||
|
},
|
||||||
|
setAllowProfilesSave(state, value) {
|
||||||
|
state.allowProfilesSave = value;
|
||||||
|
},
|
||||||
|
setWhatsNewContentHash(state, value) {
|
||||||
|
state.whatsNewContentHash = value;
|
||||||
|
},
|
||||||
|
setMigrationRemindDate(state, value) {
|
||||||
|
state.migrationRemindDate = value;
|
||||||
|
},
|
||||||
|
setCurrentProfile(state, value) {
|
||||||
|
state.currentProfile = value;
|
||||||
|
},
|
||||||
setSettings(state, value) {
|
setSettings(state, value) {
|
||||||
state.settings = Object.assign({}, state.settings, value);
|
state.settings = Object.assign({}, state.settings, value);
|
||||||
}
|
},
|
||||||
|
setSettingsRev(state, value) {
|
||||||
|
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
toolButtons,
|
||||||
fonts,
|
fonts,
|
||||||
webFonts,
|
webFonts,
|
||||||
settingDefaults,
|
settingDefaults,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
.el-alert{width:100%;padding:8px 16px;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;position:relative;background-color:#fff;overflow:hidden;opacity:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-transition:opacity .2s;transition:opacity .2s}.el-alert.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-alert--success{background-color:#f0f9eb;color:#67c23a}.el-alert--success .el-alert__description{color:#67c23a}.el-alert--info{background-color:#f4f4f5;color:#909399}.el-alert--info .el-alert__description{color:#909399}.el-alert--warning{background-color:#fdf6ec;color:#e6a23c}.el-alert--warning .el-alert__description{color:#e6a23c}.el-alert--error{background-color:#fef0f0;color:#f56c6c}.el-alert--error .el-alert__description{color:#f56c6c}.el-alert__content{display:table-cell;padding:0 8px}.el-alert__icon{font-size:16px;width:16px}.el-alert__icon.is-big{font-size:28px;width:28px}.el-alert__title{font-size:13px;line-height:18px}.el-alert__title.is-bold{font-weight:700}.el-alert .el-alert__description{font-size:12px;margin:5px 0 0}.el-alert__closebtn{font-size:12px;color:#c0c4cc;opacity:1;position:absolute;top:12px;right:15px;cursor:pointer}.el-alert__closebtn.is-customed{font-style:normal;font-size:13px;top:9px}.el-alert-fade-enter,.el-alert-fade-leave-active{opacity:0}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-aside{overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-badge{position:relative;vertical-align:middle;display:inline-block}.el-badge__content{background-color:#f56c6c;border-radius:10px;color:#fff;display:inline-block;font-size:12px;height:18px;line-height:18px;padding:0 6px;text-align:center;white-space:nowrap;border:1px solid #fff}.el-badge__content.is-fixed{position:absolute;top:0;right:10px;-webkit-transform:translateY(-50%) translateX(100%);transform:translateY(-50%) translateX(100%)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:#00468F}.el-badge__content--success{background-color:#67c23a}.el-badge__content--warning{background-color:#e6a23c}.el-badge__content--info{background-color:#909399}.el-badge__content--danger{background-color:#f56c6c}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb::after,.el-breadcrumb::before{display:table;content:""}.el-breadcrumb::after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner a,.el-breadcrumb__inner.is-link{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner a:hover,.el-breadcrumb__inner.is-link:hover{color:#00468F;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-card{border-radius:4px;border:1px solid #ebeef5;background-color:#fff;overflow:hidden;color:#303133;-webkit-transition:.3s;transition:.3s}.el-card.is-always-shadow,.el-card.is-hover-shadow:focus,.el-card.is-hover-shadow:hover{-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-card__header{padding:18px 20px;border-bottom:1px solid #ebeef5;-webkit-box-sizing:border-box;box-sizing:border-box}.el-card__body{padding:20px}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-carousel__item,.el-carousel__mask{position:absolute;height:100%;top:0;left:0}.el-carousel__item{width:100%;display:inline-block;overflow:hidden;z-index:0}.el-carousel__item.is-active{z-index:2}.el-carousel__item.is-animating{-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card{width:50%;-webkit-transition:-webkit-transform .4s ease-in-out;transition:-webkit-transform .4s ease-in-out;transition:transform .4s ease-in-out;transition:transform .4s ease-in-out,-webkit-transform .4s ease-in-out}.el-carousel__item--card.is-in-stage{cursor:pointer;z-index:1}.el-carousel__item--card.is-in-stage.is-hover .el-carousel__mask,.el-carousel__item--card.is-in-stage:hover .el-carousel__mask{opacity:.12}.el-carousel__item--card.is-active{z-index:2}.el-carousel__mask{width:100%;background-color:#fff;opacity:.24;-webkit-transition:.2s;transition:.2s}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-carousel{overflow-x:hidden;position:relative}.el-carousel__container{position:relative;height:300px}.el-carousel__arrow{border:none;outline:0;padding:0;margin:0;height:36px;width:36px;cursor:pointer;-webkit-transition:.3s;transition:.3s;border-radius:50%;background-color:rgba(31,45,61,.11);color:#fff;position:absolute;top:50%;z-index:10;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center;font-size:12px}.el-carousel__arrow--left{left:16px}.el-carousel__arrow--right{right:16px}.el-carousel__arrow:hover{background-color:rgba(31,45,61,.23)}.el-carousel__arrow i{cursor:pointer}.el-carousel__indicators{position:absolute;list-style:none;bottom:0;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);margin:0;padding:0;z-index:2}.el-carousel__indicators--outside{bottom:26px;text-align:center;position:static;-webkit-transform:none;transform:none}.el-carousel__indicators--outside .el-carousel__indicator:hover button{opacity:.64}.el-carousel__indicators--outside button{background-color:#c0c4cc;opacity:.24}.el-carousel__indicators--labels{left:0;right:0;-webkit-transform:none;transform:none;text-align:center}.el-carousel__indicators--labels .el-carousel__button{height:auto;width:auto;padding:2px 18px;font-size:12px}.el-carousel__indicators--labels .el-carousel__indicator{padding:6px 4px}.el-carousel__indicator{display:inline-block;background-color:transparent;padding:12px 4px;cursor:pointer}.el-carousel__indicator:hover button{opacity:.72}.el-carousel__indicator.is-active button{opacity:1}.el-carousel__button{display:block;opacity:.48;width:30px;height:2px;background-color:#fff;border:none;outline:0;padding:0;margin:0;cursor:pointer;-webkit-transition:.3s;transition:.3s}.carousel-arrow-left-enter,.carousel-arrow-left-leave-active{-webkit-transform:translateY(-50%) translateX(-10px);transform:translateY(-50%) translateX(-10px);opacity:0}.carousel-arrow-right-enter,.carousel-arrow-right-leave-active{-webkit-transform:translateY(-50%) translateX(10px);transform:translateY(-50%) translateX(10px);opacity:0}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:0}.el-container.is-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{100%{opacity:0}}@keyframes v-modal-out{100%{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-popup-parent--hidden{overflow:hidden}.el-dialog{position:relative;margin:0 auto 50px;background:#fff;border-radius:2px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.3);box-shadow:0 1px 3px rgba(0,0,0,.3);-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.el-dialog.is-fullscreen{width:100%;margin-top:0;margin-bottom:0;height:100%;overflow:auto}.el-dialog__wrapper{position:fixed;top:0;right:0;bottom:0;left:0;overflow:auto;margin:0}.el-dialog__header{padding:20px 20px 10px}.el-dialog__headerbtn{position:absolute;top:20px;right:20px;padding:0;background:0 0;border:none;outline:0;cursor:pointer;font-size:16px}.el-dialog__headerbtn .el-dialog__close{color:#909399}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:#00468F}.el-dialog__title{line-height:24px;font-size:18px;color:#303133}.el-dialog__body{padding:30px 20px;color:#606266;font-size:14px}.el-dialog__footer{padding:10px 20px 20px;text-align:right;-webkit-box-sizing:border-box;box-sizing:border-box}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial;padding:25px 25px 30px}.el-dialog--center .el-dialog__footer{text-align:inherit}.dialog-fade-enter-active{-webkit-animation:dialog-fade-in .3s;animation:dialog-fade-in .3s}.dialog-fade-leave-active{-webkit-animation:dialog-fade-out .3s;animation:dialog-fade-out .3s}@-webkit-keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@keyframes dialog-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}@-webkit-keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes dialog-fade-out{0%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}100%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@media only screen and (max-width:767px){.hidden-xs-only{display:none!important}}@media only screen and (min-width:768px){.hidden-sm-and-up{display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.hidden-sm-only{display:none!important}}@media only screen and (max-width:991px){.hidden-sm-and-down{display:none!important}}@media only screen and (min-width:992px){.hidden-md-and-up{display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.hidden-md-only{display:none!important}}@media only screen and (max-width:1199px){.hidden-md-and-down{display:none!important}}@media only screen and (min-width:1200px){.hidden-lg-and-up{display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.hidden-lg-only{display:none!important}}@media only screen and (max-width:1919px){.hidden-lg-and-down{display:none!important}}@media only screen and (min-width:1920px){.hidden-xl-only{display:none!important}}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
.el-footer{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-form--inline .el-form-item,.el-form--inline .el-form-item__content{display:inline-block;vertical-align:top}.el-form-item::after,.el-form-item__content::after{clear:both}.el-form--label-left .el-form-item__label{text-align:left}.el-form--label-top .el-form-item__label{float:none;display:inline-block;text-align:left;padding:0 0 10px}.el-form--inline .el-form-item{margin-right:10px}.el-form--inline .el-form-item__label{float:none;display:inline-block}.el-form-item__content .el-input-group,.el-form-item__label{vertical-align:middle}.el-form--inline.el-form--label-top .el-form-item__content{display:block}.el-form-item{margin-bottom:22px}.el-form-item::after,.el-form-item::before{display:table;content:""}.el-form-item .el-form-item{margin-bottom:0}.el-form-item--mini.el-form-item,.el-form-item--small.el-form-item{margin-bottom:18px}.el-form-item .el-input__validateIcon{display:none}.el-form-item--medium .el-form-item__content,.el-form-item--medium .el-form-item__label{line-height:36px}.el-form-item--small .el-form-item__content,.el-form-item--small .el-form-item__label{line-height:32px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--mini .el-form-item__content,.el-form-item--mini .el-form-item__label{line-height:28px}.el-form-item--mini .el-form-item__error{padding-top:1px}.el-form-item__label{text-align:right;float:left;font-size:14px;color:#606266;line-height:40px;padding:0 12px 0 0;-webkit-box-sizing:border-box;box-sizing:border-box}.el-form-item__content{line-height:40px;position:relative;font-size:14px}.el-form-item__content::after,.el-form-item__content::before{display:table;content:""}.el-form-item__error{color:#f56c6c;font-size:12px;line-height:1;padding-top:4px;position:absolute;top:100%;left:0}.el-form-item__error--inline{position:relative;top:auto;left:auto;display:inline-block;margin-left:10px}.el-form-item.is-required:not(.is-no-asterisk)>.el-form-item__label:before{content:'*';color:#f56c6c;margin-right:4px}.el-form-item.is-error .el-input__inner,.el-form-item.is-error .el-input__inner:focus,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner:focus{border-color:#f56c6c}.el-form-item.is-error .el-input-group__append .el-input__inner,.el-form-item.is-error .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-error .el-input__validateIcon{color:#f56c6c}.el-form-item.is-success .el-input__inner,.el-form-item.is-success .el-input__inner:focus,.el-form-item.is-success .el-textarea__inner,.el-form-item.is-success .el-textarea__inner:focus{border-color:#67c23a}.el-form-item.is-success .el-input-group__append .el-input__inner,.el-form-item.is-success .el-input-group__prepend .el-input__inner{border-color:transparent}.el-form-item.is-success .el-input__validateIcon{color:#67c23a}.el-form-item--feedback .el-input__validateIcon{display:inline-block}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-header{padding:0 20px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-flex-negative:0;flex-shrink:0}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@font-face{font-family:element-icons;src:url(fonts/element-icons.woff) format("woff"),url(fonts/element-icons.ttf) format("truetype");font-weight:400;font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-info:before{content:"\e61a"}.el-icon-error:before{content:"\e62c"}.el-icon-success:before{content:"\e62d"}.el-icon-warning:before{content:"\e62e"}.el-icon-question:before{content:"\e634"}.el-icon-back:before{content:"\e606"}.el-icon-arrow-left:before{content:"\e600"}.el-icon-arrow-down:before{content:"\e603"}.el-icon-arrow-right:before{content:"\e604"}.el-icon-arrow-up:before{content:"\e605"}.el-icon-caret-left:before{content:"\e60a"}.el-icon-caret-bottom:before{content:"\e60b"}.el-icon-caret-top:before{content:"\e60c"}.el-icon-caret-right:before{content:"\e60e"}.el-icon-d-arrow-left:before{content:"\e610"}.el-icon-d-arrow-right:before{content:"\e613"}.el-icon-minus:before{content:"\e621"}.el-icon-plus:before{content:"\e62b"}.el-icon-remove:before{content:"\e635"}.el-icon-circle-plus:before{content:"\e601"}.el-icon-remove-outline:before{content:"\e63c"}.el-icon-circle-plus-outline:before{content:"\e602"}.el-icon-close:before{content:"\e60f"}.el-icon-check:before{content:"\e611"}.el-icon-circle-close:before{content:"\e607"}.el-icon-circle-check:before{content:"\e639"}.el-icon-circle-close-outline:before{content:"\e609"}.el-icon-circle-check-outline:before{content:"\e63e"}.el-icon-zoom-out:before{content:"\e645"}.el-icon-zoom-in:before{content:"\e641"}.el-icon-d-caret:before{content:"\e615"}.el-icon-sort:before{content:"\e640"}.el-icon-sort-down:before{content:"\e630"}.el-icon-sort-up:before{content:"\e631"}.el-icon-tickets:before{content:"\e63f"}.el-icon-document:before{content:"\e614"}.el-icon-goods:before{content:"\e618"}.el-icon-sold-out:before{content:"\e63b"}.el-icon-news:before{content:"\e625"}.el-icon-message:before{content:"\e61b"}.el-icon-date:before{content:"\e608"}.el-icon-printer:before{content:"\e62f"}.el-icon-time:before{content:"\e642"}.el-icon-bell:before{content:"\e622"}.el-icon-mobile-phone:before{content:"\e624"}.el-icon-service:before{content:"\e63a"}.el-icon-view:before{content:"\e643"}.el-icon-menu:before{content:"\e620"}.el-icon-more:before{content:"\e646"}.el-icon-more-outline:before{content:"\e626"}.el-icon-star-on:before{content:"\e637"}.el-icon-star-off:before{content:"\e63d"}.el-icon-location:before{content:"\e61d"}.el-icon-location-outline:before{content:"\e61f"}.el-icon-phone:before{content:"\e627"}.el-icon-phone-outline:before{content:"\e628"}.el-icon-picture:before{content:"\e629"}.el-icon-picture-outline:before{content:"\e62a"}.el-icon-delete:before{content:"\e612"}.el-icon-search:before{content:"\e619"}.el-icon-edit:before{content:"\e61c"}.el-icon-edit-outline:before{content:"\e616"}.el-icon-rank:before{content:"\e632"}.el-icon-refresh:before{content:"\e633"}.el-icon-share:before{content:"\e636"}.el-icon-setting:before{content:"\e638"}.el-icon-upload:before{content:"\e60d"}.el-icon-upload2:before{content:"\e644"}.el-icon-download:before{content:"\e617"}.el-icon-loading:before{content:"\e61e"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}@keyframes rotating{0%{-webkit-transform:rotateZ(0);transform:rotateZ(0)}100%{-webkit-transform:rotateZ(360deg);transform:rotateZ(360deg)}}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:rgba(255,255,255,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-loading-spinner .el-loading-text{color:#00468F;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#00468F;stroke-linecap:round}.el-loading-spinner i{color:#00468F}.el-loading-fade-enter,.el-loading-fade-leave-active{opacity:0}@-webkit-keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes loading-rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}100%{stroke-dasharray:90,150;stroke-dashoffset:-120px}}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-main{display:block;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-preferred-size:auto;flex-basis:auto;overflow:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding:20px}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-message__closeBtn:focus,.el-message__content:focus{outline-width:0}.el-message{min-width:380px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#ebeef5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,-webkit-transform .4s;transition:opacity .3s,-webkit-transform .4s;transition:opacity .3s,transform .4s;transition:opacity .3s,transform .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67c23a}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#e6a23c}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#f56c6c}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#c0c4cc;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67c23a}.el-message .el-icon-error{color:#f56c6c}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#e6a23c}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-notification{display:-webkit-box;display:-ms-flexbox;display:flex;width:330px;padding:14px 26px 14px 13px;border-radius:8px;-webkit-box-sizing:border-box;box-sizing:border-box;border:1px solid #ebeef5;position:fixed;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s;transition:opacity .3s,transform .3s,left .3s,right .3s,top .4s,bottom .3s,-webkit-transform .3s;overflow:hidden}.el-notification.right{right:16px}.el-notification.left{left:16px}.el-notification__group{margin-left:13px}.el-notification__title{font-weight:700;font-size:16px;color:#303133;margin:0}.el-notification__content{font-size:14px;line-height:21px;margin:6px 0 0;color:#606266;text-align:justify}.el-notification__content p{margin:0}.el-notification__icon{height:24px;width:24px;font-size:24px}.el-notification__closeBtn{position:absolute;top:18px;right:15px;cursor:pointer;color:#909399;font-size:16px}.el-notification__closeBtn:hover{color:#606266}.el-notification .el-icon-success{color:#67c23a}.el-notification .el-icon-error{color:#f56c6c}.el-notification .el-icon-info{color:#909399}.el-notification .el-icon-warning{color:#e6a23c}.el-notification-fade-enter.right{right:0;-webkit-transform:translateX(100%);transform:translateX(100%)}.el-notification-fade-enter.left{left:0;-webkit-transform:translateX(-100%);transform:translateX(-100%)}.el-notification-fade-leave-active{opacity:0}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-select-group{margin:0;padding:0}.el-select-group__wrap{position:relative;list-style:none;margin:0;padding:0}.el-select-group__wrap:not(:last-of-type){padding-bottom:24px}.el-select-group__wrap:not(:last-of-type)::after{content:'';position:absolute;display:block;left:20px;right:20px;bottom:12px;height:1px;background:#e4e7ed}.el-select-group__title{padding-left:20px;font-size:12px;color:#909399;line-height:30px}.el-select-group .el-select-dropdown__item{padding-left:20px}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-select-dropdown__item{font-size:14px;padding:0 20px;position:relative;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#606266;height:34px;line-height:34px;-webkit-box-sizing:border-box;box-sizing:border-box;cursor:pointer}.el-select-dropdown__item.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-select-dropdown__item.is-disabled:hover{background-color:#fff}.el-select-dropdown__item.hover,.el-select-dropdown__item:hover{background-color:#f5f7fa}.el-select-dropdown__item.selected{color:#00468F;font-weight:700}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-popover{position:absolute;background:#fff;min-width:150px;border-radius:4px;border:1px solid #ebeef5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1)}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-progress{position:relative;line-height:1}.el-progress__text{font-size:14px;color:#606266;display:inline-block;vertical-align:middle;margin-left:10px;line-height:1}.el-progress__text i{vertical-align:middle;display:block}.el-progress--circle{display:inline-block}.el-progress--circle .el-progress__text{position:absolute;top:50%;left:0;width:100%;text-align:center;margin:0;-webkit-transform:translate(0,-50%);transform:translate(0,-50%)}.el-progress--circle .el-progress__text i{vertical-align:middle;display:inline-block}.el-progress--without-text .el-progress__text{display:none}.el-progress--without-text .el-progress-bar{padding-right:0;margin-right:0;display:block}.el-progress-bar,.el-progress-bar__inner::after,.el-progress-bar__innerText{display:inline-block;vertical-align:middle}.el-progress--text-inside .el-progress-bar{padding-right:0;margin-right:0}.el-progress.is-success .el-progress-bar__inner{background-color:#67c23a}.el-progress.is-success .el-progress__text{color:#67c23a}.el-progress.is-exception .el-progress-bar__inner{background-color:#f56c6c}.el-progress.is-exception .el-progress__text{color:#f56c6c}.el-progress-bar{padding-right:50px;width:100%;margin-right:-55px;-webkit-box-sizing:border-box;box-sizing:border-box}.el-progress-bar__outer{height:6px;border-radius:100px;background-color:#ebeef5;overflow:hidden;position:relative;vertical-align:middle}.el-progress-bar__inner{position:absolute;left:0;top:0;height:100%;background-color:#00468F;text-align:right;border-radius:100px;line-height:1;white-space:nowrap;-webkit-transition:width .6s ease;transition:width .6s ease}.el-progress-bar__inner::after{content:"";height:100%}.el-progress-bar__innerText{color:#fff;font-size:12px;margin:0 5px}@-webkit-keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}@keyframes progress{0%{background-position:0 0}100%{background-position:32px 0}}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@charset "UTF-8";.el-radio-button,.el-radio-button__inner{display:inline-block;position:relative;outline:0}.el-radio-button__inner{line-height:1;white-space:nowrap;vertical-align:middle;background:#fff;border:1px solid #dcdfe6;font-weight:500;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-radio-button__inner.is-round{padding:12px 20px}.el-radio-button__inner:hover{color:#00468F}.el-radio-button__inner [class*=el-icon-]{line-height:.9}.el-radio-button__inner [class*=el-icon-]+span{margin-left:5px}.el-radio-button:first-child .el-radio-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-radio-button__orig-radio{opacity:0;outline:0;position:absolute;z-index:-1}.el-radio-button__orig-radio:checked+.el-radio-button__inner{color:#fff;background-color:#00468F;border-color:#00468F;-webkit-box-shadow:-1px 0 0 0 #00468F;box-shadow:-1px 0 0 0 #00468F}.el-radio-button__orig-radio:disabled+.el-radio-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;-webkit-box-shadow:none;box-shadow:none}.el-radio-button__orig-radio:disabled:checked+.el-radio-button__inner{background-color:#f2f6fc}.el-radio-button:last-child .el-radio-button__inner{border-radius:0 4px 4px 0}.el-radio-button:first-child:last-child .el-radio-button__inner{border-radius:4px}.el-radio-button--medium .el-radio-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-radio-button--medium .el-radio-button__inner.is-round{padding:10px 20px}.el-radio-button--small .el-radio-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-radio-button--small .el-radio-button__inner.is-round{padding:9px 15px}.el-radio-button--mini .el-radio-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-radio-button--mini .el-radio-button__inner.is-round{padding:7px 15px}.el-radio-button:focus:not(.is-focus):not(:active):not(.is-disabled){-webkit-box-shadow:0 0 2px 2px #00468F;box-shadow:0 0 2px 2px #00468F}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-radio-group{display:inline-block;line-height:1;vertical-align:middle;font-size:0}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@charset "UTF-8";.el-radio,.el-radio--medium.is-bordered .el-radio__label{font-size:14px}.el-radio,.el-radio__input{white-space:nowrap;line-height:1;outline:0}.el-radio,.el-radio__inner,.el-radio__input{position:relative;display:inline-block}.el-radio{color:#606266;font-weight:500;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.el-radio.is-bordered{padding:12px 20px 0 10px;border-radius:4px;border:1px solid #dcdfe6;-webkit-box-sizing:border-box;box-sizing:border-box;height:40px}.el-radio.is-bordered.is-checked{border-color:#00468F}.el-radio.is-bordered.is-disabled{cursor:not-allowed;border-color:#ebeef5}.el-radio__input.is-disabled .el-radio__inner,.el-radio__input.is-disabled.is-checked .el-radio__inner{background-color:#f5f7fa;border-color:#e4e7ed}.el-radio.is-bordered+.el-radio.is-bordered{margin-left:10px}.el-radio--medium.is-bordered{padding:10px 20px 0 10px;border-radius:4px;height:36px}.el-radio--mini.is-bordered .el-radio__label,.el-radio--small.is-bordered .el-radio__label{font-size:12px}.el-radio--medium.is-bordered .el-radio__inner{height:14px;width:14px}.el-radio--small.is-bordered{padding:8px 15px 0 10px;border-radius:3px;height:32px}.el-radio--small.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio--mini.is-bordered{padding:6px 15px 0 10px;border-radius:3px;height:28px}.el-radio--mini.is-bordered .el-radio__inner{height:12px;width:12px}.el-radio+.el-radio{margin-left:30px}.el-radio__input{cursor:pointer;vertical-align:middle}.el-radio__input.is-disabled .el-radio__inner{cursor:not-allowed}.el-radio__input.is-disabled .el-radio__inner::after{cursor:not-allowed;background-color:#f5f7fa}.el-radio__input.is-disabled .el-radio__inner+.el-radio__label{cursor:not-allowed}.el-radio__input.is-disabled.is-checked .el-radio__inner::after{background-color:#c0c4cc}.el-radio__input.is-disabled+span.el-radio__label{color:#c0c4cc;cursor:not-allowed}.el-radio__input.is-checked .el-radio__inner{border-color:#00468F;background:#00468F}.el-radio__input.is-checked .el-radio__inner::after{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.el-radio__input.is-checked+.el-radio__label{color:#00468F}.el-radio__input.is-focus .el-radio__inner{border-color:#00468F}.el-radio__inner{border:1px solid #dcdfe6;border-radius:100%;width:14px;height:14px;background-color:#fff;cursor:pointer;-webkit-box-sizing:border-box;box-sizing:border-box}.el-radio__inner:hover{border-color:#00468F}.el-radio__inner::after{width:4px;height:4px;border-radius:100%;background-color:#fff;content:"";position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in}.el-radio__original{opacity:0;outline:0;position:absolute;z-index:-1;top:0;left:0;right:0;bottom:0;margin:0}.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled) .el-radio__inner{-webkit-box-shadow:0 0 2px 2px #00468F;box-shadow:0 0 2px 2px #00468F}.el-radio__label{font-size:14px;padding-left:10px}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-rate__icon,.el-rate__item{position:relative;display:inline-block}.el-rate{height:20px;line-height:1}.el-rate:active,.el-rate:focus{outline-width:0}.el-rate__item{font-size:0;vertical-align:middle}.el-rate__icon{font-size:18px;margin-right:6px;color:#c0c4cc;-webkit-transition:.3s;transition:.3s}.el-rate__decimal,.el-rate__icon .path2{position:absolute;top:0;left:0}.el-rate__icon.hover{-webkit-transform:scale(1.15);transform:scale(1.15)}.el-rate__decimal{display:inline-block;overflow:hidden}.el-rate__text{font-size:14px;vertical-align:middle}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@charset "UTF-8";body{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;font-weight:400;font-size:14px;color:#000;-webkit-font-smoothing:antialiased}a{color:#00468F;text-decoration:none}a:focus,a:hover{color:rgb(51, 107, 165)}a:active{color:rgb(0, 63, 129)}h1,h2,h3,h4,h5,h6{color:#606266;font-weight:inherit}h1:first-child,h2:first-child,h3:first-child,h4:first-child,h5:first-child,h6:first-child,p:first-child{margin-top:0}h1:last-child,h2:last-child,h3:last-child,h4:last-child,h5:last-child,h6:last-child,p:last-child{margin-bottom:0}h1{font-size:20px}h2{font-size:18px}h3{font-size:16px}h4,h5,h6,p{font-size:inherit}p{line-height:1.8}sub,sup{font-size:13px}small{font-size:12px}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-row{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box}.el-row::after,.el-row::before{display:table;content:""}.el-row::after{clear:both}.el-row--flex{display:-webkit-box;display:-ms-flexbox;display:flex}.el-row--flex:after,.el-row--flex:before{display:none}.el-row--flex.is-justify-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-row--flex.is-justify-end{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.el-row--flex.is-justify-space-between{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-row--flex.is-justify-space-around{-ms-flex-pack:distribute;justify-content:space-around}.el-row--flex.is-align-middle{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-row--flex.is-align-bottom{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-scrollbar{overflow:hidden;position:relative}.el-scrollbar:active>.el-scrollbar__bar,.el-scrollbar:focus>.el-scrollbar__bar,.el-scrollbar:hover>.el-scrollbar__bar{opacity:1;-webkit-transition:opacity 340ms ease-out;transition:opacity 340ms ease-out}.el-scrollbar__wrap{overflow:scroll;height:100%}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{width:0;height:0}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:rgba(144,147,153,.3);-webkit-transition:.3s background-color;transition:.3s background-color}.el-scrollbar__thumb:hover{background-color:rgba(144,147,153,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px;opacity:0;-webkit-transition:opacity 120ms ease-out;transition:opacity 120ms ease-out}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.el-popper .popper__arrow,.el-popper .popper__arrow::after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03));filter:drop-shadow(0 2px 12px rgba(0, 0, 0, .03))}.el-popper .popper__arrow::after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow::after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow::after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow::after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow::after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-select-dropdown{position:absolute;z-index:1001;border:1px solid #e4e7ed;border-radius:4px;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:5px 0}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected{color:#00468F;background-color:#fff}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover{background-color:#f5f7fa}.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after{position:absolute;right:20px;font-family:element-icons;content:"\E611";font-size:12px;font-weight:700;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty{padding:10px 0;margin:0;text-align:center;color:#999;font-size:14px}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{list-style:none;padding:6px 0;margin:0;-webkit-box-sizing:border-box;box-sizing:border-box}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.el-time-spinner{width:100%;white-space:nowrap}.el-spinner{display:inline-block;vertical-align:middle}.el-spinner-inner{-webkit-animation:rotate 2s linear infinite;animation:rotate 2s linear infinite;width:50px;height:50px}.el-spinner-inner .path{stroke:#ececec;stroke-linecap:round;-webkit-animation:dash 1.5s ease-in-out infinite;animation:dash 1.5s ease-in-out infinite}@-webkit-keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}@keyframes dash{0%{stroke-dasharray:1,150;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-35}100%{stroke-dasharray:90,150;stroke-dashoffset:-124}}
|
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user