Compare commits
341 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f203384b00 | ||
|
|
9ac3be455c | ||
|
|
20b74a9dcd | ||
|
|
3b848a5a86 | ||
|
|
a9b5e865a5 | ||
|
|
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 | ||
|
|
7982698880 | ||
|
|
afbdff8a88 | ||
|
|
cec07208ac | ||
|
|
c1b82d0fd2 | ||
|
|
7d3a689577 |
@@ -5,8 +5,7 @@ const stream = require('stream');
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
|
||||
const got = require('got');
|
||||
const decompress = require('decompress');
|
||||
const decompressTargz = require('decompress-targz');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
const publicDir = `${distDir}/tmp/public`;
|
||||
@@ -15,6 +14,8 @@ const outDir = `${distDir}/linux`;
|
||||
const tempDownloadDir = `${distDir}/tmp/download`;
|
||||
|
||||
async function main() {
|
||||
const decomp = new FileDecompressor();
|
||||
|
||||
await fs.emptyDir(outDir);
|
||||
// перемещаем public на место
|
||||
if (await fs.pathExists(publicDir))
|
||||
@@ -32,11 +33,7 @@ async function main() {
|
||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
|
||||
plugins: [
|
||||
decompressTargz()
|
||||
]
|
||||
});
|
||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
||||
console.log('files decompressed');
|
||||
}
|
||||
// копируем в дистрибутив
|
||||
@@ -53,11 +50,7 @@ async function main() {
|
||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
await decompress(`${tempDownloadDir}/ipfs.tar.gz`, `${tempDownloadDir}`, {
|
||||
plugins: [
|
||||
decompressTargz()
|
||||
]
|
||||
});
|
||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/ipfs.tar.gz`, tempDownloadDir));
|
||||
console.log('files decompressed');
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const AppCachePlugin = require('appcache-webpack-plugin');
|
||||
|
||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
@@ -32,7 +33,15 @@ module.exports = merge(baseWpConfig, {
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin(),
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
output: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
new OptimizeCSSAssetsPlugin()
|
||||
]
|
||||
},
|
||||
@@ -45,6 +54,7 @@ module.exports = merge(baseWpConfig, {
|
||||
template: `${clientDir}/index.html.template`,
|
||||
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 got = require('got');
|
||||
const decompress = require('decompress');
|
||||
const decompressTargz = require('decompress-targz');
|
||||
const FileDecompressor = require('../server/core/FileDecompressor');
|
||||
|
||||
const distDir = path.resolve(__dirname, '../dist');
|
||||
const publicDir = `${distDir}/tmp/public`;
|
||||
@@ -15,6 +14,8 @@ const outDir = `${distDir}/win`;
|
||||
const tempDownloadDir = `${distDir}/tmp/download`;
|
||||
|
||||
async function main() {
|
||||
const decomp = new FileDecompressor();
|
||||
|
||||
await fs.emptyDir(outDir);
|
||||
// перемещаем public на место
|
||||
if (await fs.pathExists(publicDir))
|
||||
@@ -32,11 +33,7 @@ async function main() {
|
||||
console.log(`done downloading ${sqliteRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
await decompress(`${tempDownloadDir}/sqlite.tar.gz`, `${tempDownloadDir}`, {
|
||||
plugins: [
|
||||
decompressTargz()
|
||||
]
|
||||
});
|
||||
console.log(await decomp.unpackTarZZ(`${tempDownloadDir}/sqlite.tar.gz`, tempDownloadDir));
|
||||
console.log('files decompressed');
|
||||
}
|
||||
// копируем в дистрибутив
|
||||
@@ -53,7 +50,7 @@ async function main() {
|
||||
console.log(`done downloading ${ipfsRemoteUrl}`);
|
||||
|
||||
//распаковываем
|
||||
await decompress(`${tempDownloadDir}/ipfs.zip`, `${tempDownloadDir}`);
|
||||
console.log(await decomp.unpack(`${tempDownloadDir}/ipfs.zip`, tempDownloadDir));
|
||||
console.log('files decompressed');
|
||||
}
|
||||
// копируем в дистрибутив
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import axios from 'axios';
|
||||
import {sleep} from '../share/utils';
|
||||
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api/reader'
|
||||
baseURL: '/api/reader'
|
||||
});
|
||||
|
||||
const workerApi = axios.create({
|
||||
baseURL: '/api/worker'
|
||||
baseURL: '/api/worker'
|
||||
});
|
||||
|
||||
class Reader {
|
||||
async loadBook(url, callback) {
|
||||
async loadBook(opts, callback) {
|
||||
const refreshPause = 300;
|
||||
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;
|
||||
if (!workerId)
|
||||
@@ -41,7 +42,7 @@ class Reader {
|
||||
throw new Error(errMes);
|
||||
}
|
||||
if (i > 0)
|
||||
await sleep(refreshPause);
|
||||
await utils.sleep(refreshPause);
|
||||
|
||||
i++;
|
||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||
@@ -49,8 +50,9 @@ class Reader {
|
||||
}
|
||||
//проверка воркера
|
||||
const prevProgress = response.data.progress;
|
||||
const prevState = response.data.state;
|
||||
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'];
|
||||
}
|
||||
|
||||
callback({state: 'loading', progress: 0});
|
||||
const options = {
|
||||
onDownloadProgress: progress => {
|
||||
while (progress.loaded > estSize) estSize *= 1.5;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -47,14 +47,12 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
rootRoute: function() {
|
||||
this.setAppTitle();
|
||||
this.redirectIfNeeded();
|
||||
},
|
||||
mode: function() {
|
||||
this.setAppTitle();
|
||||
this.redirectIfNeeded();
|
||||
}
|
||||
},
|
||||
@@ -113,13 +111,19 @@ class App extends Vue {
|
||||
this.dispatch('config/loadConfig');
|
||||
this.$watch('apiError', function(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({
|
||||
title: 'Ошибка API',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: newError.response.config.url + '<br>' + newError.response.statusText
|
||||
message: mes
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setAppTitle();
|
||||
this.redirectIfNeeded();
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
@@ -198,15 +202,18 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
redirectIfNeeded() {
|
||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
|
||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
|
||||
//старый url
|
||||
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) {
|
||||
window.location = `/#/reader?url=${url}`;
|
||||
} else {
|
||||
this.$router.replace('/reader');
|
||||
q.url = decodeURIComponent(url);
|
||||
}
|
||||
|
||||
window.history.replaceState({}, '', '/');
|
||||
this.$router.replace({ path: '/reader', query: q });
|
||||
}
|
||||
|
||||
//yandex-метрика для omnireader
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Скопировать текст
|
||||
</template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Скопировать текст
|
||||
</template>
|
||||
|
||||
<div ref="text" class="text" tabindex="-1">
|
||||
<div v-html="text"></div>
|
||||
</div>
|
||||
</Window>
|
||||
<div ref="text" class="text" tabindex="-1">
|
||||
<div v-html="text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -109,23 +105,6 @@ class CopyTextPage extends Vue {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<h4>Возможности читалки:</h4>
|
||||
<ul>
|
||||
<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>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
|
||||
<li>управление кликом и с клавиатуры</li>
|
||||
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
|
||||
<li>регистрация не требуется</li>
|
||||
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
|
||||
</ul>
|
||||
|
||||
<p>В качестве URL можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
<p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
|
||||
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать 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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,21 +44,25 @@
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import {copyTextToClipboard} from '../../../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class CommonHelpPage extends Vue {
|
||||
created() {
|
||||
this.config = this.$store.state.config;
|
||||
}
|
||||
|
||||
get automationHtml() {
|
||||
if (this.config.mode == 'omnireader') {
|
||||
return `<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
|
||||
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
get mode() {
|
||||
return this.$store.state.config.mode;
|
||||
}
|
||||
|
||||
async copyText(text, mes) {
|
||||
const result = await copyTextToClipboard(text);
|
||||
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 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,11 +53,10 @@ class DonateHelpPage extends Vue {
|
||||
|
||||
async copyAddress(address, prefix) {
|
||||
const result = await copyTextToClipboard(address);
|
||||
const msg = (result ? `${prefix}-адрес ${address} успешно скопирован в буфер обмена` : 'Копирование не удалось');
|
||||
if (result)
|
||||
this.$notify.success({message: msg});
|
||||
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
|
||||
else
|
||||
this.$notify.error({message: msg});
|
||||
this.$notify.error({message: 'Копирование не удалось'});
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Справка
|
||||
</template>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Справка
|
||||
</template>
|
||||
|
||||
<el-tabs type="border-card" v-model="selectedTab">
|
||||
<el-tab-pane class="tab" label="Общее">
|
||||
<CommonHelpPage></CommonHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Клавиатура">
|
||||
<HotkeysHelpPage></HotkeysHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Мышь/тачпад">
|
||||
<MouseHelpPage></MouseHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Помочь проекту" name="donate">
|
||||
<DonateHelpPage></DonateHelpPage>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</Window>
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs type="border-card" v-model="selectedTab">
|
||||
<el-tab-pane class="tab" label="Общее">
|
||||
<CommonHelpPage></CommonHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Клавиатура">
|
||||
<HotkeysHelpPage></HotkeysHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Мышь/тачпад">
|
||||
<MouseHelpPage></MouseHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="История версий" name="releases">
|
||||
<VersionHistoryPage></VersionHistoryPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Помочь проекту" name="donate">
|
||||
<DonateHelpPage></DonateHelpPage>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -36,6 +34,7 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
@@ -44,6 +43,7 @@ export default @Component({
|
||||
HotkeysHelpPage,
|
||||
MouseHelpPage,
|
||||
DonateHelpPage,
|
||||
VersionHistoryPage,
|
||||
},
|
||||
})
|
||||
class HelpPage extends Vue {
|
||||
@@ -57,6 +57,10 @@ class HelpPage extends Vue {
|
||||
this.selectedTab = 'donate';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
this.selectedTab = 'releases';
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
this.close();
|
||||
@@ -68,23 +72,6 @@ class HelpPage extends Vue {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
||||
<li><b>X</b> - открыть недавние</li>
|
||||
<li><b>O</b> - автономный режим</li>
|
||||
<li><b>S</b> - открыть окно настроек</li>
|
||||
</ul>
|
||||
</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 class="part">
|
||||
<span class="greeting bold-font">{{ title }}</span>
|
||||
<div class="space"></div>
|
||||
<span class="greeting">Добро пожаловать!</span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также: <b>rtf, doc, docx</b>, и вскоре: pdf, epub, mobi</span>
|
||||
<span class="greeting">...и распознается сжатие: <b>zip, bz2, gz</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, pdf, epub, mobi</b></span>
|
||||
</div>
|
||||
|
||||
<div class="part center">
|
||||
@@ -18,14 +18,31 @@
|
||||
Загрузить файл с диска
|
||||
</el-button>
|
||||
<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 class="part bottom">
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</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>
|
||||
|
||||
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,12 +50,18 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
|
||||
import {versionHistory} from '../versionHistory';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
PasteTextPage,
|
||||
},
|
||||
})
|
||||
class LoaderPage extends Vue {
|
||||
bookUrl = null;
|
||||
loadPercent = 0;
|
||||
pasteTextActive = false;
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
@@ -46,6 +69,8 @@ class LoaderPage extends Vue {
|
||||
|
||||
mounted() {
|
||||
this.progress = this.$refs.progress;
|
||||
if (this.mode == 'omnireader')
|
||||
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
|
||||
}
|
||||
|
||||
activated() {
|
||||
@@ -53,7 +78,7 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.$store.state.config.mode == 'omnireader')
|
||||
if (this.mode == 'omnireader')
|
||||
return 'Omni Reader - браузерная онлайн-читалка.';
|
||||
return 'Универсальная читалка книг и ресурсов интернета.';
|
||||
|
||||
@@ -64,13 +89,19 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
get version() {
|
||||
return `v${this.$store.state.config.version}`;
|
||||
return this.$store.state.config.version;
|
||||
}
|
||||
|
||||
get isExternalConverter() {
|
||||
return this.$store.state.config.useExternalBookConverter;
|
||||
}
|
||||
|
||||
get clientVersion() {
|
||||
let v = versionHistory[0].header;
|
||||
v = v.split(' ')[0];
|
||||
return v;
|
||||
}
|
||||
|
||||
submitUrl() {
|
||||
if (this.bookUrl) {
|
||||
this.$emit('load-book', {url: this.bookUrl});
|
||||
@@ -83,12 +114,27 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
loadFile() {
|
||||
const file = this.$refs.file.files[0];
|
||||
const file = this.$refs.file.files[0];
|
||||
this.$refs.file.value = '';
|
||||
if (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() {
|
||||
this.$emit('help-toggle');
|
||||
}
|
||||
@@ -101,7 +147,15 @@ class LoaderPage extends Vue {
|
||||
window.open('http://samlib.ru/comment/b/bookpauk/bookpauk_reader', '_blank');
|
||||
}
|
||||
|
||||
openOldVersion() {
|
||||
window.open('http://old.omnireader.ru', '_blank');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.pasteTextActive) {
|
||||
return this.$refs.pasteTextPage.keyHook(event);
|
||||
}
|
||||
|
||||
//недостатки сторонних ui
|
||||
const input = this.$refs.input.$refs.input;
|
||||
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
||||
@@ -130,6 +184,7 @@ class LoaderPage extends Vue {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.part {
|
||||
@@ -141,8 +196,8 @@ class LoaderPage extends Vue {
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 130%;
|
||||
line-height: 170%;
|
||||
font-size: 120%;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
.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>
|
||||
.el-progress__text {
|
||||
color: lightgreen;
|
||||
color: lightgreen !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
{{ header }}
|
||||
</template>
|
||||
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
{{ header }}
|
||||
</template>
|
||||
|
||||
<div class="content">
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
<div class="content">
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
|
||||
<div v-show="!initStep" class="input">
|
||||
<input ref="input" class="el-input__inner"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
</div>
|
||||
<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="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</Window>
|
||||
<div v-show="!initStep" class="input">
|
||||
<input ref="input" class="el-input__inner"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
</div>
|
||||
<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="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -61,6 +57,8 @@ class SearchPage extends Vue {
|
||||
}
|
||||
|
||||
async init(parsed) {
|
||||
this.$refs.window.init();
|
||||
|
||||
if (this.parsed != parsed) {
|
||||
this.initStep = true;
|
||||
this.stopInit = false;
|
||||
@@ -178,32 +176,13 @@ class SearchPage extends Vue {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
min-width: 430px;
|
||||
}
|
||||
|
||||
.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>
|
||||
<div ref="main" class="main" @click="close">
|
||||
<div class="mainWindow" @click.stop>
|
||||
<Window @close="close">
|
||||
<template slot="header">
|
||||
Установить позицию
|
||||
</template>
|
||||
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||
<template slot="header">
|
||||
Установить позицию
|
||||
</template>
|
||||
|
||||
<div class="slider">
|
||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||
</div>
|
||||
</Window>
|
||||
<div class="slider">
|
||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||
</div>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Window from '../../share/Window.vue';
|
||||
|
||||
@@ -28,7 +23,8 @@ export default @Component({
|
||||
},
|
||||
watch: {
|
||||
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() {
|
||||
this.commit = this.$store.commit;
|
||||
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) {
|
||||
@@ -63,26 +68,6 @@ class SetPositionPage extends Vue {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
margin: 20px;
|
||||
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;` +
|
||||
` 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 len = lines.length;
|
||||
@@ -317,4 +317,56 @@ export default class DrawHelper {
|
||||
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>
|
||||
<!-- img -->
|
||||
</div>
|
||||
<div ref="scrollBox1" class="layout" style="overflow: hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<div ref="scrollingPage1" class="layout" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||
<div v-html="page1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="scrollBox2" class="layout" style="overflow: hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<div ref="scrollingPage2" class="layout" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
||||
<div ref="scrollBox2" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||
<div ref="scrollingPage2" class="layout over-hidden" @transitionend="onPage2TransitionEnd" @animationend="onPage2AnimationEnd">
|
||||
<div v-html="page2"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -23,7 +23,6 @@
|
||||
oncontextmenu="return false;">
|
||||
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
<div v-show="fontsLoading" ref="fontsLoading"></div>
|
||||
</div>
|
||||
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
@@ -77,7 +76,6 @@ class TextPage extends Vue {
|
||||
page2 = null;
|
||||
statusBar = null;
|
||||
statusBarClickable = null;
|
||||
fontsLoading = null;
|
||||
|
||||
lastBook = null;
|
||||
bookPos = 0;
|
||||
@@ -133,7 +131,6 @@ class TextPage extends Vue {
|
||||
}, 10);
|
||||
|
||||
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
||||
this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@@ -157,9 +154,10 @@ class TextPage extends Vue {
|
||||
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
|
||||
|
||||
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.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.scrollingPage2.style.width = this.w + 'px';
|
||||
@@ -170,6 +168,12 @@ class TextPage extends Vue {
|
||||
this.fontShift = this.fontVertShift/100;
|
||||
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
|
||||
this.drawHelper.realWidth = this.realWidth;
|
||||
this.drawHelper.realHeight = this.realHeight;
|
||||
@@ -195,14 +199,8 @@ class TextPage extends Vue {
|
||||
this.drawHelper.lineHeight = this.lineHeight;
|
||||
this.drawHelper.context = this.context;
|
||||
|
||||
//сообщение "Загрузка шрифтов..."
|
||||
const flText = 'Загрузка шрифта...';
|
||||
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';
|
||||
//statusBar
|
||||
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
|
||||
|
||||
//parsed
|
||||
if (this.parsed) {
|
||||
@@ -222,30 +220,35 @@ class TextPage extends Vue {
|
||||
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||
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
|
||||
const pageDelta = this.h - (this.pageLineCount*this.lineHeight - this.lineInterval);
|
||||
let y = this.indentTB + pageDelta/2;
|
||||
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
|
||||
let y = pageSpace/2;
|
||||
if (this.showStatusBar)
|
||||
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||
const page1 = this.$refs.scrollBox1;
|
||||
const page2 = this.$refs.scrollBox2;
|
||||
page1.style.width = this.w + 'px';
|
||||
page2.style.width = this.w + 'px';
|
||||
page1.style.height = (this.h - pageDelta) + 'px';
|
||||
page2.style.height = (this.h - pageDelta) + 'px';
|
||||
let page1 = this.$refs.scrollBox1;
|
||||
let page2 = this.$refs.scrollBox2;
|
||||
|
||||
page1.style.perspective = '3072px';
|
||||
page2.style.perspective = '3072px';
|
||||
|
||||
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';
|
||||
page2.style.top = y + 'px';
|
||||
page1.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() {
|
||||
@@ -260,6 +263,18 @@ class TextPage extends Vue {
|
||||
async loadFonts() {
|
||||
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)
|
||||
this.fontsLoaded = {};
|
||||
//загрузка дин.шрифта
|
||||
@@ -290,6 +305,8 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
this.fontsLoading = false;
|
||||
if (inst)
|
||||
inst.close();
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
@@ -369,13 +386,17 @@ class TextPage extends Vue {
|
||||
this.meta = bookManager.metaOnly(this.book);
|
||||
this.fb2 = this.meta.fb2;
|
||||
|
||||
const authorName = _.compact([
|
||||
this.fb2.lastName,
|
||||
this.fb2.firstName,
|
||||
this.fb2.middleName
|
||||
]).join(' ');
|
||||
let authorNames = [];
|
||||
if (this.fb2.author) {
|
||||
authorNames = this.fb2.author.map(a => _.compact([
|
||||
a.lastName,
|
||||
a.firstName,
|
||||
a.middleName
|
||||
]).join(' '));
|
||||
}
|
||||
|
||||
this.title = _.compact([
|
||||
authorName,
|
||||
authorNames.join(', '),
|
||||
this.fb2.bookTitle
|
||||
]).join(' - ');
|
||||
|
||||
@@ -613,7 +634,7 @@ class TextPage extends Vue {
|
||||
const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
|
||||
const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', '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));
|
||||
let page1 = this.$refs.scrollingPage1;
|
||||
@@ -633,8 +654,22 @@ class TextPage extends Vue {
|
||||
duration, this.pageChangeDirectionDown, transition1Finish);
|
||||
break;
|
||||
case 'downShift':
|
||||
page1.style.height = this.scrollHeight + 'px';
|
||||
page2.style.height = this.scrollHeight + 'px';
|
||||
|
||||
await this.drawHelper.doPageAnimationDownShift(page1, page2,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -971,7 +1006,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
if (!this.mobile)
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
if (event.touches.length == 1) {
|
||||
@@ -987,19 +1022,19 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.mobile)
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onTouchCancel() {
|
||||
if (!this.mobile)
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onMouseDown(event) {
|
||||
if (this.mobile)
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
if (event.button == 0) {
|
||||
@@ -1015,13 +1050,13 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.mobile)
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
this.endClickRepeat();
|
||||
}
|
||||
|
||||
onMouseWheel(event) {
|
||||
if (this.mobile)
|
||||
if (this.$isMobileDevice)
|
||||
return;
|
||||
if (event.deltaY > 0) {
|
||||
this.doDown();
|
||||
@@ -1098,6 +1133,14 @@ class TextPage extends Vue {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.over-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.on-top {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back {
|
||||
z-index: 5;
|
||||
}
|
||||
@@ -1163,4 +1206,5 @@ class TextPage extends Vue {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default class BookParser {
|
||||
if (tag == 'binary') {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
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 : '');
|
||||
}
|
||||
|
||||
@@ -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 (tag == 'body') {
|
||||
if (!isFirstBody)
|
||||
@@ -319,15 +325,19 @@ export default class BookParser {
|
||||
|
||||
text = text.replace(/[\t\n\r\xa0]/g, ' ');
|
||||
|
||||
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
||||
switch (path) {
|
||||
case '/fictionbook/description/title-info/author/first-name':
|
||||
fb2.firstName = text;
|
||||
if (authorLength)
|
||||
fb2.author[authorLength - 1].firstName = text;
|
||||
break;
|
||||
case '/fictionbook/description/title-info/author/middle-name':
|
||||
fb2.middleName = text;
|
||||
if (authorLength)
|
||||
fb2.author[authorLength - 1].middleName = text;
|
||||
break;
|
||||
case '/fictionbook/description/title-info/author/last-name':
|
||||
fb2.lastName = text;
|
||||
if (authorLength)
|
||||
fb2.author[authorLength - 1].lastName = text;
|
||||
break;
|
||||
case '/fictionbook/description/title-info/genre':
|
||||
fb2.genre = text;
|
||||
@@ -610,7 +620,8 @@ export default class BookParser {
|
||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
||||
para.parsed.showImages === this.showImages &&
|
||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
||||
para.parsed.imageFitWidth === this.imageFitWidth
|
||||
para.parsed.imageFitWidth === this.imageFitWidth &&
|
||||
para.parsed.compactTextPerc === this.compactTextPerc
|
||||
)
|
||||
return para.parsed;
|
||||
|
||||
@@ -625,6 +636,7 @@ export default class BookParser {
|
||||
showImages: this.showImages,
|
||||
imageHeightLines: this.imageHeightLines,
|
||||
imageFitWidth: this.imageFitWidth,
|
||||
compactTextPerc: this.compactTextPerc,
|
||||
visible: !(
|
||||
(this.cutEmptyParagraphs && para.cut) ||
|
||||
(para.addIndex > this.addEmptyParagraphs)
|
||||
@@ -655,6 +667,7 @@ export default class BookParser {
|
||||
let style = {};
|
||||
let ofs = 0;//смещение от начала параграфа para.offset
|
||||
let imgW = 0;
|
||||
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||
for (const part of parts) {
|
||||
style = part.style;
|
||||
@@ -739,7 +752,7 @@ export default class BookParser {
|
||||
p = (style.space ? p + parsed.p*style.space : p);
|
||||
let w = this.measureText(str, style) + p;
|
||||
let wordTail = word;
|
||||
if (w > parsed.w && prevStr != '') {
|
||||
if (w > parsed.w + compactWidth && prevStr != '') {
|
||||
if (parsed.wordWrap) {//по слогам
|
||||
let slogi = this.splitToSlogi(word);
|
||||
|
||||
@@ -752,7 +765,7 @@ export default class BookParser {
|
||||
for (let k = 0; k < slogiLen - 1; k++) {
|
||||
let slog = slogi[0];
|
||||
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
||||
if (ww <= parsed.w) {
|
||||
if (ww <= parsed.w + compactWidth) {
|
||||
s += slog;
|
||||
ss += slog;
|
||||
} else
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import localForage from 'localforage';
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as utils from '../../../share/utils';
|
||||
import BookParser from './BookParser';
|
||||
|
||||
const maxDataSize = 500*1024*1024;//chars, not bytes
|
||||
|
||||
const bmCacheStore = localForage.createInstance({
|
||||
name: 'bmCacheStore'
|
||||
});
|
||||
const maxDataSize = 300*1024*1024;//compressed bytes
|
||||
|
||||
const bmMetaStore = localForage.createInstance({
|
||||
name: 'bmMetaStore'
|
||||
@@ -23,58 +20,84 @@ const bmRecentStore = localForage.createInstance({
|
||||
|
||||
class BookManager {
|
||||
async init(settings) {
|
||||
this.loaded = false;
|
||||
this.settings = settings;
|
||||
|
||||
//this.booksCached нужен только для ускорения загрузки читалки
|
||||
this.booksCached = await bmCacheStore.getItem('books');
|
||||
if (!this.booksCached)
|
||||
this.booksCached = {};
|
||||
this.recent = await bmCacheStore.getItem('recent');
|
||||
this.books = Object.assign({}, this.booksCached);
|
||||
this.eventListeners = [];
|
||||
this.books = {};
|
||||
this.recent = {};
|
||||
|
||||
this.recentChanged1 = true;
|
||||
this.recentChanged2 = true;
|
||||
|
||||
if (!this.books || !this.recent) {
|
||||
this.books = {};
|
||||
this.recent = {};
|
||||
await this.loadMeta(true);
|
||||
} else {
|
||||
this.loadMeta(false);
|
||||
this.recentLast = await bmRecentStore.getItem('recent-last');
|
||||
if (this.recentLast) {
|
||||
this.recent[this.recentLast.key] = this.recentLast;
|
||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||
if (_.isObject(meta)) {
|
||||
this.books[meta.key] = meta;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
for (let i = 0; i < len; i++) {
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
const key = await bmMetaStore.key(i);
|
||||
const keySplit = key.split('-');
|
||||
|
||||
if (keySplit.length == 2 && keySplit[0] == 'bmMeta') {
|
||||
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();
|
||||
for (let i = 0; i < len; i++) {
|
||||
const key = await bmRecentStore.key(i);
|
||||
let r = await bmRecentStore.getItem(key);
|
||||
this.recent[r.key] = r;
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
key = await bmRecentStore.key(i);
|
||||
if (key) {
|
||||
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.cleanRecentBooks();
|
||||
|
||||
this.booksCached = {};
|
||||
for (const key in this.books) {
|
||||
this.booksCached[key] = this.metaOnly(this.books[key]);
|
||||
}
|
||||
await bmCacheStore.setItem('books', this.booksCached);
|
||||
this.recentChanged = true;
|
||||
this.loaded = true;
|
||||
this.emit('load-stored-finish');
|
||||
}
|
||||
|
||||
async cleanBooks() {
|
||||
@@ -84,7 +107,8 @@ class BookManager {
|
||||
let toDel = null;
|
||||
for (let key in this.books) {
|
||||
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) {
|
||||
toDel = book;
|
||||
@@ -100,51 +124,173 @@ class BookManager {
|
||||
}
|
||||
}
|
||||
|
||||
async addBook(newBook, callback) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
async deflateWithProgress(data, callback) {
|
||||
const chunkSize = 128*1024;
|
||||
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};
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
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.booksCached[meta.key] = this.metaOnly(result);
|
||||
|
||||
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
||||
await bmDataStore.setItem(`bmData-${meta.key}`, result.data);
|
||||
await bmCacheStore.setItem('books', this.booksCached);
|
||||
await bmDataStore.setItem(`bmData-${meta.key}`, data);
|
||||
|
||||
callback(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
hasBookParsed(meta) {
|
||||
async hasBookParsed(meta) {
|
||||
if (!this.books)
|
||||
return false;
|
||||
if (!meta.url)
|
||||
return false;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async getBook(meta, callback) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
let result = undefined;
|
||||
if (!meta.key)
|
||||
meta.key = this.keyFromUrl(meta.url);
|
||||
|
||||
result = this.books[meta.key];
|
||||
|
||||
if (result && !result.data) {
|
||||
result.data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||
this.books[meta.key] = result;
|
||||
if (!result) {
|
||||
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||
if (result)
|
||||
this.books[meta.key] = result;
|
||||
}
|
||||
|
||||
//Если файл на сервере изменился, считаем, что в кеше его нету
|
||||
if (meta.path && result && meta.path != result.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -152,29 +298,19 @@ class BookManager {
|
||||
}
|
||||
|
||||
async delBook(meta) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
|
||||
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
||||
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
||||
|
||||
delete this.books[meta.key];
|
||||
delete this.booksCached[meta.key];
|
||||
|
||||
await bmCacheStore.setItem('books', this.booksCached);
|
||||
}
|
||||
|
||||
async parseBook(meta, data, callback) {
|
||||
if (!this.books)
|
||||
await this.init();
|
||||
|
||||
const parsed = new BookParser(this.settings);
|
||||
|
||||
const parsedMeta = await parsed.parse(data, callback);
|
||||
const result = Object.assign({}, meta, parsedMeta, {
|
||||
length: data.length,
|
||||
textLength: parsed.textLength,
|
||||
data,
|
||||
parsed
|
||||
});
|
||||
|
||||
@@ -183,7 +319,7 @@ class BookManager {
|
||||
|
||||
metaOnly(book) {
|
||||
let result = Object.assign({}, book);
|
||||
delete result.data;
|
||||
delete result.data;//можно будет убрать эту строку со временем
|
||||
delete result.parsed;
|
||||
return result;
|
||||
}
|
||||
@@ -192,88 +328,98 @@ class BookManager {
|
||||
return utils.stringToHex(url);
|
||||
}
|
||||
|
||||
async setRecentBook(value, noTouch) {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
//-- recent --------------------------------------------------------------
|
||||
async setRecentBook(value) {
|
||||
const result = this.metaOnly(value);
|
||||
if (!noTouch)
|
||||
Object.assign(result, {touchTime: Date.now()});
|
||||
result.touchTime = Date.now();
|
||||
result.deleted = 0;
|
||||
|
||||
if (result.textLength && !result.bookPos && result.bookPosPercent)
|
||||
result.bookPos = Math.round(result.bookPosPercent*result.textLength);
|
||||
if (this.recent[result.key] && this.recent[result.key].deleted) {
|
||||
//восстановим из небытия пользовательские данные
|
||||
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;
|
||||
|
||||
await bmRecentStore.setItem(result.key, result);
|
||||
await this.cleanRecentBooks();
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
|
||||
this.recentChanged1 = true;
|
||||
this.recentChanged2 = true;
|
||||
this.recentLast = result;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
|
||||
this.recentChanged = true;
|
||||
this.emit('recent-changed', result.key);
|
||||
return result;
|
||||
}
|
||||
|
||||
async getRecentBook(value) {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
return this.recent[value.key];
|
||||
let result = this.recent[value.key];
|
||||
if (!result) {
|
||||
result = await bmRecentStore.getItem(value.key);
|
||||
if (result)
|
||||
this.recent[value.key] = result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async delRecentBook(value) {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
this.recent[value.key].deleted = 1;
|
||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
||||
|
||||
await bmRecentStore.removeItem(value.key);
|
||||
delete this.recent[value.key];
|
||||
await bmCacheStore.setItem('recent', this.recent);
|
||||
|
||||
this.recentChanged1 = true;
|
||||
this.recentChanged2 = true;
|
||||
if (this.recentLast.key == value.key) {
|
||||
this.recentLast = null;
|
||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||
}
|
||||
this.emit('recent-deleted', value.key);
|
||||
this.emit('recent-changed', value.key);
|
||||
}
|
||||
|
||||
async cleanRecentBooks() {
|
||||
if (!this.recent)
|
||||
await this.init();
|
||||
const sorted = this.getSortedRecent();
|
||||
|
||||
if (Object.keys(this.recent).length > 1000) {
|
||||
let min = Date.now();
|
||||
let found = null;
|
||||
for (let key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (book.touchTime < min) {
|
||||
min = book.touchTime;
|
||||
found = book;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
await this.delRecentBook(found);
|
||||
await this.cleanRecentBooks();
|
||||
}
|
||||
let isDel = false;
|
||||
for (let i = 1000; i < sorted.length; i++) {
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
delete this.recent[sorted[i].key];
|
||||
await bmRecentStore.removeItem(sorted[i].key);
|
||||
isDel = true;
|
||||
}
|
||||
|
||||
this.sortedRecentCached = null;
|
||||
|
||||
if (isDel)
|
||||
this.emit('recent-changed');
|
||||
return isDel;
|
||||
}
|
||||
|
||||
mostRecentBook() {
|
||||
if (!this.recentChanged1 && this.mostRecentCached) {
|
||||
return this.mostRecentCached;
|
||||
if (this.recentLast) {
|
||||
return this.recentLast;
|
||||
}
|
||||
const oldRecentLast = this.recentLast;
|
||||
|
||||
let max = 0;
|
||||
let result = null;
|
||||
for (let key in this.recent) {
|
||||
const book = this.recent[key];
|
||||
if (book.touchTime > max) {
|
||||
if (!book.deleted && book.touchTime > max) {
|
||||
max = book.touchTime;
|
||||
result = book;
|
||||
}
|
||||
}
|
||||
this.mostRecentCached = result;
|
||||
this.recentChanged1 = false;
|
||||
this.recentLast = result;
|
||||
bmRecentStore.setItem('recent-last', this.recentLast);//no await
|
||||
|
||||
if (this.recentLast !== oldRecentLast)
|
||||
this.emit('recent-changed');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getSortedRecent() {
|
||||
if (!this.recentChanged2 && this.sortedRecentCached) {
|
||||
if (!this.recentChanged && this.sortedRecentCached) {
|
||||
return this.sortedRecentCached;
|
||||
}
|
||||
|
||||
@@ -282,10 +428,60 @@ class BookManager {
|
||||
result.sort((a, b) => b.touchTime - a.touchTime);
|
||||
|
||||
this.sortedRecentCached = result;
|
||||
this.recentChanged2 = false;
|
||||
this.recentChanged = false;
|
||||
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>
|
||||
<div class="window">
|
||||
<div class="header">
|
||||
<span class="header-text"><slot name="header"></slot></span>
|
||||
<span class="close-button" @click="close"><i class="el-icon-close"></i></span>
|
||||
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="windowBox" @click.stop>
|
||||
<div class="window">
|
||||
<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>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,17 +19,116 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-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 {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
init() {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.windowBox {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.window {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -39,9 +143,9 @@ class Window extends Vue {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #e5e7ea;
|
||||
background-color: #59B04F;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
@@ -54,8 +158,12 @@ class Window extends Vue {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #69C05F;
|
||||
}
|
||||
</style>
|
||||
@@ -9,98 +9,44 @@ Vue.use(ElementUI, { locale });
|
||||
*/
|
||||
|
||||
//------------------------------------------------------
|
||||
//import './theme/index.css';
|
||||
|
||||
import './theme/icon.css';
|
||||
import './theme/tooltip.css';
|
||||
import './theme/index.css';
|
||||
|
||||
import ElMenu from 'element-ui/lib/menu';
|
||||
import './theme/menu.css';
|
||||
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
import './theme/menu-item.css';
|
||||
|
||||
import ElButton from 'element-ui/lib/button';
|
||||
import './theme/button.css';
|
||||
|
||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
||||
import './theme/button-group.css';
|
||||
|
||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
||||
import './theme/checkbox.css';
|
||||
|
||||
import ElTabs from 'element-ui/lib/tabs';
|
||||
import './theme/tabs.css';
|
||||
|
||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
||||
import './theme/tab-pane.css';
|
||||
|
||||
import ElTooltip from 'element-ui/lib/tooltip';
|
||||
import './theme/tooltip.css';
|
||||
|
||||
import ElCol from 'element-ui/lib/col';
|
||||
import './theme/col.css';
|
||||
|
||||
import ElContainer from 'element-ui/lib/container';
|
||||
import './theme/container.css';
|
||||
|
||||
import ElAside from 'element-ui/lib/aside';
|
||||
import './theme/aside.css';
|
||||
|
||||
import ElHeader from 'element-ui/lib/header';
|
||||
import './theme/header.css';
|
||||
|
||||
import ElMain from 'element-ui/lib/main';
|
||||
import './theme/main.css';
|
||||
|
||||
import ElInput from 'element-ui/lib/input';
|
||||
import './theme/input.css';
|
||||
|
||||
import ElInputNumber from 'element-ui/lib/input-number';
|
||||
import './theme/input-number.css';
|
||||
|
||||
import ElSelect from 'element-ui/lib/select';
|
||||
import './theme/select.css';
|
||||
|
||||
import ElOption from 'element-ui/lib/option';
|
||||
import './theme/option.css';
|
||||
|
||||
import ElTable from 'element-ui/lib/table';
|
||||
import './theme/table.css';
|
||||
|
||||
import ElTableColumn from 'element-ui/lib/table-column';
|
||||
import './theme/table-column.css';
|
||||
|
||||
import ElProgress from 'element-ui/lib/progress';
|
||||
import './theme/progress.css';
|
||||
|
||||
import ElSlider from 'element-ui/lib/slider';
|
||||
import './theme/slider.css';
|
||||
|
||||
import ElForm from 'element-ui/lib/form';
|
||||
import './theme/form.css';
|
||||
|
||||
import ElFormItem from 'element-ui/lib/form-item';
|
||||
import './theme/form-item.css';
|
||||
|
||||
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 './theme/notification.css';
|
||||
|
||||
import Loading from 'element-ui/lib/loading';
|
||||
import './theme/loading.css';
|
||||
|
||||
import MessageBox from 'element-ui/lib/message-box';
|
||||
import './theme/message-box.css';
|
||||
|
||||
const components = {
|
||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
||||
ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
||||
ElColorPicker,
|
||||
ElColorPicker, ElDialog,
|
||||
};
|
||||
|
||||
for (let name in components) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html manifest="/app/manifest.appcache">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
@@ -9,5 +9,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://yastatic.net/share2/share.js" async="async"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Vue from 'vue';
|
||||
import App from './components/App.vue';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './element';
|
||||
|
||||
import App from './components/App.vue';
|
||||
//Vue.config.productionTip = false;
|
||||
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
||||
@@ -2,21 +2,25 @@ import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
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 Card = () => import('./components/CardIndex/Card/Card.vue');
|
||||
const Book = () => import('./components/CardIndex/Book/Book.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 Income = () => import('./components/Income/Income.vue');
|
||||
const Sources = () => import('./components/Sources/Sources.vue');
|
||||
const Settings = () => import('./components/Settings/Settings.vue');
|
||||
const Help = () => import('./components/Help/Help.vue');
|
||||
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||
|
||||
const myRoutes = [
|
||||
['/', 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) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function stringToHex(str) {
|
||||
let result = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
result += str.charCodeAt(i).toString(16);
|
||||
}
|
||||
return result;
|
||||
return Buffer.from(str).toString('hex');
|
||||
}
|
||||
|
||||
export function hexToString(str) {
|
||||
let result = '';
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
result += String.fromCharCode(parseInt(str.substr(i, 2), 16));
|
||||
}
|
||||
return result;
|
||||
return Buffer.from(str, 'hex').toString();
|
||||
}
|
||||
|
||||
export function randomArray(len) {
|
||||
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) {
|
||||
@@ -26,6 +39,10 @@ export function formatDate(d, format) {
|
||||
case 'normal':
|
||||
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')}`;
|
||||
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;
|
||||
}
|
||||
|
||||
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 = [
|
||||
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
|
||||
{name: 'GEO_1', label: 'BPG Arial', fontVertShift: 10},
|
||||
@@ -123,61 +137,78 @@ const webFonts = [
|
||||
];
|
||||
|
||||
const settingDefaults = {
|
||||
textColor: '#000000',
|
||||
backgroundColor: '#EBE2C9',
|
||||
wallpaper: '',
|
||||
fontStyle: '',// 'italic'
|
||||
fontWeight: '',// 'bold'
|
||||
fontSize: 20,// px
|
||||
fontName: 'ReaderDefault',
|
||||
webFontName: '',
|
||||
fontVertShift: 0,
|
||||
textVertShift: -20,
|
||||
textColor: '#000000',
|
||||
backgroundColor: '#EBE2C9',
|
||||
wallpaper: '',
|
||||
fontStyle: '',// 'italic'
|
||||
fontWeight: '',// 'bold'
|
||||
fontSize: 20,// px
|
||||
fontName: 'ReaderDefault',
|
||||
webFontName: '',
|
||||
fontVertShift: 0,
|
||||
textVertShift: 0,
|
||||
|
||||
lineInterval: 3,// px, межстрочный интервал
|
||||
textAlignJustify: true,// выравнивание по ширине
|
||||
p: 25,// px, отступ параграфа
|
||||
indentLR: 15,// px, отступ всего текста слева и справа
|
||||
indentTB: 0,// px, отступ всего текста сверху и снизу
|
||||
wordWrap: true,//перенос по слогам
|
||||
keepLastToFirst: true,// перенос последней строки в первую при листании
|
||||
lineInterval: 3,// px, межстрочный интервал
|
||||
textAlignJustify: true,// выравнивание по ширине
|
||||
p: 25,// px, отступ параграфа
|
||||
indentLR: 15,// px, отступ всего текста слева и справа
|
||||
indentTB: 0,// px, отступ всего текста сверху и снизу
|
||||
wordWrap: true,//перенос по слогам
|
||||
keepLastToFirst: false,// перенос последней строки в первую при листании
|
||||
|
||||
showStatusBar: true,
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAlpha: 0.4,
|
||||
showStatusBar: true,
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAlpha: 0.4,
|
||||
|
||||
scrollingDelay: 3000,// замедление, ms
|
||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||
scrollingDelay: 3000,// замедление, ms
|
||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||
|
||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
|
||||
allowUrlParamBookPos: false,
|
||||
lazyParseEnabled: false,
|
||||
copyFullText: false,
|
||||
showClickMapPage: true,
|
||||
clickControl: true,
|
||||
cutEmptyParagraphs: false,
|
||||
addEmptyParagraphs: 0,
|
||||
blinkCachedLoad: true,
|
||||
showImages: true,
|
||||
showInlineImagesInCenter: true,
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
allowUrlParamBookPos: false,
|
||||
lazyParseEnabled: false,
|
||||
copyFullText: false,
|
||||
showClickMapPage: true,
|
||||
clickControl: true,
|
||||
cutEmptyParagraphs: false,
|
||||
addEmptyParagraphs: 0,
|
||||
blinkCachedLoad: true,
|
||||
showImages: true,
|
||||
showInlineImagesInCenter: true,
|
||||
compactTextPerc: 0,
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
showMigrationDialog: true,
|
||||
enableSitesFilter: true,
|
||||
|
||||
fontShifts: {},
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||
for (const font of webFonts)
|
||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||
for (const button of toolButtons)
|
||||
settingDefaults.showToolButton[button.name] = button.show;
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
toolBarActive: true,
|
||||
serverSyncEnabled: false,
|
||||
serverStorageKey: '',
|
||||
profiles: {},
|
||||
profilesRev: 0,
|
||||
allowProfilesSave: false,//подстраховка для разработки
|
||||
whatsNewContentHash: '',
|
||||
migrationRemindDate: '',
|
||||
currentProfile: '',
|
||||
settings: Object.assign({}, settingDefaults),
|
||||
settingsRev: {},
|
||||
};
|
||||
|
||||
// getters
|
||||
@@ -191,12 +222,40 @@ const mutations = {
|
||||
setToolBarActive(state, 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) {
|
||||
state.settings = Object.assign({}, state.settings, value);
|
||||
}
|
||||
},
|
||||
setSettingsRev(state, value) {
|
||||
state.settingsRev = Object.assign({}, state.settingsRev, value);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
toolButtons,
|
||||
fonts,
|
||||
webFonts,
|
||||
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