Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -9,6 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
const AppCachePlugin = require('appcache-webpack-plugin');
|
||||||
|
|
||||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||||
const clientDir = path.resolve(__dirname, '../client');
|
const clientDir = path.resolve(__dirname, '../client');
|
||||||
@@ -53,6 +54,7 @@ module.exports = merge(baseWpConfig, {
|
|||||||
template: `${clientDir}/index.html.template`,
|
template: `${clientDir}/index.html.template`,
|
||||||
filename: `${publicDir}/index.html`
|
filename: `${publicDir}/index.html`
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}])
|
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||||
|
new AppCachePlugin({})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,14 +47,12 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
watch: {
|
watch: {
|
||||||
rootRoute: function() {
|
|
||||||
this.setAppTitle();
|
|
||||||
this.redirectIfNeeded();
|
|
||||||
},
|
|
||||||
mode: function() {
|
mode: function() {
|
||||||
|
this.setAppTitle();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -123,6 +121,9 @@ class App extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.setAppTitle();
|
||||||
|
this.redirectIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
@@ -201,15 +202,18 @@ class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redirectIfNeeded() {
|
redirectIfNeeded() {
|
||||||
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) {
|
if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
|
||||||
//старый url
|
//старый url
|
||||||
const search = window.location.search.substr(1);
|
const search = window.location.search.substr(1);
|
||||||
const url = search.split('url=')[1] || '';
|
const s = search.split('url=');
|
||||||
|
const url = s[1] || '';
|
||||||
|
const q = utils.parseQuery(s[0] || '');
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location = `/#/reader?url=${url}`;
|
q.url = decodeURIComponent(url);
|
||||||
} else {
|
|
||||||
this.$router.replace('/reader');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
this.$router.replace({ path: '/reader', query: q });
|
||||||
}
|
}
|
||||||
|
|
||||||
//yandex-метрика для omnireader
|
//yandex-метрика для omnireader
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
<Window @close="close">
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
Скопировать текст
|
Скопировать текст
|
||||||
@@ -10,8 +8,6 @@
|
|||||||
<div v-html="text"></div>
|
<div v-html="text"></div>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -109,23 +105,6 @@ class CopyTextPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
|||||||
@@ -23,12 +23,19 @@
|
|||||||
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</p>
|
||||||
|
|
||||||
<p>Для автономной загрузки читалки (без интернета):<br>
|
<p>Для автономной загрузки читалки (без интернета):<br>
|
||||||
В Google Chrome можно установить флаг <span class="clickable" @click="copyText('chrome://flags/#show-saved-copy')">chrome://flags/#show-saved-copy</span>
|
В Google Chrome можно установить флаг <span class="clickable" @click="copyText('chrome://flags/#show-saved-copy', 'Ссылка на флаг успешно скопирована в буфер обмена. Можно открыть ее в новой вкладке.')">chrome://flags/#show-saved-copy</span>
|
||||||
в значение "Primary". В этом случае на стандартной странице "нет соединения" появится кнопка для автономной загрузки сайта из кэша.<br>
|
в значение "Primary". В этом случае на стандартной странице "нет соединения" появится кнопка для автономной загрузки сайта из кэша.<br>
|
||||||
В Mozilla Firefox в автономном режиме сайт загружается из кэша автоматически. Если этого не происходит, можно установить опцию
|
В Mozilla Firefox в автономном режиме сайт загружается из кэша автоматически. Если этого не происходит, можно установить опцию
|
||||||
"Веб-разработка" -> "Работать автономно".</p>
|
"Веб-разработка" -> "Работать автономно".</p>
|
||||||
|
|
||||||
<div v-html="automationHtml"></div>
|
<div v-show="mode == 'omnireader'">
|
||||||
|
<p>Вы также можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||||
|
<br><span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||||
|
<strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||||
|
</span>
|
||||||
|
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,22 +51,15 @@ export default @Component({
|
|||||||
})
|
})
|
||||||
class CommonHelpPage extends Vue {
|
class CommonHelpPage extends Vue {
|
||||||
created() {
|
created() {
|
||||||
this.config = this.$store.state.config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get automationHtml() {
|
get mode() {
|
||||||
if (this.config.mode == 'omnireader') {
|
return this.$store.state.config.mode;
|
||||||
return `<p>Вы также можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
|
||||||
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
|
|
||||||
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`;
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyText(text) {
|
async copyText(text, mes) {
|
||||||
const result = await copyTextToClipboard(text);
|
const result = await copyTextToClipboard(text);
|
||||||
const msg = (result ? `Ссылка на флаг успешно скопирована в буфер обмена. Можно открыть ее в новой вкладке.` : 'Копирование не удалось');
|
const msg = (result ? mes : 'Копирование не удалось');
|
||||||
if (result)
|
if (result)
|
||||||
this.$notify.success({message: msg});
|
this.$notify.success({message: msg});
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
<Window @close="close">
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
Справка
|
Справка
|
||||||
@@ -22,11 +20,8 @@
|
|||||||
<el-tab-pane label="Помочь проекту" name="donate">
|
<el-tab-pane label="Помочь проекту" name="donate">
|
||||||
<DonateHelpPage></DonateHelpPage>
|
<DonateHelpPage></DonateHelpPage>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -77,23 +72,6 @@ class HelpPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs {
|
.el-tabs {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,290 +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.$nextTick(() => {
|
|
||||||
this.$refs.input.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rowKey(row) {
|
|
||||||
return row.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTableData() {
|
|
||||||
let result = [];
|
|
||||||
|
|
||||||
const sorted = bookManager.getSortedRecent();
|
|
||||||
for (let i = 0; i < sorted.length; i++) {
|
|
||||||
const book = sorted[i];
|
|
||||||
if (book.deleted)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
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 = '';
|
|
||||||
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())
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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('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>
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
||||||
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
|
||||||
data-title="Omni Reader - браузерная онлайн-читалка"
|
data-title="Omni Reader - браузерная онлайн-читалка"
|
||||||
data-url="http://omnireader.ru">
|
data-url="https://omnireader.ru">
|
||||||
</div>
|
</div>
|
||||||
<div class="space"></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="openComments">Отзывы о читалке</span>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
<Window @close="close">
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
|
<span style="position: relative; top: -3px">
|
||||||
Вставьте текст и нажмите
|
Вставьте текст и нажмите
|
||||||
<el-button size="mini" style="font-size: 120%; color: blue" @click="loadBuffer">Загрузить</el-button>
|
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||||
|
|
||||||
или F2
|
или F2
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -15,8 +14,6 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -99,23 +96,6 @@ class PasteTextPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
@@ -123,6 +103,7 @@ class PasteTextPage extends Vue {
|
|||||||
padding: 0 10px 0 10px;
|
padding: 0 10px 0 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
|
min-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text:focus {
|
.text:focus {
|
||||||
@@ -133,4 +114,10 @@ hr {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
color: blue;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -94,6 +94,6 @@ class ProgressPage extends Vue {
|
|||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
.el-progress__text {
|
.el-progress__text {
|
||||||
color: lightgreen;
|
color: lightgreen !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header v-show="toolBarActive" height='50px'>
|
<el-header v-show="toolBarActive" height='50px'>
|
||||||
<div class="header">
|
<div ref="header" class="header">
|
||||||
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
|
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
|
||||||
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
|
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<el-tooltip v-show="showToolButton['history']" content="Открыть недавние" :open-delay="1000" effect="light">
|
<el-tooltip v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
|
||||||
<el-button ref="history" class="tool-button" :class="buttonActiveClass('history')" @click="buttonClick('history')"><i class="el-icon-document"></i></el-button>
|
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
@stop-text-search="stopTextSearch">
|
@stop-text-search="stopTextSearch">
|
||||||
</SearchPage>
|
</SearchPage>
|
||||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
||||||
<HistoryPage v-show="historyActive" ref="historyPage" @load-book="loadBook" @history-toggle="historyToggle"></HistoryPage>
|
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
|
||||||
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
||||||
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
||||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||||
@@ -87,6 +87,87 @@
|
|||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="Внимание!"
|
||||||
|
:visible.sync="migrationVisible1"
|
||||||
|
width="90%">
|
||||||
|
<div>
|
||||||
|
Появилась httpS-версия сайта по адресу <a href="https://omnireader.ru" target="_blank">https://omnireader.ru</a><br>
|
||||||
|
Работа по httpS-протоколу, помимо безопасности соединения, позволяет воспользоваться всеми возможностями
|
||||||
|
современных браузеров, а именно, применительно к нашему ресурсу:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>возможность автономной работы с читалкой (без доступа к интернету), кеширование сайта через appcache</li>
|
||||||
|
<li>безопасная передача на сервер данных о настройках и читаемых книгах при включенной синхронизации; все данные шифруются на стороне
|
||||||
|
браузера ключом доступа и никто (в т.ч. администратор) не имеет возможности их прочитать
|
||||||
|
<li>использование встроенных в JS функций шифрования и других</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Для того, чтобы перейти на новую версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
|
||||||
|
<ul>
|
||||||
|
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
|
||||||
|
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
после этого все данные будут автоматически сохранены на сервер
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
произойдет переход на https-версию читалки и откроется окно для ввода ключа
|
||||||
|
</span><br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
Старая http-версия сайта будет доступна до конца 2019 года.<br>
|
||||||
|
Приносим извинения за доставленные неудобства.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
|
||||||
|
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="Внимание!"
|
||||||
|
:visible.sync="migrationVisible2"
|
||||||
|
width="90%">
|
||||||
|
<div>
|
||||||
|
Информация для пользователей старой версии читалки по адресу <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a><br>
|
||||||
|
Для того, чтобы перейти на новую httpS-версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
|
||||||
|
<ul>
|
||||||
|
<li>перейти на старую версию ресурса <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a></li>
|
||||||
|
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
|
||||||
|
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
после этого все данные будут автоматически сохранены на сервер
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
произойдет переход на https-версию читалки и откроется окно для ввода ключа
|
||||||
|
</span><br>
|
||||||
|
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
|
||||||
|
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
Старая http-версия сайта будет доступна до конца 2019 года.<br>
|
||||||
|
Приносим извинения за доставленные неудобства.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
|
||||||
|
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
</el-main>
|
</el-main>
|
||||||
|
|
||||||
</el-container>
|
</el-container>
|
||||||
@@ -106,7 +187,7 @@ import ProgressPage from './ProgressPage/ProgressPage.vue';
|
|||||||
import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
|
import SetPositionPage from './SetPositionPage/SetPositionPage.vue';
|
||||||
import SearchPage from './SearchPage/SearchPage.vue';
|
import SearchPage from './SearchPage/SearchPage.vue';
|
||||||
import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
|
import CopyTextPage from './CopyTextPage/CopyTextPage.vue';
|
||||||
import HistoryPage from './HistoryPage/HistoryPage.vue';
|
import RecentBooksPage from './RecentBooksPage/RecentBooksPage.vue';
|
||||||
import SettingsPage from './SettingsPage/SettingsPage.vue';
|
import SettingsPage from './SettingsPage/SettingsPage.vue';
|
||||||
import HelpPage from './HelpPage/HelpPage.vue';
|
import HelpPage from './HelpPage/HelpPage.vue';
|
||||||
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
||||||
@@ -126,7 +207,7 @@ export default @Component({
|
|||||||
SetPositionPage,
|
SetPositionPage,
|
||||||
SearchPage,
|
SearchPage,
|
||||||
CopyTextPage,
|
CopyTextPage,
|
||||||
HistoryPage,
|
RecentBooksPage,
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
HelpPage,
|
HelpPage,
|
||||||
ClickMapPage,
|
ClickMapPage,
|
||||||
@@ -157,10 +238,12 @@ export default @Component({
|
|||||||
this.updateRoute();
|
this.updateRoute();
|
||||||
},
|
},
|
||||||
loaderActive: function(newValue) {
|
loaderActive: function(newValue) {
|
||||||
|
(async() => {
|
||||||
const recent = this.mostRecentBook();
|
const recent = this.mostRecentBook();
|
||||||
if (!newValue && !this.loading && recent && !bookManager.hasBookParsed(recent)) {
|
if (!newValue && !this.loading && recent && !await bookManager.hasBookParsed(recent)) {
|
||||||
this.loadBook(recent);
|
this.loadBook(recent);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -173,7 +256,7 @@ class Reader extends Vue {
|
|||||||
setPositionActive = false;
|
setPositionActive = false;
|
||||||
searchActive = false;
|
searchActive = false;
|
||||||
copyTextActive = false;
|
copyTextActive = false;
|
||||||
historyActive = false;
|
recentBooksActive = false;
|
||||||
settingsActive = false;
|
settingsActive = false;
|
||||||
helpActive = false;
|
helpActive = false;
|
||||||
clickMapActive = false;
|
clickMapActive = false;
|
||||||
@@ -190,6 +273,8 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
whatsNewVisible = false;
|
whatsNewVisible = false;
|
||||||
whatsNewContent = '';
|
whatsNewContent = '';
|
||||||
|
migrationVisible1 = false;
|
||||||
|
migrationVisible2 = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -216,26 +301,25 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
this.debouncedSaveRecent = _.debounce(async() => {
|
|
||||||
const serverStorage = this.$refs.serverStorage;
|
|
||||||
while (!serverStorage.inited) await utils.sleep(1000);
|
|
||||||
await serverStorage.saveRecent();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
this.debouncedSaveRecentLast = _.debounce(async() => {
|
|
||||||
const serverStorage = this.$refs.serverStorage;
|
|
||||||
while (!serverStorage.inited) await utils.sleep(1000);
|
|
||||||
await serverStorage.saveRecentLast();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
|
|
||||||
|
//TODO: убрать в будущем
|
||||||
|
if (this.showToolButton['history']) {
|
||||||
|
const newShowToolButton = Object.assign({}, this.showToolButton);
|
||||||
|
newShowToolButton['recentBooks'] = true;
|
||||||
|
delete newShowToolButton['history'];
|
||||||
|
const newSettings = Object.assign({}, this.settings, { showToolButton: newShowToolButton });
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.updateHeaderMinWidth();
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
bookManager.addEventListener(this.bookManagerEvent);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
@@ -252,7 +336,9 @@ class Reader extends Vue {
|
|||||||
this.checkActivateDonateHelpPage();
|
this.checkActivateDonateHelpPage();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
|
await this.$refs.serverStorage.init();
|
||||||
await this.showWhatsNew();
|
await this.showWhatsNew();
|
||||||
|
await this.showMigration();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +350,16 @@ class Reader extends Vue {
|
|||||||
this.clickControl = settings.clickControl;
|
this.clickControl = settings.clickControl;
|
||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
|
this.showMigrationDialog = settings.showMigrationDialog;
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
|
|
||||||
|
this.updateHeaderMinWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHeaderMinWidth() {
|
||||||
|
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
||||||
|
if (this.$refs.header)
|
||||||
|
this.$refs.header.style.minWidth = 65*showButtonCount + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSetStorageAccessKey() {
|
checkSetStorageAccessKey() {
|
||||||
@@ -320,6 +415,33 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showMigration() {
|
||||||
|
await utils.sleep(3000);
|
||||||
|
if (!this.settingsActive &&
|
||||||
|
this.mode == 'omnireader' && this.showMigrationDialog && this.migrationRemindDate != utils.formatDate(new Date(), 'coDate')) {
|
||||||
|
if (window.location.protocol == 'http:') {
|
||||||
|
this.migrationVisible1 = true;
|
||||||
|
} else if (window.location.protocol == 'https:') {
|
||||||
|
this.migrationVisible2 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationDialogDisable() {
|
||||||
|
this.migrationVisible1 = false;
|
||||||
|
this.migrationVisible2 = false;
|
||||||
|
if (this.showMigrationDialog) {
|
||||||
|
const newSettings = Object.assign({}, this.settings, { showMigrationDialog: false });
|
||||||
|
this.commit('reader/setSettings', newSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationDialogRemind() {
|
||||||
|
this.migrationVisible1 = false;
|
||||||
|
this.migrationVisible2 = false;
|
||||||
|
this.commit('reader/setMigrationRemindDate', utils.formatDate(new Date(), 'coDate'));
|
||||||
|
}
|
||||||
|
|
||||||
openVersionHistory() {
|
openVersionHistory() {
|
||||||
this.whatsNewVisible = false;
|
this.whatsNewVisible = false;
|
||||||
this.versionHistoryToggle();
|
this.versionHistoryToggle();
|
||||||
@@ -350,12 +472,16 @@ class Reader extends Vue {
|
|||||||
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
const pos = (recent && recent.bookPos && this.allowUrlParamBookPos ? `__p=${recent.bookPos}&` : '');
|
||||||
const url = (recent ? `url=${recent.url}` : '');
|
const url = (recent ? `url=${recent.url}` : '');
|
||||||
if (isNewRoute)
|
if (isNewRoute)
|
||||||
this.$router.push(`/reader?${pos}${url}`);
|
this.$router.push(`/reader?${pos}${url}`).catch(() => {});
|
||||||
else
|
else
|
||||||
this.$router.replace(`/reader?${pos}${url}`);
|
this.$router.replace(`/reader?${pos}${url}`).catch(() => {});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mode() {
|
||||||
|
return this.$store.state.config.mode;
|
||||||
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
let result = '';
|
let result = '';
|
||||||
const path = this.$route.fullPath;
|
const path = this.$route.fullPath;
|
||||||
@@ -380,22 +506,15 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async bookManagerEvent(eventName) {
|
async bookManagerEvent(eventName) {
|
||||||
const serverStorage = this.$refs.serverStorage;
|
if (eventName == 'recent-changed') {
|
||||||
if (eventName == 'load-meta-finish') {
|
if (this.recentBooksActive) {
|
||||||
serverStorage.init();
|
await this.$refs.recentBooksPage.updateTableData();
|
||||||
const result = await bookManager.cleanRecentBooks();
|
}
|
||||||
if (result)
|
|
||||||
this.debouncedSaveRecent();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventName == 'recent-changed' || eventName == 'save-recent') {
|
|
||||||
if (this.historyActive) {
|
|
||||||
this.$refs.historyPage.updateTableData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventName == 'set-recent' || eventName == 'recent-deleted') {
|
||||||
const oldBook = this.mostRecentBookReactive;
|
const oldBook = this.mostRecentBookReactive;
|
||||||
const newBook = bookManager.mostRecentBook();
|
const newBook = bookManager.mostRecentBook();
|
||||||
|
|
||||||
if (oldBook && newBook) {
|
if (oldBook && newBook) {
|
||||||
if (oldBook.key != newBook.key) {
|
if (oldBook.key != newBook.key) {
|
||||||
this.loadingBook = true;
|
this.loadingBook = true;
|
||||||
@@ -409,12 +528,6 @@ class Reader extends Vue {
|
|||||||
this.bookPosChanged({bookPos: newBook.bookPos});
|
this.bookPosChanged({bookPos: newBook.bookPos});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventName == 'recent-changed') {
|
|
||||||
this.debouncedSaveRecentLast();
|
|
||||||
} else {
|
|
||||||
this.debouncedSaveRecent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +549,10 @@ class Reader extends Vue {
|
|||||||
return this.$store.state.reader.whatsNewContentHash;
|
return this.$store.state.reader.whatsNewContentHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get migrationRemindDate() {
|
||||||
|
return this.$store.state.reader.migrationRemindDate;
|
||||||
|
}
|
||||||
|
|
||||||
addAction(pos) {
|
addAction(pos) {
|
||||||
let a = this.actionList;
|
let a = this.actionList;
|
||||||
if (!a.length || a[a.length - 1] != pos) {
|
if (!a.length || a[a.length - 1] != pos) {
|
||||||
@@ -476,7 +593,7 @@ class Reader extends Vue {
|
|||||||
closeAllTextPages() {
|
closeAllTextPages() {
|
||||||
this.setPositionActive = false;
|
this.setPositionActive = false;
|
||||||
this.copyTextActive = false;
|
this.copyTextActive = false;
|
||||||
this.historyActive = false;
|
this.recentBooksActive = false;
|
||||||
this.settingsActive = false;
|
this.settingsActive = false;
|
||||||
this.stopScrolling();
|
this.stopScrolling();
|
||||||
this.stopSearch();
|
this.stopSearch();
|
||||||
@@ -568,14 +685,14 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
historyToggle() {
|
recentBooksToggle() {
|
||||||
this.historyActive = !this.historyActive;
|
this.recentBooksActive = !this.recentBooksActive;
|
||||||
if (this.historyActive) {
|
if (this.recentBooksActive) {
|
||||||
this.closeAllTextPages();
|
this.closeAllTextPages();
|
||||||
this.$refs.historyPage.init();
|
this.$refs.recentBooksPage.init();
|
||||||
this.historyActive = true;
|
this.recentBooksActive = true;
|
||||||
} else {
|
} else {
|
||||||
this.historyActive = false;
|
this.recentBooksActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +701,10 @@ class Reader extends Vue {
|
|||||||
if (this.settingsActive) {
|
if (this.settingsActive) {
|
||||||
this.closeAllTextPages();
|
this.closeAllTextPages();
|
||||||
this.settingsActive = true;
|
this.settingsActive = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.settingsPage.init();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.settingsActive = false;
|
this.settingsActive = false;
|
||||||
}
|
}
|
||||||
@@ -660,8 +781,8 @@ class Reader extends Vue {
|
|||||||
case 'copyText':
|
case 'copyText':
|
||||||
this.copyTextToggle();
|
this.copyTextToggle();
|
||||||
break;
|
break;
|
||||||
case 'history':
|
case 'recentBooks':
|
||||||
this.historyToggle();
|
this.recentBooksToggle();
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
@@ -684,7 +805,7 @@ class Reader extends Vue {
|
|||||||
case 'scrolling':
|
case 'scrolling':
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
case 'history':
|
case 'recentBooks':
|
||||||
case 'settings':
|
case 'settings':
|
||||||
if (this[`${button}Active`])
|
if (this[`${button}Active`])
|
||||||
classResult = classActive;
|
classResult = classActive;
|
||||||
@@ -702,7 +823,7 @@ class Reader extends Vue {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.activePage == 'LoaderPage' || !this.mostRecentBook()) {
|
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case 'undoAction':
|
case 'undoAction':
|
||||||
case 'redoAction':
|
case 'redoAction':
|
||||||
@@ -712,9 +833,9 @@ class Reader extends Vue {
|
|||||||
case 'copyText':
|
case 'copyText':
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
case 'history':
|
case 'recentBooks':
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
if (!this.mostRecentBook())
|
if (!this.mostRecentBookReactive)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -759,7 +880,8 @@ class Reader extends Vue {
|
|||||||
//акивируем страницу с текстом
|
//акивируем страницу с текстом
|
||||||
this.$nextTick(async() => {
|
this.$nextTick(async() => {
|
||||||
const last = this.mostRecentBookReactive;
|
const last = this.mostRecentBookReactive;
|
||||||
const isParsed = bookManager.hasBookParsed(last);
|
const isParsed = await bookManager.hasBookParsed(last);
|
||||||
|
|
||||||
if (!isParsed) {
|
if (!isParsed) {
|
||||||
this.$root.$emit('set-app-title');
|
this.$root.$emit('set-app-title');
|
||||||
return;
|
return;
|
||||||
@@ -793,7 +915,7 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
// уже просматривается сейчас
|
// уже просматривается сейчас
|
||||||
const lastBook = (this.$refs.page ? this.$refs.page.lastBook : null);
|
const lastBook = (this.$refs.page ? this.$refs.page.lastBook : null);
|
||||||
if (!opts.force && lastBook && lastBook.url == url && bookManager.hasBookParsed(lastBook)) {
|
if (!opts.force && lastBook && lastBook.url == url && await bookManager.hasBookParsed(lastBook)) {
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -855,7 +977,6 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress.setState({totalSteps: 5});
|
progress.setState({totalSteps: 5});
|
||||||
|
|
||||||
// не удалось, скачиваем книгу полностью с конвертацией
|
// не удалось, скачиваем книгу полностью с конвертацией
|
||||||
let loadCached = true;
|
let loadCached = true;
|
||||||
if (!book) {
|
if (!book) {
|
||||||
@@ -954,8 +1075,8 @@ class Reader extends Vue {
|
|||||||
if (!handled && this.settingsActive)
|
if (!handled && this.settingsActive)
|
||||||
handled = this.$refs.settingsPage.keyHook(event);
|
handled = this.$refs.settingsPage.keyHook(event);
|
||||||
|
|
||||||
if (!handled && this.historyActive)
|
if (!handled && this.recentBooksActive)
|
||||||
handled = this.$refs.historyPage.keyHook(event);
|
handled = this.$refs.recentBooksPage.keyHook(event);
|
||||||
|
|
||||||
if (!handled && this.setPositionActive)
|
if (!handled && this.setPositionActive)
|
||||||
handled = this.$refs.setPositionPage.keyHook(event);
|
handled = this.$refs.setPositionPage.keyHook(event);
|
||||||
@@ -1005,7 +1126,7 @@ class Reader extends Vue {
|
|||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
break;
|
break;
|
||||||
case 'KeyX':
|
case 'KeyX':
|
||||||
this.historyToggle();
|
this.recentBooksToggle();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
@@ -1040,7 +1161,6 @@ class Reader extends Vue {
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-width: 550px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-main {
|
.el-main {
|
||||||
@@ -1064,6 +1184,10 @@ class Reader extends Vue {
|
|||||||
box-shadow: 3px 3px 5px black;
|
box-shadow: 3px 3px 5px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-button + .tool-button {
|
||||||
|
margin: 0 2px 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.tool-button:hover {
|
.tool-button:hover {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|||||||
320
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
320
client/components/Reader/RecentBooksPage/RecentBooksPage.vue
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
<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,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
{{ header }}
|
{{ header }}
|
||||||
</template>
|
</template>
|
||||||
@@ -21,8 +19,6 @@
|
|||||||
</el-button-group>
|
</el-button-group>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -61,6 +57,8 @@ class SearchPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(parsed) {
|
async init(parsed) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
if (this.parsed != parsed) {
|
if (this.parsed != parsed) {
|
||||||
this.initStep = true;
|
this.initStep = true;
|
||||||
this.stopInit = false;
|
this.stopInit = false;
|
||||||
@@ -178,32 +176,13 @@ class SearchPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
height: 125px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
min-width: 430px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import readerApi from '../../../api/reader';
|
|||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import * as cryptoUtils from '../../../share/cryptoUtils';
|
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||||
|
|
||||||
const maxSetTries = 5;
|
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
watch: {
|
watch: {
|
||||||
serverSyncEnabled: function() {
|
serverSyncEnabled: function() {
|
||||||
@@ -46,11 +44,16 @@ class ServerStorage extends Vue {
|
|||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
this.debouncedSaveRecent = _.debounce((itemKey) => {
|
||||||
|
this.saveRecent(itemKey);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
this.debouncedNotifySuccess = _.debounce(() => {
|
||||||
|
this.success('Данные синхронизированы с сервером');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
this.oldProfiles = {};
|
this.oldProfiles = {};
|
||||||
this.oldSettings = {};
|
this.oldSettings = {};
|
||||||
this.oldRecent = {};
|
|
||||||
this.oldRecentLast = {};
|
|
||||||
this.oldRecentLastDiff = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -61,13 +64,30 @@ class ServerStorage extends Vue {
|
|||||||
} else {
|
} else {
|
||||||
await this.serverStorageKeyChanged();
|
await this.serverStorageKeyChanged();
|
||||||
}
|
}
|
||||||
this.oldRecent = _.cloneDeep(bookManager.recent);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
this.oldRecentLast = _.cloneDeep(bookManager.recentLast) || {};
|
|
||||||
} finally {
|
} finally {
|
||||||
this.inited = true;
|
this.inited = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bookManagerEvent(eventName, itemKey) {
|
||||||
|
if (eventName == 'recent-changed') {
|
||||||
|
if (itemKey) {
|
||||||
|
if (!this.recentDeltaInited) {
|
||||||
|
this.warning('Функции сохранения на сервер пока недоступны');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.recentDelta)
|
||||||
|
this.recentDelta = {};
|
||||||
|
|
||||||
|
this.recentDelta[itemKey] = _.cloneDeep(bookManager.recent[itemKey]);
|
||||||
|
|
||||||
|
this.debouncedSaveRecent(itemKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async generateNewServerStorageKey() {
|
async generateNewServerStorageKey() {
|
||||||
const key = utils.toBase58(utils.randomArray(32));
|
const key = utils.toBase58(utils.randomArray(32));
|
||||||
this.commit('reader/setServerStorageKey', key);
|
this.commit('reader/setServerStorageKey', key);
|
||||||
@@ -95,7 +115,7 @@ class ServerStorage extends Vue {
|
|||||||
await this.loadProfiles(force);
|
await this.loadProfiles(force);
|
||||||
this.checkCurrentProfile();
|
this.checkCurrentProfile();
|
||||||
await this.currentProfileChanged(force);
|
await this.currentProfileChanged(force);
|
||||||
await this.loadRecent(force);
|
await this.loadRecent();
|
||||||
if (force)
|
if (force)
|
||||||
await this.saveRecent();
|
await this.saveRecent();
|
||||||
}
|
}
|
||||||
@@ -146,10 +166,6 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifySuccess() {
|
|
||||||
this.success('Данные синхронизированы с сервером');
|
|
||||||
}
|
|
||||||
|
|
||||||
success(message) {
|
success(message) {
|
||||||
if (this.showServerStorageMessages)
|
if (this.showServerStorageMessages)
|
||||||
this.$notify.success({message});
|
this.$notify.success({message});
|
||||||
@@ -165,7 +181,7 @@ class ServerStorage extends Vue {
|
|||||||
this.$notify.error({message});
|
this.$notify.error({message});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(force) {
|
async loadSettings(force = false, doNotifySuccess = true) {
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
|
if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -202,7 +218,8 @@ class ServerStorage extends Vue {
|
|||||||
this.commit('reader/setSettings', sets.data);
|
this.commit('reader/setSettings', sets.data);
|
||||||
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
|
this.commit('reader/setSettingsRev', {[setsId]: sets.rev});
|
||||||
|
|
||||||
this.notifySuccess();
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
} else {
|
} else {
|
||||||
this.warning(`Неверный ответ сервера: ${sets.state}`);
|
this.warning(`Неверный ответ сервера: ${sets.state}`);
|
||||||
}
|
}
|
||||||
@@ -220,32 +237,18 @@ class ServerStorage extends Vue {
|
|||||||
try {
|
try {
|
||||||
const setsId = `settings-${this.currentProfile}`;
|
const setsId = `settings-${this.currentProfile}`;
|
||||||
let result = {state: ''};
|
let result = {state: ''};
|
||||||
let tries = 0;
|
|
||||||
while (result.state != 'success' && tries < maxSetTries) {
|
|
||||||
const oldRev = this.settingsRev[setsId] || 0;
|
const oldRev = this.settingsRev[setsId] || 0;
|
||||||
try {
|
try {
|
||||||
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
|
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.savingSettings = false;
|
|
||||||
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.state == 'reject') {
|
if (result.state == 'reject') {
|
||||||
await this.loadSettings(true);
|
await this.loadSettings(true, false);
|
||||||
const newSettings = utils.applyObjDiff(this.settings, diff);
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
this.commit('reader/setSettings', newSettings);
|
} else if (result.state == 'success') {
|
||||||
}
|
|
||||||
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tries >= maxSetTries) {
|
|
||||||
//отменять изменения не будем, просто предупредим
|
|
||||||
//this.commit('reader/setSettings', this.oldSettings);
|
|
||||||
console.error(result);
|
|
||||||
this.error('Не удалось отправить настройки на сервер. Данные не сохранены и могут быть перезаписаны.');
|
|
||||||
} else {
|
|
||||||
this.oldSettings = _.cloneDeep(this.settings);
|
this.oldSettings = _.cloneDeep(this.settings);
|
||||||
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
|
this.commit('reader/setSettingsRev', {[setsId]: this.settingsRev[setsId] + 1});
|
||||||
}
|
}
|
||||||
@@ -254,7 +257,7 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadProfiles(force) {
|
async loadProfiles(force = false, doNotifySuccess = true) {
|
||||||
if (!this.keyInited || !this.serverSyncEnabled)
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -289,8 +292,10 @@ class ServerStorage extends Vue {
|
|||||||
this.oldProfiles = _.cloneDeep(prof.data);
|
this.oldProfiles = _.cloneDeep(prof.data);
|
||||||
this.commit('reader/setProfiles', prof.data);
|
this.commit('reader/setProfiles', prof.data);
|
||||||
this.commit('reader/setProfilesRev', prof.rev);
|
this.commit('reader/setProfilesRev', prof.rev);
|
||||||
|
this.checkCurrentProfile();
|
||||||
|
|
||||||
this.notifySuccess();
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
} else {
|
} else {
|
||||||
this.warning(`Неверный ответ сервера: ${prof.state}`);
|
this.warning(`Неверный ответ сервера: ${prof.state}`);
|
||||||
}
|
}
|
||||||
@@ -304,7 +309,7 @@ class ServerStorage extends Vue {
|
|||||||
if (utils.isEmptyObjDiff(diff))
|
if (utils.isEmptyObjDiff(diff))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//обнуляются профили во время разработки, подстраховка
|
//обнуляются профили во время разработки при hotReload, подстраховка
|
||||||
if (!this.$store.state.reader.allowProfilesSave) {
|
if (!this.$store.state.reader.allowProfilesSave) {
|
||||||
console.error('Сохранение профилей не санкционировано');
|
console.error('Сохранение профилей не санкционировано');
|
||||||
return;
|
return;
|
||||||
@@ -313,33 +318,16 @@ class ServerStorage extends Vue {
|
|||||||
this.savingProfiles = true;
|
this.savingProfiles = true;
|
||||||
try {
|
try {
|
||||||
let result = {state: ''};
|
let result = {state: ''};
|
||||||
let tries = 0;
|
|
||||||
while (result.state != 'success' && tries < maxSetTries) {
|
|
||||||
try {
|
try {
|
||||||
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
|
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.savingProfiles = false;
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
this.commit('reader/setProfiles', this.oldProfiles);
|
|
||||||
this.checkCurrentProfile();
|
|
||||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения отменены.`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.state == 'reject') {
|
if (result.state == 'reject') {
|
||||||
await this.loadProfiles(true);
|
await this.loadProfiles(true, false);
|
||||||
const newProfiles = utils.applyObjDiff(this.profiles, diff);
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
this.commit('reader/setProfiles', newProfiles);
|
} else if (result.state == 'success') {
|
||||||
}
|
|
||||||
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tries >= maxSetTries) {
|
|
||||||
this.commit('reader/setProfiles', this.oldProfiles);
|
|
||||||
this.checkCurrentProfile();
|
|
||||||
console.error(result);
|
|
||||||
this.error('Не удалось отправить данные на сервер. Изменения отменены.');
|
|
||||||
} else {
|
|
||||||
this.oldProfiles = _.cloneDeep(this.profiles);
|
this.oldProfiles = _.cloneDeep(this.profiles);
|
||||||
this.commit('reader/setProfilesRev', this.profilesRev + 1);
|
this.commit('reader/setProfilesRev', this.profilesRev + 1);
|
||||||
}
|
}
|
||||||
@@ -348,252 +336,195 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadRecent(force) {
|
async initRecentDelta() {
|
||||||
|
let recentDelta = null;
|
||||||
|
try {
|
||||||
|
recentDelta = await this.storageGet({recentDelta: {}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentDelta.state == 'success') {
|
||||||
|
recentDelta = recentDelta.items.recentDelta;
|
||||||
|
|
||||||
|
if (recentDelta.rev == 0)
|
||||||
|
recentDelta.data = {};
|
||||||
|
|
||||||
|
this.recentDelta = recentDelta.data;
|
||||||
|
this.recentDeltaInited = true;
|
||||||
|
} else {
|
||||||
|
this.warning(`Неверный ответ сервера: ${recentDelta.state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadRecent(doNotifySuccess = true) {
|
||||||
if (!this.keyInited || !this.serverSyncEnabled)
|
if (!this.keyInited || !this.serverSyncEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const oldRev = bookManager.recentRev;
|
const oldRecentRev = bookManager.recentRev;
|
||||||
const oldLastRev = bookManager.recentLastRev;
|
const oldRecentDeltaRev = bookManager.recentDeltaRev;
|
||||||
const oldLastDiffRev = bookManager.recentLastDiffRev;
|
|
||||||
//проверим ревизию на сервере
|
//проверим ревизию на сервере
|
||||||
let revs = null;
|
let revs = null;
|
||||||
if (!force) {
|
|
||||||
try {
|
try {
|
||||||
revs = await this.storageCheck({recent: {}, recentLast: {}, recentLastDiff: {}});
|
revs = await this.storageCheck({recent: {}, recentDelta: {}});
|
||||||
if (revs.state == 'success' && revs.items.recent.rev == oldRev &&
|
if (revs.state == 'success' && revs.items.recent.rev == oldRecentRev &&
|
||||||
revs.items.recentLast.rev == oldLastRev &&
|
revs.items.recentDelta.rev == oldRecentDeltaRev) {
|
||||||
revs.items.recentLastDiff.rev == oldLastDiffRev) {
|
if (!this.recentDeltaInited)
|
||||||
|
await this.initRecentDelta();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (force || revs.items.recent.rev != oldRev) {
|
|
||||||
let recent = null;
|
let recent = null;
|
||||||
try {
|
try {
|
||||||
recent = await this.storageGet({recent: {}});
|
if (revs.items.recent.rev != oldRecentRev) {
|
||||||
|
recent = await this.storageGet({recent: {}, recentDelta: {}});
|
||||||
|
} else {
|
||||||
|
recent = await this.storageGet({recentDelta: {}});
|
||||||
|
recent.items.recent = {data: _.cloneDeep(bookManager.recent), rev: oldRecentRev};
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recent.state == 'success') {
|
if (recent.state == 'success') {
|
||||||
|
let recentDelta = recent.items.recentDelta;
|
||||||
recent = recent.items.recent;
|
recent = recent.items.recent;
|
||||||
|
|
||||||
if (recent.rev == 0)
|
if (recent.rev == 0)
|
||||||
recent.data = {};
|
recent.data = {};
|
||||||
|
|
||||||
this.oldRecent = _.cloneDeep(recent.data);
|
let newRecent = {};
|
||||||
await bookManager.setRecent(recent.data);
|
if (recentDelta && recentDelta.data) {
|
||||||
|
if (recentDelta.data.diff) {
|
||||||
|
newRecent = recent.data;
|
||||||
|
const key = recentDelta.data.diff.key;
|
||||||
|
if (newRecent[key])
|
||||||
|
newRecent[key] = utils.applyObjDiff(newRecent[key], recentDelta.data.diff);
|
||||||
|
} else {
|
||||||
|
newRecent = Object.assign(recent.data, recentDelta.data);
|
||||||
|
}
|
||||||
|
this.recentDelta = recentDelta.data;
|
||||||
|
} else {
|
||||||
|
newRecent = recent.data;
|
||||||
|
this.recentDelta = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recentDeltaInited = true;
|
||||||
|
|
||||||
|
if (!bookManager.loaded) {
|
||||||
|
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||||
|
while (!bookManager.loaded) await utils.sleep(100);
|
||||||
|
}
|
||||||
|
await bookManager.setRecent(newRecent);
|
||||||
await bookManager.setRecentRev(recent.rev);
|
await bookManager.setRecentRev(recent.rev);
|
||||||
|
await bookManager.setRecentDeltaRev(recentDelta.rev);
|
||||||
} else {
|
} else {
|
||||||
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
this.warning(`Неверный ответ сервера: ${recent.state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (doNotifySuccess)
|
||||||
|
this.debouncedNotifySuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force || revs.items.recentLast.rev != oldLastRev || revs.items.recentLastDiff.rev != oldLastDiffRev) {
|
async saveRecent(itemKey, recurse) {
|
||||||
let recentLast = null;
|
|
||||||
try {
|
|
||||||
recentLast = await this.storageGet({recentLast: {}, recentLastDiff: {}});
|
|
||||||
} catch(e) {
|
|
||||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recentLast.state == 'success') {
|
|
||||||
const recentLastDiff = recentLast.items.recentLastDiff;
|
|
||||||
recentLast = recentLast.items.recentLast;
|
|
||||||
|
|
||||||
if (recentLast.rev == 0)
|
|
||||||
recentLast.data = {};
|
|
||||||
if (recentLastDiff.rev == 0)
|
|
||||||
recentLastDiff.data = {};
|
|
||||||
|
|
||||||
this.oldRecentLastDiff = _.cloneDeep(recentLastDiff.data);
|
|
||||||
this.oldRecentLast = _.cloneDeep(recentLast.data);
|
|
||||||
|
|
||||||
recentLast.data = utils.applyObjDiff(recentLast.data, recentLastDiff.data);
|
|
||||||
|
|
||||||
await bookManager.setRecentLast(recentLast.data);
|
|
||||||
await bookManager.setRecentLastRev(recentLast.rev);
|
|
||||||
await bookManager.setRecentLastDiffRev(recentLastDiff.rev);
|
|
||||||
} else {
|
|
||||||
this.warning(`Неверный ответ сервера: ${recentLast.state}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notifySuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveRecent() {
|
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const bm = bookManager;
|
const bm = bookManager;
|
||||||
|
|
||||||
const diff = utils.getObjDiff(this.oldRecent, bm.recent);
|
//вычисление критерия сохранения целиком
|
||||||
if (utils.isEmptyObjDiff(diff))
|
if (!this.sameKeyCount)
|
||||||
return;
|
this.sameKeyCount = 0;
|
||||||
|
if (this.prevItemKey == itemKey) {
|
||||||
|
this.sameKeyCount++;
|
||||||
|
} else {
|
||||||
|
this.sameKeyCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = Object.keys(this.recentDelta).length - (1*(!!this.recentDelta.diff));
|
||||||
|
this.makeDeltaDiff = (l == 1 && this.prevItemKey == itemKey ? this.makeDeltaDiff : false);
|
||||||
|
const forceSaveRecent = l > 10 || (this.sameKeyCount > 5 && (l > 1)) || (l == 1 && this.sameKeyCount > 10 && !this.makeDeltaDiff);
|
||||||
|
|
||||||
|
this.sameKeyCount = (!forceSaveRecent ? this.sameKeyCount : 0);
|
||||||
|
this.prevItemKey = itemKey;
|
||||||
|
|
||||||
|
//дифф от дельты для уменьшения размера передаваемых данных в частном случае
|
||||||
|
if (this.makeDeltaDiff) {
|
||||||
|
this.recentDelta.diff = utils.getObjDiff(this.prevSavedItem, bm.recent[itemKey]);
|
||||||
|
this.recentDelta.diff.key = itemKey;
|
||||||
|
delete this.recentDelta[itemKey];
|
||||||
|
} else if (this.recentDelta.diff) {
|
||||||
|
const key = this.recentDelta.diff.key;
|
||||||
|
if (!this.prevSavedItem && bm.recent[key])
|
||||||
|
this.prevSavedItem = _.cloneDeep(bm.recent[key]);
|
||||||
|
if (this.prevSavedItem) {
|
||||||
|
this.recentDelta[key] = utils.applyObjDiff(this.prevSavedItem, this.recentDelta.diff);
|
||||||
|
}
|
||||||
|
delete this.recentDelta.diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
//сохранение
|
||||||
this.savingRecent = true;
|
this.savingRecent = true;
|
||||||
try {
|
try {
|
||||||
|
if (forceSaveRecent) {//сохраняем recent целиком
|
||||||
let result = {state: ''};
|
let result = {state: ''};
|
||||||
let tries = 0;
|
|
||||||
while (result.state != 'success' && tries < maxSetTries) {
|
|
||||||
try {
|
try {
|
||||||
result = await this.storageSet({recent: {rev: bm.recentRev + 1, data: bm.recent}});
|
result = await this.storageSet({recent: {rev: bm.recentRev + 1, data: bm.recent}, recentDelta: {rev: bm.recentDeltaRev + 1, data: {}}});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
this.savingRecent = false;
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения не сохранены.`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.state == 'reject') {
|
if (result.state == 'reject') {
|
||||||
await this.loadRecent(true);
|
await this.loadRecent(false);
|
||||||
//похоже это лишнее
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
/*const newRecent = utils.applyObjDiff(bm.recent, diff);
|
if (!recurse) {
|
||||||
await bm.setRecent(newRecent);*/
|
this.savingRecent = false;
|
||||||
|
this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||||
|
this.saveRecent(itemKey, true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
this.makeDeltaDiff = true;
|
||||||
|
this.prevSavedItem = _.cloneDeep(bm.recent[itemKey]);
|
||||||
|
|
||||||
tries++;
|
this.recentDelta = {};
|
||||||
}
|
|
||||||
|
|
||||||
if (tries >= maxSetTries) {
|
|
||||||
console.error(result);
|
|
||||||
this.error('Не удалось отправить данные на сервер. Данные не сохранены и могут быть перезаписаны.');
|
|
||||||
} else {
|
|
||||||
this.oldRecent = _.cloneDeep(bm.recent);
|
|
||||||
await bm.setRecentRev(bm.recentRev + 1);
|
await bm.setRecentRev(bm.recentRev + 1);
|
||||||
await this.saveRecentLast(true);
|
await bm.setRecentDeltaRev(bm.recentDeltaRev + 1);
|
||||||
|
}
|
||||||
|
} else {//сохраняем только дифф
|
||||||
|
let result = {state: ''};
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await this.storageSet({recentDelta: {rev: bm.recentDeltaRev + 1, data: this.recentDelta}});
|
||||||
|
} catch(e) {
|
||||||
|
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.state == 'reject') {
|
||||||
|
await this.loadRecent(false);
|
||||||
|
this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
|
||||||
|
if (!recurse) {
|
||||||
|
this.savingRecent = false;
|
||||||
|
this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
|
||||||
|
this.saveRecent(itemKey, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (result.state == 'success') {
|
||||||
|
await bm.setRecentDeltaRev(bm.recentDeltaRev + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.savingRecent = false;
|
this.savingRecent = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRecentLast(force = false) {
|
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecentLast)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bm = bookManager;
|
|
||||||
let recentLast = bm.recentLast;
|
|
||||||
recentLast = (recentLast ? recentLast : {});
|
|
||||||
let lastRev = bm.recentLastRev;
|
|
||||||
|
|
||||||
const diff = utils.getObjDiff(this.oldRecentLast, recentLast);
|
|
||||||
if (utils.isEmptyObjDiff(diff))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (this.oldRecentLast.key == recentLast.key && JSON.stringify(recentLast) > JSON.stringify(diff)) {
|
|
||||||
await this.saveRecentLastDiff(diff, force);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.savingRecentLast = true;
|
|
||||||
try {
|
|
||||||
let result = {state: ''};
|
|
||||||
let tries = 0;
|
|
||||||
while (result.state != 'success' && tries < maxSetTries) {
|
|
||||||
if (force) {
|
|
||||||
try {
|
|
||||||
const revs = await this.storageCheck({recentLast: {}});
|
|
||||||
if (revs.items.recentLast.rev)
|
|
||||||
lastRev = revs.items.recentLast.rev;
|
|
||||||
} catch(e) {
|
|
||||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await this.storageSet({recentLast: {rev: lastRev + 1, data: recentLast}}, force);
|
|
||||||
} catch(e) {
|
|
||||||
this.savingRecentLast = false;
|
|
||||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения не сохранены.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.state == 'reject') {
|
|
||||||
await this.loadRecent(false);
|
|
||||||
this.savingRecentLast = false;//!!!
|
|
||||||
return;//!!!
|
|
||||||
}
|
|
||||||
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tries >= maxSetTries) {
|
|
||||||
console.error(result);
|
|
||||||
this.error('Не удалось отправить данные на сервер. Данные не сохранены и могут быть перезаписаны.');
|
|
||||||
} else {
|
|
||||||
this.oldRecentLast = _.cloneDeep(recentLast);
|
|
||||||
await bm.setRecentLastRev(lastRev + 1);
|
|
||||||
await this.saveRecentLastDiff({}, true);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.savingRecentLast = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveRecentLastDiff(diff, force = false) {
|
|
||||||
if (!this.keyInited || !this.serverSyncEnabled || this.savingRecentLastDiff)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bm = bookManager;
|
|
||||||
let lastRev = bm.recentLastDiffRev;
|
|
||||||
|
|
||||||
const d = utils.getObjDiff(this.oldRecentLastDiff, diff);
|
|
||||||
if (utils.isEmptyObjDiff(d))
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.savingRecentLastDiff = true;
|
|
||||||
try {
|
|
||||||
let result = {state: ''};
|
|
||||||
let tries = 0;
|
|
||||||
while (result.state != 'success' && tries < maxSetTries) {
|
|
||||||
if (force) {
|
|
||||||
try {
|
|
||||||
const revs = await this.storageCheck({recentLastDiff: {}});
|
|
||||||
if (revs.items.recentLastDiff.rev)
|
|
||||||
lastRev = revs.items.recentLastDiff.rev;
|
|
||||||
} catch(e) {
|
|
||||||
this.error(`Ошибка соединения с сервером: ${e.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await this.storageSet({recentLastDiff: {rev: lastRev + 1, data: diff}}, force);
|
|
||||||
} catch(e) {
|
|
||||||
this.savingRecentLastDiff = false;
|
|
||||||
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения не сохранены.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.state == 'reject') {
|
|
||||||
await this.loadRecent(false);
|
|
||||||
this.savingRecentLastDiff = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tries >= maxSetTries) {
|
|
||||||
console.error(result);
|
|
||||||
this.error('Не удалось отправить данные на сервер. Данные не сохранены и могут быть перезаписаны.');
|
|
||||||
} else {
|
|
||||||
this.oldRecentLastDiff = _.cloneDeep(diff);
|
|
||||||
await bm.setRecentLastDiffRev(lastRev + 1);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.savingRecentLastDiff = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async storageCheck(items) {
|
async storageCheck(items) {
|
||||||
return await this.storageApi('check', items);
|
return await this.storageApi('check', items);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
Установить позицию
|
Установить позицию
|
||||||
</template>
|
</template>
|
||||||
@@ -10,8 +8,6 @@
|
|||||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -43,6 +39,8 @@ class SetPositionPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(sliderValue, sliderMax) {
|
init(sliderValue, sliderMax) {
|
||||||
|
this.$refs.window.init();
|
||||||
|
|
||||||
this.sliderMax = sliderMax;
|
this.sliderMax = sliderMax;
|
||||||
this.sliderValue = sliderValue;
|
this.sliderValue = sliderValue;
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
@@ -70,26 +68,6 @@ class SetPositionPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 40;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
height: 140px;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main" @click="close">
|
<Window ref="window" height="95%" width="600px" @close="close">
|
||||||
<div class="mainWindow" @click.stop>
|
|
||||||
<Window @close="close">
|
|
||||||
<template slot="header">
|
<template slot="header">
|
||||||
Настройки
|
Настройки
|
||||||
</template>
|
</template>
|
||||||
@@ -284,6 +282,19 @@
|
|||||||
<el-checkbox v-model="textAlignJustify">По ширине</el-checkbox>
|
<el-checkbox v-model="textAlignJustify">По ширине</el-checkbox>
|
||||||
<el-checkbox v-model="wordWrap">Перенос по слогам</el-checkbox>
|
<el-checkbox v-model="wordWrap">Перенос по слогам</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="">
|
||||||
|
<el-col :span="12">
|
||||||
|
Компактность
|
||||||
|
</el-col>
|
||||||
|
<el-tooltip :open-delay="500" effect="light" placement="top">
|
||||||
|
<template slot="content">
|
||||||
|
Степень компактности текста в процентах.<br>
|
||||||
|
Чем больше компактность, тем хуже выравнивание<br>
|
||||||
|
по правому краю.
|
||||||
|
</template>
|
||||||
|
<el-input-number v-model="compactTextPerc" :min="0" :max="100"></el-input-number>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="Обработка">
|
<el-form-item label="Обработка">
|
||||||
<el-checkbox v-model="cutEmptyParagraphs">Убирать пустые строки</el-checkbox>
|
<el-checkbox v-model="cutEmptyParagraphs">Убирать пустые строки</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -447,6 +458,14 @@
|
|||||||
<el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
|
<el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="Уведомление">
|
||||||
|
<el-tooltip :open-delay="500" effect="light">
|
||||||
|
<template slot="content">
|
||||||
|
Показывать диалог о переходе на httpS-версию
|
||||||
|
</template>
|
||||||
|
<el-checkbox v-model="showMigrationDialog">Уведомлять о переходе на httpS-версию</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
|
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>
|
||||||
@@ -494,8 +513,6 @@
|
|||||||
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -520,6 +537,7 @@ export default @Component({
|
|||||||
this.settingsChanged();
|
this.settingsChanged();
|
||||||
},
|
},
|
||||||
form: function(newValue) {
|
form: function(newValue) {
|
||||||
|
if (this.inited)
|
||||||
this.commit('reader/setSettings', newValue);
|
this.commit('reader/setSettings', newValue);
|
||||||
},
|
},
|
||||||
fontBold: function(newValue) {
|
fontBold: function(newValue) {
|
||||||
@@ -569,6 +587,11 @@ class SettingsPage extends Vue {
|
|||||||
this.settingsChanged();
|
this.settingsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.$refs.window.init();
|
||||||
|
this.inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
settingsChanged() {
|
settingsChanged() {
|
||||||
if (_.isEqual(this.form, this.settings))
|
if (_.isEqual(this.form, this.settings))
|
||||||
return;
|
return;
|
||||||
@@ -632,7 +655,7 @@ class SettingsPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get setStorageKeyLink() {
|
get setStorageKeyLink() {
|
||||||
return `http://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
|
return `https://${window.location.host}/#/reader?setStorageAccessKey=${utils.toBase58(this.serverStorageKey)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get predefineTextColors() {
|
get predefineTextColors() {
|
||||||
@@ -671,9 +694,10 @@ class SettingsPage extends Vue {
|
|||||||
|
|
||||||
async setDefaults() {
|
async setDefaults() {
|
||||||
try {
|
try {
|
||||||
if (await this.$confirm('Подтвердите установку настроек по-умолчанию:', '', {
|
if (await this.$confirm('Подтвердите установку настроек по умолчанию:', '', {
|
||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Отмена',
|
cancelButtonText: 'Отмена',
|
||||||
|
customClass: 'prompt-dialog',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
})) {
|
})) {
|
||||||
this.form = Object.assign({}, rstore.settingDefaults);
|
this.form = Object.assign({}, rstore.settingDefaults);
|
||||||
@@ -700,6 +724,7 @@ class SettingsPage extends Vue {
|
|||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Отмена',
|
cancelButtonText: 'Отмена',
|
||||||
inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
|
inputValidator: (str) => { if (!str) return 'Название не должно быть пустым'; else if (str.length > 50) return 'Слишком длинное название'; else return true; },
|
||||||
|
customClass: 'prompt-dialog',
|
||||||
});
|
});
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
if (this.profiles[result.value]) {
|
if (this.profiles[result.value]) {
|
||||||
@@ -731,7 +756,8 @@ class SettingsPage extends Vue {
|
|||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Отмена',
|
cancelButtonText: 'Отмена',
|
||||||
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||||
type: 'warning'
|
customClass: 'prompt-dialog',
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.value && result.value.toLowerCase() == 'да') {
|
if (result.value && result.value.toLowerCase() == 'да') {
|
||||||
@@ -762,7 +788,8 @@ class SettingsPage extends Vue {
|
|||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Отмена',
|
cancelButtonText: 'Отмена',
|
||||||
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Удаление не подтверждено'; },
|
||||||
type: 'warning'
|
customClass: 'prompt-dialog',
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.value && result.value.toLowerCase() == 'да') {
|
if (result.value && result.value.toLowerCase() == 'да') {
|
||||||
@@ -801,7 +828,8 @@ class SettingsPage extends Vue {
|
|||||||
cancelButtonText: 'Отмена',
|
cancelButtonText: 'Отмена',
|
||||||
inputValidator: (str) => { if (str && utils.fromBase58(str).length == 32) return true; else return 'Неверный формат ключа'; },
|
inputValidator: (str) => { if (str && utils.fromBase58(str).length == 32) return true; else return 'Неверный формат ключа'; },
|
||||||
inputValue: (key && _.isString(key) ? key : null),
|
inputValue: (key && _.isString(key) ? key : null),
|
||||||
type: 'warning'
|
customClass: 'prompt-dialog',
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.value && utils.fromBase58(result.value).length == 32) {
|
if (result.value && utils.fromBase58(result.value).length == 32) {
|
||||||
@@ -820,7 +848,8 @@ class SettingsPage extends Vue {
|
|||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: 'Отмена',
|
cancelButtonText: 'Отмена',
|
||||||
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
|
inputValidator: (str) => { if (str && str.toLowerCase() === 'да') return true; else return 'Генерация не подтверждена'; },
|
||||||
type: 'warning'
|
customClass: 'prompt-dialog',
|
||||||
|
type: 'warning',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.value && result.value.toLowerCase() == 'да') {
|
if (result.value && result.value.toLowerCase() == 'да') {
|
||||||
@@ -843,23 +872,6 @@ class SettingsPage extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 60;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainWindow {
|
|
||||||
height: 70%;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
line-height: 130%;
|
line-height: 130%;
|
||||||
@@ -871,9 +883,9 @@ class SettingsPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
padding: 0;
|
padding: 0 !important;
|
||||||
margin: 0;
|
margin: 0 !important;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picked {
|
.color-picked {
|
||||||
@@ -905,3 +917,10 @@ class SettingsPage extends Vue {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prompt-dialog {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -350,20 +350,18 @@ export default class DrawHelper {
|
|||||||
page2.style.background = backgroundColor;
|
page2.style.background = backgroundColor;
|
||||||
|
|
||||||
if (isDown) {
|
if (isDown) {
|
||||||
page2.style.transformOrigin = '10%';
|
page2.style.transformOrigin = '5%';
|
||||||
await sleep(30);
|
await sleep(30);
|
||||||
|
|
||||||
page2.style.transformOrigin = '0%';
|
|
||||||
page2.style.transition = `${duration}ms ease-in-out`;
|
page2.style.transition = `${duration}ms ease-in-out`;
|
||||||
page2.style.transform = `rotateY(-120deg)`;
|
page2.style.transform = `rotateY(-120deg) translateX(${this.w/4}px)`;
|
||||||
await animation2Finish(duration);
|
await animation2Finish(duration);
|
||||||
} else {
|
} else {
|
||||||
page2.style.transformOrigin = '90%';
|
page2.style.transformOrigin = '95%';
|
||||||
await sleep(30);
|
await sleep(30);
|
||||||
|
|
||||||
page2.style.transformOrigin = '100%';
|
|
||||||
page2.style.transition = `${duration}ms ease-in-out`;
|
page2.style.transition = `${duration}ms ease-in-out`;
|
||||||
page2.style.transform = `rotateY(120deg)`;
|
page2.style.transform = `rotateY(120deg) translateX(-${this.w/4}px)`;
|
||||||
await animation2Finish(duration);
|
await animation2Finish(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ class TextPage extends Vue {
|
|||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
||||||
this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -221,6 +220,7 @@ class TextPage extends Vue {
|
|||||||
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
||||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||||
|
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||||
}
|
}
|
||||||
|
|
||||||
//scrolling page
|
//scrolling page
|
||||||
@@ -1006,7 +1006,7 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTouchStart(event) {
|
onTouchStart(event) {
|
||||||
if (!this.mobile)
|
if (!this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
if (event.touches.length == 1) {
|
if (event.touches.length == 1) {
|
||||||
@@ -1022,19 +1022,19 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTouchEnd() {
|
onTouchEnd() {
|
||||||
if (!this.mobile)
|
if (!this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
onTouchCancel() {
|
onTouchCancel() {
|
||||||
if (!this.mobile)
|
if (!this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown(event) {
|
onMouseDown(event) {
|
||||||
if (this.mobile)
|
if (this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
if (event.button == 0) {
|
if (event.button == 0) {
|
||||||
@@ -1050,13 +1050,13 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMouseUp() {
|
onMouseUp() {
|
||||||
if (this.mobile)
|
if (this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
this.endClickRepeat();
|
this.endClickRepeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel(event) {
|
onMouseWheel(event) {
|
||||||
if (this.mobile)
|
if (this.$isMobileDevice)
|
||||||
return;
|
return;
|
||||||
if (event.deltaY > 0) {
|
if (event.deltaY > 0) {
|
||||||
this.doDown();
|
this.doDown();
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export default class BookParser {
|
|||||||
if (tag == 'binary') {
|
if (tag == 'binary') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
||||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +620,8 @@ export default class BookParser {
|
|||||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
||||||
para.parsed.showImages === this.showImages &&
|
para.parsed.showImages === this.showImages &&
|
||||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
para.parsed.imageHeightLines === this.imageHeightLines &&
|
||||||
para.parsed.imageFitWidth === this.imageFitWidth
|
para.parsed.imageFitWidth === this.imageFitWidth &&
|
||||||
|
para.parsed.compactTextPerc === this.compactTextPerc
|
||||||
)
|
)
|
||||||
return para.parsed;
|
return para.parsed;
|
||||||
|
|
||||||
@@ -635,6 +636,7 @@ export default class BookParser {
|
|||||||
showImages: this.showImages,
|
showImages: this.showImages,
|
||||||
imageHeightLines: this.imageHeightLines,
|
imageHeightLines: this.imageHeightLines,
|
||||||
imageFitWidth: this.imageFitWidth,
|
imageFitWidth: this.imageFitWidth,
|
||||||
|
compactTextPerc: this.compactTextPerc,
|
||||||
visible: !(
|
visible: !(
|
||||||
(this.cutEmptyParagraphs && para.cut) ||
|
(this.cutEmptyParagraphs && para.cut) ||
|
||||||
(para.addIndex > this.addEmptyParagraphs)
|
(para.addIndex > this.addEmptyParagraphs)
|
||||||
@@ -665,6 +667,7 @@ export default class BookParser {
|
|||||||
let style = {};
|
let style = {};
|
||||||
let ofs = 0;//смещение от начала параграфа para.offset
|
let ofs = 0;//смещение от начала параграфа para.offset
|
||||||
let imgW = 0;
|
let imgW = 0;
|
||||||
|
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
||||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
style = part.style;
|
style = part.style;
|
||||||
@@ -749,7 +752,7 @@ export default class BookParser {
|
|||||||
p = (style.space ? p + parsed.p*style.space : p);
|
p = (style.space ? p + parsed.p*style.space : p);
|
||||||
let w = this.measureText(str, style) + p;
|
let w = this.measureText(str, style) + p;
|
||||||
let wordTail = word;
|
let wordTail = word;
|
||||||
if (w > parsed.w && prevStr != '') {
|
if (w > parsed.w + compactWidth && prevStr != '') {
|
||||||
if (parsed.wordWrap) {//по слогам
|
if (parsed.wordWrap) {//по слогам
|
||||||
let slogi = this.splitToSlogi(word);
|
let slogi = this.splitToSlogi(word);
|
||||||
|
|
||||||
@@ -762,7 +765,7 @@ export default class BookParser {
|
|||||||
for (let k = 0; k < slogiLen - 1; k++) {
|
for (let k = 0; k < slogiLen - 1; k++) {
|
||||||
let slog = slogi[0];
|
let slog = slogi[0];
|
||||||
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
let ww = this.measureText(s + slog + (slog[slog.length - 1] == '-' ? '' : '-'), style) + p;
|
||||||
if (ww <= parsed.w) {
|
if (ww <= parsed.w + compactWidth) {
|
||||||
s += slog;
|
s += slog;
|
||||||
ss += slog;
|
ss += slog;
|
||||||
} else
|
} else
|
||||||
|
|||||||
@@ -18,49 +18,41 @@ const bmRecentStore = localForage.createInstance({
|
|||||||
name: 'bmRecentStore'
|
name: 'bmRecentStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
const bmCacheStore = localForage.createInstance({
|
|
||||||
name: 'bmCacheStore'
|
|
||||||
});
|
|
||||||
|
|
||||||
class BookManager {
|
class BookManager {
|
||||||
async init(settings) {
|
async init(settings) {
|
||||||
|
this.loaded = false;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
|
||||||
this.eventListeners = [];
|
this.eventListeners = [];
|
||||||
|
|
||||||
//bmCacheStore нужен только для ускорения загрузки читалки
|
|
||||||
this.booksCached = await bmCacheStore.getItem('books');
|
|
||||||
if (!this.booksCached)
|
|
||||||
this.booksCached = {};
|
|
||||||
this.recent = await bmCacheStore.getItem('recent');
|
|
||||||
this.recentLast = await bmCacheStore.getItem('recent-last');
|
|
||||||
if (this.recentLast)
|
|
||||||
this.recent[this.recentLast.key] = this.recentLast;
|
|
||||||
this.recentRev = await bmRecentStore.getItem('recent-rev') || 0;
|
|
||||||
this.recentLastRev = await bmRecentStore.getItem('recent-last-rev') || 0;
|
|
||||||
this.recentLastDiffRev = await bmRecentStore.getItem('recent-last-diff-rev') || 0;
|
|
||||||
this.books = Object.assign({}, this.booksCached);
|
|
||||||
|
|
||||||
this.recentChanged2 = true;
|
|
||||||
|
|
||||||
if (!this.books || !this.recent) {
|
|
||||||
this.books = {};
|
this.books = {};
|
||||||
this.recent = {};
|
this.recent = {};
|
||||||
await this.loadMeta(true);
|
|
||||||
} else {
|
this.recentLast = await bmRecentStore.getItem('recent-last');
|
||||||
this.loadMeta(false);
|
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.recentRev = await bmRecentStore.getItem('recent-rev') || 0;
|
||||||
//хранение в отдельных записях дает относительно
|
this.recentDeltaRev = await bmRecentStore.getItem('recent-delta-rev') || 0;
|
||||||
//нормальное поведение при нескольких вкладках с читалкой в браузере
|
|
||||||
async loadMeta(immediate) {
|
this.recentChanged = true;
|
||||||
if (!immediate)
|
|
||||||
|
this.loadStored();//no await
|
||||||
|
}
|
||||||
|
|
||||||
|
//Долгая асинхронная загрузка из хранилища.
|
||||||
|
//Хранение в отдельных записях дает относительно
|
||||||
|
//нормальное поведение при нескольких вкладках с читалкой в браузере.
|
||||||
|
async loadStored() {
|
||||||
|
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||||
await utils.sleep(2000);
|
await utils.sleep(2000);
|
||||||
|
|
||||||
let len = await bmMetaStore.length();
|
let len = await bmMetaStore.length();
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
const key = await bmMetaStore.key(i);
|
const key = await bmMetaStore.key(i);
|
||||||
const keySplit = key.split('-');
|
const keySplit = key.split('-');
|
||||||
|
|
||||||
@@ -68,6 +60,7 @@ class BookManager {
|
|||||||
let meta = await bmMetaStore.getItem(key);
|
let meta = await bmMetaStore.getItem(key);
|
||||||
|
|
||||||
if (_.isObject(meta)) {
|
if (_.isObject(meta)) {
|
||||||
|
//уже может быть распарсена книга
|
||||||
const oldBook = this.books[meta.key];
|
const oldBook = this.books[meta.key];
|
||||||
this.books[meta.key] = meta;
|
this.books[meta.key] = meta;
|
||||||
|
|
||||||
@@ -80,11 +73,9 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//"ленивая" загрузка
|
|
||||||
(async() => {
|
|
||||||
let key = null;
|
let key = null;
|
||||||
len = await bmRecentStore.length();
|
len = await bmRecentStore.length();
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
key = await bmRecentStore.key(i);
|
key = await bmRecentStore.key(i);
|
||||||
if (key) {
|
if (key) {
|
||||||
let r = await bmRecentStore.getItem(key);
|
let r = await bmRecentStore.getItem(key);
|
||||||
@@ -95,7 +86,6 @@ class BookManager {
|
|||||||
await bmRecentStore.removeItem(key);
|
await bmRecentStore.removeItem(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
|
||||||
//размножение для дебага
|
//размножение для дебага
|
||||||
/*if (key) {
|
/*if (key) {
|
||||||
@@ -106,17 +96,11 @@ class BookManager {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
await this.cleanBooks();
|
await this.cleanBooks();
|
||||||
|
await this.cleanRecentBooks();
|
||||||
|
|
||||||
//очистка позже
|
this.recentChanged = true;
|
||||||
//await this.cleanRecentBooks();
|
this.loaded = true;
|
||||||
|
this.emit('load-stored-finish');
|
||||||
this.booksCached = {};
|
|
||||||
for (const key in this.books) {
|
|
||||||
this.booksCached[key] = this.metaOnly(this.books[key]);
|
|
||||||
}
|
|
||||||
await bmCacheStore.setItem('books', this.booksCached);
|
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
this.emit('load-meta-finish');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanBooks() {
|
async cleanBooks() {
|
||||||
@@ -136,14 +120,13 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (size > maxDataSize && toDel) {
|
if (size > maxDataSize && toDel) {
|
||||||
await this._delBook(toDel);
|
await this.delBook(toDel);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async deflateWithProgress(data, callback) {
|
async deflateWithProgress(data, callback) {
|
||||||
const chunkSize = 128*1024;
|
const chunkSize = 128*1024;
|
||||||
const deflator = new utils.pako.Deflate({level: 5});
|
const deflator = new utils.pako.Deflate({level: 5});
|
||||||
@@ -178,9 +161,41 @@ class BookManager {
|
|||||||
return deflator.result;
|
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) {
|
async addBook(newBook, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
let meta = {url: newBook.url, path: newBook.path};
|
let meta = {url: newBook.url, path: newBook.path};
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromUrl(meta.url);
|
||||||
meta.addTime = Date.now();
|
meta.addTime = Date.now();
|
||||||
@@ -200,51 +215,67 @@ class BookManager {
|
|||||||
|
|
||||||
let data = newBook.data;
|
let data = newBook.data;
|
||||||
if (result.dataCompressed) {
|
if (result.dataCompressed) {
|
||||||
//data = utils.pako.deflate(data, {level: 9});
|
//data = utils.pako.deflate(data, {level: 5});
|
||||||
data = await this.deflateWithProgress(data, cb2);
|
data = await this.deflateWithProgress(data, cb2);
|
||||||
result.dataCompressedLength = data.byteLength;
|
result.dataCompressedLength = data.byteLength;
|
||||||
}
|
}
|
||||||
callback(95);
|
callback(95);
|
||||||
|
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
this.booksCached[meta.key] = this.metaOnly(result);
|
|
||||||
|
|
||||||
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
await bmMetaStore.setItem(`bmMeta-${meta.key}`, this.metaOnly(result));
|
||||||
await bmDataStore.setItem(`bmData-${meta.key}`, data);
|
await bmDataStore.setItem(`bmData-${meta.key}`, data);
|
||||||
await bmCacheStore.setItem('books', this.booksCached);
|
|
||||||
|
|
||||||
callback(100);
|
callback(100);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBookParsed(meta) {
|
async hasBookParsed(meta) {
|
||||||
if (!this.books)
|
if (!this.books)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.url)
|
if (!meta.url)
|
||||||
return false;
|
return false;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromUrl(meta.url);
|
||||||
|
|
||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
|
if (!book && !this.loaded) {
|
||||||
|
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||||
|
if (book)
|
||||||
|
this.books[meta.key] = book;
|
||||||
|
}
|
||||||
|
|
||||||
return !!(book && book.parsed);
|
return !!(book && book.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBook(meta, callback) {
|
async getBook(meta, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
if (!meta.key)
|
if (!meta.key)
|
||||||
meta.key = this.keyFromUrl(meta.url);
|
meta.key = this.keyFromUrl(meta.url);
|
||||||
|
|
||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
||||||
|
if (result)
|
||||||
|
this.books[meta.key] = result;
|
||||||
|
}
|
||||||
|
|
||||||
if (result && !result.parsed) {
|
if (result && !result.parsed) {
|
||||||
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
|
||||||
callback(10);
|
callback(5);
|
||||||
await utils.sleep(10);
|
await utils.sleep(10);
|
||||||
|
|
||||||
|
let cb = (perc) => {
|
||||||
|
const p = 5 + Math.round(15*perc/100);
|
||||||
|
callback(p);
|
||||||
|
};
|
||||||
|
|
||||||
if (result.dataCompressed) {
|
if (result.dataCompressed) {
|
||||||
try {
|
try {
|
||||||
data = utils.pako.inflate(data, {to: 'string'});
|
//data = utils.pako.inflate(data, {to: 'string'});
|
||||||
|
data = await this.inflateWithProgress(data, cb);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.delBook(meta);
|
this.delBook(meta);
|
||||||
throw e;
|
throw e;
|
||||||
@@ -252,7 +283,7 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
callback(20);
|
callback(20);
|
||||||
|
|
||||||
const cb = (perc) => {
|
cb = (perc) => {
|
||||||
const p = 20 + Math.round(80*perc/100);
|
const p = 20 + Math.round(80*perc/100);
|
||||||
callback(p);
|
callback(p);
|
||||||
};
|
};
|
||||||
@@ -264,27 +295,14 @@ class BookManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _delBook(meta) {
|
async delBook(meta) {
|
||||||
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
await bmMetaStore.removeItem(`bmMeta-${meta.key}`);
|
||||||
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
await bmDataStore.removeItem(`bmData-${meta.key}`);
|
||||||
|
|
||||||
delete this.books[meta.key];
|
delete this.books[meta.key];
|
||||||
delete this.booksCached[meta.key];
|
|
||||||
}
|
|
||||||
|
|
||||||
async delBook(meta) {
|
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
await this._delBook(meta);
|
|
||||||
|
|
||||||
await bmCacheStore.setItem('books', this.booksCached);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseBook(meta, data, callback) {
|
async parseBook(meta, data, callback) {
|
||||||
if (!this.books)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
const parsed = new BookParser(this.settings);
|
const parsed = new BookParser(this.settings);
|
||||||
|
|
||||||
const parsedMeta = await parsed.parse(data, callback);
|
const parsedMeta = await parsed.parse(data, callback);
|
||||||
@@ -308,9 +326,8 @@ class BookManager {
|
|||||||
return utils.stringToHex(url);
|
return utils.stringToHex(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-- recent --------------------------------------------------------------
|
||||||
async setRecentBook(value) {
|
async setRecentBook(value) {
|
||||||
if (!this.recent)
|
|
||||||
await this.init();
|
|
||||||
const result = this.metaOnly(value);
|
const result = this.metaOnly(value);
|
||||||
result.touchTime = Date.now();
|
result.touchTime = Date.now();
|
||||||
result.deleted = 0;
|
result.deleted = 0;
|
||||||
@@ -327,67 +344,59 @@ class BookManager {
|
|||||||
|
|
||||||
await bmRecentStore.setItem(result.key, result);
|
await bmRecentStore.setItem(result.key, result);
|
||||||
|
|
||||||
//кэшируем, аккуратно
|
|
||||||
let saveRecent = false;
|
|
||||||
if (!(this.recentLast && this.recentLast.key == result.key)) {
|
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
saveRecent = true;
|
|
||||||
}
|
|
||||||
this.recentLast = result;
|
this.recentLast = result;
|
||||||
await bmCacheStore.setItem('recent-last', this.recentLast);
|
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||||
|
|
||||||
this.mostRecentCached = result;
|
this.recentChanged = true;
|
||||||
this.recentChanged2 = true;
|
this.emit('recent-changed', result.key);
|
||||||
|
|
||||||
if (saveRecent)
|
|
||||||
this.emit('save-recent');
|
|
||||||
this.emit('recent-changed');
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentBook(value) {
|
async getRecentBook(value) {
|
||||||
if (!this.recent)
|
let result = this.recent[value.key];
|
||||||
await this.init();
|
if (!result) {
|
||||||
return this.recent[value.key];
|
result = await bmRecentStore.getItem(value.key);
|
||||||
|
if (result)
|
||||||
|
this.recent[value.key] = result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value) {
|
||||||
if (!this.recent)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
this.recent[value.key].deleted = 1;
|
this.recent[value.key].deleted = 1;
|
||||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
|
|
||||||
this.mostRecentCached = null;
|
if (this.recentLast.key == value.key) {
|
||||||
this.recentChanged2 = true;
|
this.recentLast = null;
|
||||||
|
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||||
this.emit('save-recent');
|
}
|
||||||
|
this.emit('recent-deleted', value.key);
|
||||||
|
this.emit('recent-changed', value.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
if (!this.recent)
|
|
||||||
await this.init();
|
|
||||||
|
|
||||||
const sorted = this.getSortedRecent();
|
const sorted = this.getSortedRecent();
|
||||||
|
|
||||||
let isDel = false;
|
let isDel = false;
|
||||||
for (let i = 1000; i < sorted.length; i++) {
|
for (let i = 1000; i < sorted.length; i++) {
|
||||||
await bmRecentStore.removeItem(sorted[i].key);
|
await bmRecentStore.removeItem(sorted[i].key);
|
||||||
delete this.recent[sorted[i].key];
|
delete this.recent[sorted[i].key];
|
||||||
|
await bmRecentStore.removeItem(sorted[i].key);
|
||||||
isDel = true;
|
isDel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortedRecentCached = null;
|
this.sortedRecentCached = null;
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
|
|
||||||
|
if (isDel)
|
||||||
|
this.emit('recent-changed');
|
||||||
return isDel;
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
mostRecentBook() {
|
||||||
if (this.mostRecentCached) {
|
if (this.recentLast) {
|
||||||
return this.mostRecentCached;
|
return this.recentLast;
|
||||||
}
|
}
|
||||||
|
const oldRecentLast = this.recentLast;
|
||||||
|
|
||||||
let max = 0;
|
let max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
@@ -398,12 +407,17 @@ class BookManager {
|
|||||||
result = book;
|
result = book;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.mostRecentCached = result;
|
this.recentLast = result;
|
||||||
|
bmRecentStore.setItem('recent-last', this.recentLast);//no await
|
||||||
|
|
||||||
|
if (this.recentLast !== oldRecentLast)
|
||||||
|
this.emit('recent-changed');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortedRecent() {
|
getSortedRecent() {
|
||||||
if (!this.recentChanged2 && this.sortedRecentCached) {
|
if (!this.recentChanged && this.sortedRecentCached) {
|
||||||
return this.sortedRecentCached;
|
return this.sortedRecentCached;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +426,7 @@ class BookManager {
|
|||||||
result.sort((a, b) => b.touchTime - a.touchTime);
|
result.sort((a, b) => b.touchTime - a.touchTime);
|
||||||
|
|
||||||
this.sortedRecentCached = result;
|
this.sortedRecentCached = result;
|
||||||
this.recentChanged2 = false;
|
this.recentChanged = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +434,6 @@ class BookManager {
|
|||||||
const mergedRecent = _.cloneDeep(this.recent);
|
const mergedRecent = _.cloneDeep(this.recent);
|
||||||
|
|
||||||
Object.assign(mergedRecent, value);
|
Object.assign(mergedRecent, value);
|
||||||
const newRecent = {};
|
|
||||||
|
|
||||||
//"ленивое" обновление хранилища
|
//"ленивое" обновление хранилища
|
||||||
(async() => {
|
(async() => {
|
||||||
@@ -432,19 +445,13 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
for (const rec of Object.values(mergedRecent)) {
|
this.recent = mergedRecent;
|
||||||
if (rec.key) {
|
|
||||||
newRecent[rec.key] = rec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.recent = newRecent;
|
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
|
|
||||||
this.recentLast = null;
|
this.recentLast = null;
|
||||||
await bmCacheStore.setItem('recent-last', this.recentLast);
|
await bmRecentStore.setItem('recent-last', this.recentLast);
|
||||||
|
|
||||||
this.mostRecentCached = null;
|
this.recentChanged = true;
|
||||||
|
this.emit('set-recent');
|
||||||
this.emit('recent-changed');
|
this.emit('recent-changed');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,35 +460,9 @@ class BookManager {
|
|||||||
this.recentRev = value;
|
this.recentRev = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRecentLast(value) {
|
async setRecentDeltaRev(value) {
|
||||||
if (!value.key)
|
await bmRecentStore.setItem('recent-delta-rev', value);
|
||||||
value = null;
|
this.recentDeltaRev = value;
|
||||||
|
|
||||||
this.recentLast = value;
|
|
||||||
await bmCacheStore.setItem('recent-last', this.recentLast);
|
|
||||||
if (value && value.key) {
|
|
||||||
//гарантия переключения книги
|
|
||||||
const mostRecent = this.mostRecentBook();
|
|
||||||
if (mostRecent)
|
|
||||||
this.recent[mostRecent.key].touchTime = value.touchTime - 1;
|
|
||||||
|
|
||||||
this.recent[value.key] = value;
|
|
||||||
await bmRecentStore.setItem(value.key, value);
|
|
||||||
await bmCacheStore.setItem('recent', this.recent);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mostRecentCached = null;
|
|
||||||
this.emit('recent-changed');
|
|
||||||
}
|
|
||||||
|
|
||||||
async setRecentLastRev(value) {
|
|
||||||
await bmRecentStore.setItem('recent-last-rev', value);
|
|
||||||
this.recentLastRev = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setRecentLastDiffRev(value) {
|
|
||||||
await bmRecentStore.setItem('recent-last-diff-rev', value);
|
|
||||||
this.recentLastDiffRev = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(listener) {
|
addEventListener(listener) {
|
||||||
@@ -496,9 +477,13 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit(eventName, value) {
|
emit(eventName, value) {
|
||||||
for (const listener of this.eventListeners)
|
if (this.eventListeners) {
|
||||||
|
for (const listener of this.eventListeners) {
|
||||||
|
//console.log(eventName);
|
||||||
listener(eventName, value);
|
listener(eventName, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,42 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
showUntil: '2019-09-19',
|
||||||
|
header: '0.7.1 (2019-09-20)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<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',
|
showUntil: '2019-06-22',
|
||||||
header: '0.6.9 (2019-06-23)',
|
header: '0.6.9 (2019-06-23)',
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||||
|
<div ref="windowBox" class="windowBox" @click.stop>
|
||||||
<div class="window">
|
<div class="window">
|
||||||
<div class="header">
|
<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="header-text"><slot name="header"></slot></span>
|
||||||
<span class="close-button" @click="close"><i class="el-icon-close"></i></span>
|
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -14,17 +19,116 @@ import Vue from 'vue';
|
|||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
|
props: {
|
||||||
|
height: { type: String, default: '100%' },
|
||||||
|
width: { type: String, default: '100%' },
|
||||||
|
maxWidth: { type: String, default: '' },
|
||||||
|
topShift: { type: Number, default: 0 },
|
||||||
|
}
|
||||||
})
|
})
|
||||||
class Window extends Vue {
|
class Window extends Vue {
|
||||||
close() {
|
init() {
|
||||||
this.$emit('close');
|
this.$nextTick(() => {
|
||||||
|
this.$refs.windowBox.style.height = this.height;
|
||||||
|
this.$refs.windowBox.style.width = this.width;
|
||||||
|
if (this.maxWidth)
|
||||||
|
this.$refs.windowBox.style.maxWidth = this.maxWidth;
|
||||||
|
|
||||||
|
const left = (this.$refs.main.offsetWidth - this.$refs.windowBox.offsetWidth)/2;
|
||||||
|
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
|
||||||
|
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMouseDown(event) {
|
||||||
|
if (this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.$refs.header.style.cursor = 'move';
|
||||||
|
this.startX = event.screenX;
|
||||||
|
this.startY = event.screenY;
|
||||||
|
this.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp(event) {
|
||||||
|
if (event.button == 0) {
|
||||||
|
this.$refs.header.style.cursor = 'default';
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(event) {
|
||||||
|
if (this.moving) {
|
||||||
|
const deltaX = event.screenX - this.startX;
|
||||||
|
const deltaY = event.screenY - this.startY;
|
||||||
|
this.startX = event.screenX;
|
||||||
|
this.startY = event.screenY;
|
||||||
|
|
||||||
|
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart(event) {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
this.$refs.header.style.cursor = 'move';
|
||||||
|
this.startX = touch.screenX;
|
||||||
|
this.startY = touch.screenY;
|
||||||
|
this.moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchMove(event) {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
if (event.touches.length == 1 && this.moving) {
|
||||||
|
const touch = event.touches[0];
|
||||||
|
const deltaX = touch.screenX - this.startX;
|
||||||
|
const deltaY = touch.screenY - this.startY;
|
||||||
|
this.startX = touch.screenX;
|
||||||
|
this.startY = touch.screenY;
|
||||||
|
|
||||||
|
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
|
||||||
|
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchEnd() {
|
||||||
|
if (!this.$isMobileDevice)
|
||||||
|
return;
|
||||||
|
this.$refs.header.style.cursor = 'default';
|
||||||
|
this.moving = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.moving)
|
||||||
|
this.$emit('close');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.main {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.windowBox {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.window {
|
.window {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -39,9 +143,9 @@ class Window extends Vue {
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
background-color: #e5e7ea;
|
background-color: #59B04F;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
@@ -54,8 +158,12 @@ class Window extends Vue {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 40px;
|
width: 30px;
|
||||||
height: 40px;
|
height: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
background-color: #69C05F;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html manifest="/app/manifest.appcache">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './components/App.vue';
|
|
||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './element';
|
import './element';
|
||||||
|
|
||||||
|
import App from './components/App.vue';
|
||||||
//Vue.config.productionTip = false;
|
//Vue.config.productionTip = false;
|
||||||
|
Vue.prototype.$isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
|
|||||||
@@ -2,21 +2,25 @@ import Vue from 'vue';
|
|||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import App from './components/App.vue';
|
//немедленная загрузка
|
||||||
|
import CardIndex from './components/CardIndex/CardIndex.vue';
|
||||||
|
//const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
||||||
|
|
||||||
const CardIndex = () => import('./components/CardIndex/CardIndex.vue');
|
|
||||||
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
const Search = () => import('./components/CardIndex/Search/Search.vue');
|
||||||
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
const Card = () => import('./components/CardIndex/Card/Card.vue');
|
||||||
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
const Book = () => import('./components/CardIndex/Book/Book.vue');
|
||||||
const History = () => import('./components/CardIndex/History/History.vue');
|
const History = () => import('./components/CardIndex/History/History.vue');
|
||||||
|
|
||||||
const Reader = () => import('./components/Reader/Reader.vue');
|
//немедленная загрузка
|
||||||
|
//const Reader = () => import('./components/Reader/Reader.vue');
|
||||||
|
import Reader from './components/Reader/Reader.vue';
|
||||||
|
|
||||||
//const Forum = () => import('./components/Forum/Forum.vue');
|
//const Forum = () => import('./components/Forum/Forum.vue');
|
||||||
const Income = () => import('./components/Income/Income.vue');
|
const Income = () => import('./components/Income/Income.vue');
|
||||||
const Sources = () => import('./components/Sources/Sources.vue');
|
const Sources = () => import('./components/Sources/Sources.vue');
|
||||||
const Settings = () => import('./components/Settings/Settings.vue');
|
const Settings = () => import('./components/Settings/Settings.vue');
|
||||||
const Help = () => import('./components/Help/Help.vue');
|
const Help = () => import('./components/Help/Help.vue');
|
||||||
const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
//const NotFound404 = () => import('./components/NotFound404/NotFound404.vue');
|
||||||
|
|
||||||
const myRoutes = [
|
const myRoutes = [
|
||||||
['/', null, null, '/cardindex'],
|
['/', null, null, '/cardindex'],
|
||||||
|
|||||||
@@ -129,6 +129,10 @@ export function getObjDiff(oldObj, newObj) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isObjDiff(diff) {
|
||||||
|
return (_.isObject(diff) && diff.__isDiff);
|
||||||
|
}
|
||||||
|
|
||||||
export function isEmptyObjDiff(diff) {
|
export function isEmptyObjDiff(diff) {
|
||||||
return (!_.isObject(diff) || !diff.__isDiff ||
|
return (!_.isObject(diff) || !diff.__isDiff ||
|
||||||
(!Object.keys(diff.change).length &&
|
(!Object.keys(diff.change).length &&
|
||||||
@@ -166,3 +170,27 @@ export function applyObjDiff(obj, diff, isAddChanged) {
|
|||||||
|
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ const toolButtons = [
|
|||||||
{name: 'search', show: true, text: 'Найти в тексте'},
|
{name: 'search', show: true, text: 'Найти в тексте'},
|
||||||
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
||||||
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
||||||
{name: 'history', show: true, text: 'Открыть недавние'},
|
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const fonts = [
|
const fonts = [
|
||||||
@@ -145,7 +145,7 @@ const settingDefaults = {
|
|||||||
fontName: 'ReaderDefault',
|
fontName: 'ReaderDefault',
|
||||||
webFontName: '',
|
webFontName: '',
|
||||||
fontVertShift: 0,
|
fontVertShift: 0,
|
||||||
textVertShift: -20,
|
textVertShift: 0,
|
||||||
|
|
||||||
lineInterval: 3,// px, межстрочный интервал
|
lineInterval: 3,// px, межстрочный интервал
|
||||||
textAlignJustify: true,// выравнивание по ширине
|
textAlignJustify: true,// выравнивание по ширине
|
||||||
@@ -176,10 +176,12 @@ const settingDefaults = {
|
|||||||
blinkCachedLoad: true,
|
blinkCachedLoad: true,
|
||||||
showImages: true,
|
showImages: true,
|
||||||
showInlineImagesInCenter: true,
|
showInlineImagesInCenter: true,
|
||||||
|
compactTextPerc: 0,
|
||||||
imageHeightLines: 100,
|
imageHeightLines: 100,
|
||||||
imageFitWidth: true,
|
imageFitWidth: true,
|
||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
|
showMigrationDialog: true,
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
@@ -201,6 +203,7 @@ const state = {
|
|||||||
profilesRev: 0,
|
profilesRev: 0,
|
||||||
allowProfilesSave: false,//подстраховка для разработки
|
allowProfilesSave: false,//подстраховка для разработки
|
||||||
whatsNewContentHash: '',
|
whatsNewContentHash: '',
|
||||||
|
migrationRemindDate: '',
|
||||||
currentProfile: '',
|
currentProfile: '',
|
||||||
settings: Object.assign({}, settingDefaults),
|
settings: Object.assign({}, settingDefaults),
|
||||||
settingsRev: {},
|
settingsRev: {},
|
||||||
@@ -235,6 +238,9 @@ const mutations = {
|
|||||||
setWhatsNewContentHash(state, value) {
|
setWhatsNewContentHash(state, value) {
|
||||||
state.whatsNewContentHash = value;
|
state.whatsNewContentHash = value;
|
||||||
},
|
},
|
||||||
|
setMigrationRemindDate(state, value) {
|
||||||
|
state.migrationRemindDate = value;
|
||||||
|
},
|
||||||
setCurrentProfile(state, value) {
|
setCurrentProfile(state, value) {
|
||||||
state.currentProfile = value;
|
state.currentProfile = value;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,34 @@
|
|||||||
|
server {
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/omnireader.ru/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/omnireader.ru/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
server_name omnireader.ru;
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_types *;
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://localhost:44081;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /tmp {
|
||||||
|
root /home/liberama/public;
|
||||||
|
add_header Content-Type text/xml;
|
||||||
|
add_header Content-Encoding gzip;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /home/liberama/public;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name omnireader.ru;
|
server_name omnireader.ru;
|
||||||
|
|||||||
5306
package-lock.json
generated
5306
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.6.9",
|
"version": "0.7.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
@@ -19,69 +19,70 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.22.1",
|
"babel-core": "^6.22.1",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-loader": "^7.1.1",
|
"babel-loader": "^7.1.1",
|
||||||
"babel-plugin-component": "^1.1.1",
|
"babel-plugin-component": "^1.1.1",
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
||||||
"babel-preset-env": "^1.3.2",
|
"babel-preset-env": "^1.3.2",
|
||||||
"clean-webpack-plugin": "^1.0.0",
|
"clean-webpack-plugin": "^1.0.1",
|
||||||
"copy-webpack-plugin": "^4.6.0",
|
"copy-webpack-plugin": "^4.6.0",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"disable-output-webpack-plugin": "^1.0.1",
|
"disable-output-webpack-plugin": "^1.0.1",
|
||||||
"element-theme-chalk": "^2.4.11",
|
"element-theme-chalk": "^2.12.0",
|
||||||
"eslint": "^5.11.1",
|
"eslint": "^5.16.0",
|
||||||
"eslint-plugin-html": "^5.0.0",
|
"eslint-plugin-html": "^5.0.5",
|
||||||
"eslint-plugin-node": "^8.0.0",
|
"eslint-plugin-node": "^8.0.0",
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^5.2.3",
|
||||||
"event-hooks-webpack-plugin": "^2.1.1",
|
"event-hooks-webpack-plugin": "^2.1.4",
|
||||||
"file-loader": "^3.0.1",
|
"file-loader": "^3.0.1",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"mini-css-extract-plugin": "^0.5.0",
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
"null-loader": "^0.1.1",
|
"null-loader": "^0.1.1",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"pkg": "^4.3.7",
|
"pkg": "4.3.7",
|
||||||
"terser-webpack-plugin": "^1.2.1",
|
"terser-webpack-plugin": "^1.4.1",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "^1.1.2",
|
||||||
"vue-class-component": "^6.3.2",
|
"vue-class-component": "^6.3.2",
|
||||||
"vue-loader": "^15.4.2",
|
"vue-loader": "^15.7.1",
|
||||||
"vue-style-loader": "^4.1.2",
|
"vue-style-loader": "^4.1.2",
|
||||||
"vue-template-compiler": "^2.5.21",
|
"vue-template-compiler": "^2.6.10",
|
||||||
"webpack": "^4.28.2",
|
"webpack": "^4.39.3",
|
||||||
"webpack-cli": "^3.1.2",
|
"webpack-cli": "^3.3.7",
|
||||||
"webpack-dev-middleware": "^3.4.0",
|
"webpack-dev-middleware": "^3.7.1",
|
||||||
"webpack-hot-middleware": "^2.24.3",
|
"webpack-hot-middleware": "^2.25.0",
|
||||||
"webpack-merge": "^4.1.5"
|
"webpack-merge": "^4.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"appcache-webpack-plugin": "^1.4.0",
|
||||||
"base-x": "^3.0.5",
|
"axios": "^0.18.1",
|
||||||
|
"base-x": "^3.0.6",
|
||||||
"chardet": "^0.7.0",
|
"chardet": "^0.7.0",
|
||||||
"compression": "^1.7.3",
|
"compression": "^1.7.4",
|
||||||
"decompress-zip": "^0.2.2",
|
"decompress-zip": "^0.2.2",
|
||||||
"element-ui": "^2.4.11",
|
"element-ui": "^2.12.0",
|
||||||
"express": "^4.16.4",
|
"express": "^4.17.1",
|
||||||
"fg-loadcss": "^2.1.0",
|
"fg-loadcss": "^2.1.0",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"got": "^9.5.1",
|
"got": "^9.6.0",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"iconv-lite": "^0.4.24",
|
"iconv-lite": "^0.4.24",
|
||||||
"localforage": "^1.7.3",
|
"localforage": "^1.7.3",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.15",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"multer": "^1.4.1",
|
"multer": "^1.4.2",
|
||||||
"pako": "^1.0.10",
|
"pako": "^1.0.10",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.2.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
"sql-template-strings": "^2.2.2",
|
"sql-template-strings": "^2.2.2",
|
||||||
"sqlite": "^3.0.0",
|
"sqlite": "3.0.0",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"unbzip2-stream": "^1.3.3",
|
"unbzip2-stream": "^1.3.3",
|
||||||
"vue": "^2.5.21",
|
"vue": "^2.6.10",
|
||||||
"vue-router": "^3.0.2",
|
"vue-router": "^3.1.3",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.1.1",
|
||||||
"vuex-persistedstate": "^2.5.4"
|
"vuex-persistedstate": "^2.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const FileDownloader = require('./FileDownloader');
|
|||||||
const FileDecompressor = require('./FileDecompressor');
|
const FileDecompressor = require('./FileDecompressor');
|
||||||
const BookConverter = require('./BookConverter');
|
const BookConverter = require('./BookConverter');
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
const log = require('./getLogger').getLog();
|
||||||
|
|
||||||
let singleCleanExecute = false;
|
let singleCleanExecute = false;
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ class ReaderWorker {
|
|||||||
|
|
||||||
async periodicCleanDir(dir, maxSize, timeout) {
|
async periodicCleanDir(dir, maxSize, timeout) {
|
||||||
try {
|
try {
|
||||||
|
log(`Start clean dir: ${dir}, maxSize=${maxSize}`);
|
||||||
const list = await fs.readdir(dir);
|
const list = await fs.readdir(dir);
|
||||||
|
|
||||||
let size = 0;
|
let size = 0;
|
||||||
@@ -144,16 +146,21 @@ class ReaderWorker {
|
|||||||
files.push({name, stat});
|
files.push({name, stat});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log(`found ${files.length} files in dir ${dir}`);
|
||||||
|
|
||||||
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < files.length && size > maxSize) {
|
while (i < files.length && size > maxSize) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
|
log(`rm ${dir}/${file.name}`);
|
||||||
await fs.remove(`${dir}/${file.name}`);
|
await fs.remove(`${dir}/${file.name}`);
|
||||||
size -= file.stat.size;
|
size -= file.stat.size;
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
log(`removed ${i} files`);
|
||||||
|
} catch(e) {
|
||||||
|
log(LM_ERR, e.message);
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.periodicCleanDir(dir, maxSize, timeout);
|
this.periodicCleanDir(dir, maxSize, timeout);
|
||||||
|
|||||||
Reference in New Issue
Block a user