Merge branch 'develop' into feature/quasar

This commit is contained in:
Book Pauk
2020-01-30 16:36:17 +07:00
41 changed files with 1406 additions and 142 deletions

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api'
@@ -6,9 +7,20 @@ const api = axios.create({
class Misc {
async loadConfig() {
const response = await api.post('/config', {params: [
const query = {params: [
'name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch',
]});
]};
try {
await wsc.open();
return await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const response = await api.post('/config', query);
return response.data;
}
}

View File

@@ -1,6 +1,6 @@
import axios from 'axios';
import * as utils from '../share/utils';
import wsc from './webSocketConnection';
const api = axios.create({
baseURL: '/api/reader'
@@ -11,8 +11,67 @@ const workerApi = axios.create({
});
class Reader {
constructor() {
}
async getWorkerStateFinish(workerId, callback) {
if (!callback) callback = () => {};
let response = {};
try {
await wsc.open();
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
while (1) {// eslint-disable-line no-constant-condition
response = await wsc.message(requestId);
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
}
return response;
} catch (e) {
console.error(e);
}
//если с WebSocket проблема, работаем по http
const refreshPause = 500;
let i = 0;
response = {};
while (1) {// eslint-disable-line no-constant-condition
const prevProgress = response.progress || 0;
const prevState = response.state || 0;
response = await workerApi.post('/get-state', {workerId});
response = response.data;
callback(response);
if (!response.state)
throw new Error('Неверный ответ api');
if (response.state == 'finish' || response.state == 'error') {
break;
}
if (i > 0)
await utils.sleep(refreshPause);
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
}
//проверка воркера
i = (prevProgress != response.progress || prevState != response.state ? 1 : i);
}
return response;
}
async loadBook(opts, callback) {
const refreshPause = 300;
if (!callback) callback = () => {};
let response = await api.post('/load-book', opts);
@@ -22,62 +81,90 @@ class Reader {
throw new Error('Неверный ответ api');
callback({totalSteps: 4});
callback(response.data);
let i = 0;
while (1) {// eslint-disable-line no-constant-condition
callback(response.data);
response = await this.getWorkerStateFinish(workerId, callback);
if (response.data.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
if (response) {
if (response.state == 'finish') {//воркер закончил работу, можно скачивать кешированный на сервере файл
callback({step: 4});
const book = await this.loadCachedBook(response.data.path, callback);
return Object.assign({}, response.data, {data: book.data});
const book = await this.loadCachedBook(response.path, callback, response.size);
return Object.assign({}, response, {data: book.data});
}
if (response.data.state == 'error') {
let errMes = response.data.error;
if (response.state == 'error') {
let errMes = response.error;
if (errMes.indexOf('getaddrinfo') >= 0 ||
errMes.indexOf('ECONNRESET') >= 0 ||
errMes.indexOf('EINVAL') >= 0 ||
errMes.indexOf('404') >= 0)
errMes = `Ресурс не найден по адресу: ${response.data.url}`;
errMes = `Ресурс не найден по адресу: ${response.url}`;
throw new Error(errMes);
}
if (i > 0)
await utils.sleep(refreshPause);
} else {
throw new Error('Пустой ответ сервера');
}
}
i++;
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
throw new Error('Слишком долгое время ожидания');
async checkCachedBook(url) {
let estSize = -1;
try {
const response = await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
} catch (e) {
//восстановим при необходимости файл на сервере из удаленного облака
let response = null
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/restore-cached-file', {path: url});
response = response.data;
}
const workerId = response.workerId;
if (!workerId)
throw new Error('Неверный ответ api');
response = await this.getWorkerStateFinish(workerId);
if (response.state == 'error') {
throw new Error(response.error);
}
if (response.size && estSize < 0) {
estSize = response.size;
}
//проверка воркера
const prevProgress = response.data.progress;
const prevState = response.data.state;
response = await workerApi.post('/get-state', {workerId});
i = (prevProgress != response.data.progress || prevState != response.data.state ? 1 : i);
}
return estSize;
}
async checkUrl(url) {
return await axios.head(url, {headers: {'Cache-Control': 'no-cache'}});
}
async loadCachedBook(url, callback) {
const response = await axios.head(url);
let estSize = 1000000;
if (response.headers['content-length']) {
estSize = response.headers['content-length'];
}
async loadCachedBook(url, callback, estSize = -1) {
if (!callback) callback = () => {};
callback({state: 'loading', progress: 0});
//получение размера файла
if (estSize && estSize < 0) {
estSize = await this.checkCachedBook(url);
}
//получение файла
estSize = (estSize > 0 ? estSize : 1000000);
const options = {
onDownloadProgress: progress => {
onDownloadProgress: (progress) => {
while (progress.loaded > estSize) estSize *= 1.5;
if (callback)
callback({progress: Math.round((progress.loaded*100)/estSize)});
}
}
//загрузка
return await axios.get(url, options);
}
@@ -114,13 +201,22 @@ class Reader {
}
async storage(request) {
let response = await api.post('/storage', request);
let response = null;
try {
await wsc.open();
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
} catch (e) {
console.error(e);
//если с WebSocket проблема, работаем по http
response = await api.post('/storage', request);
response = response.data;
}
const state = response.data.state;
const state = response.state;
if (!state)
throw new Error('Неверный ответ api');
return response.data;
return response;
}
}

View File

@@ -0,0 +1,176 @@
const cleanPeriod = 60*1000;//1 минута
class WebSocketConnection {
//messageLifeTime в минутах (cleanPeriod)
constructor(messageLifeTime = 5) {
this.ws = null;
this.timer = null;
this.listeners = [];
this.messageQueue = [];
this.messageLifeTime = messageLifeTime;
this.requestId = 0;
}
addListener(listener) {
if (this.listeners.indexOf(listener) < 0)
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
}
//рассылаем сообщение и удаляем те обработчики, которые его получили
emit(mes, isError) {
const len = this.listeners.length;
if (len > 0) {
let newListeners = [];
for (const listener of this.listeners) {
let emitted = false;
if (isError) {
if (listener.onError)
listener.onError(mes);
emitted = true;
} else {
if (listener.onMessage) {
if (listener.requestId) {
if (listener.requestId === mes.requestId) {
listener.onMessage(mes);
emitted = true;
}
} else {
listener.onMessage(mes);
emitted = true;
}
} else {
emitted = true;
}
}
if (!emitted)
newListeners.push(listener);
}
this.listeners = newListeners;
}
return this.listeners.length != len;
}
open(url) {
return new Promise((resolve, reject) => {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
resolve(this.ws);
} else {
let protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:'
}
url = url || `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(url);
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
let resolved = false;
this.ws.onopen = (e) => {
resolved = true;
resolve(e);
};
this.ws.onmessage = (e) => {
try {
const mes = JSON.parse(e.data);
this.messageQueue.push({regTime: Date.now(), mes});
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (!this.emit(message.mes)) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} catch (e) {
this.emit(e.message, true);
}
};
this.ws.onerror = (e) => {
this.emit(e.message, true);
if (!resolved)
reject(e);
};
}
});
}
//timeout в минутах (cleanPeriod)
message(requestId, timeout = 2) {
return new Promise((resolve, reject) => {
this.addListener({
requestId,
timeout,
onMessage: (mes) => {
if (mes.error) {
reject(mes.error);
} else {
resolve(mes);
}
},
onError: (e) => {
reject(e);
}
});
});
}
send(req) {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
const requestId = ++this.requestId;
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
return requestId;
} else {
throw new Error('WebSocket connection is not ready');
}
}
close() {
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
this.ws.close();
}
}
periodicClean() {
try {
this.timer = null;
const now = Date.now();
//чистка listeners
let newListeners = [];
for (const listener of this.listeners) {
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
newListeners.push(listener);
} else {
if (listener.onError)
listener.onError('Время ожидания ответа истекло');
}
}
this.listeners = newListeners;
//чистка messageQueue
let newMessageQueue = [];
for (const message of this.messageQueue) {
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
newMessageQueue.push(message);
}
}
this.messageQueue = newMessageQueue;
} finally {
if (this.ws.readyState == WebSocket.OPEN) {
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
}
}
}
}
export default new WebSocketConnection();

View File

@@ -1,30 +1,54 @@
<template>
<div class="page">
<div class="box">
<p class="p">Проект существует исключительно на личном энтузиазме.</p>
<p class="p">Чтобы энтузиазма было побольше, вы можете пожертвовать на развитие проекта любую сумму:</p>
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
<div class="address">
<img class="logo" src="./assets/yandex.png">
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
<div class="para">{{ yandexAddress }}</div>
<div class="para">{{ yandexAddress }}
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Скопировать
</template>
<i class="el-icon-copy-document copy-icon" @click="copyAddress(yandexAddress, 'Яндекс кошелек')"></i>
</el-tooltip>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/bitcoin.png">
<el-button class="button" @click="copyAddress(bitcoinAddress, 'Bitcoin')">Скопировать</el-button><br>
<div class="para">{{ bitcoinAddress }}</div>
<div class="para">{{ bitcoinAddress }}
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Скопировать
</template>
<i class="el-icon-copy-document copy-icon" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')"></i>
</el-tooltip>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/litecoin.png">
<el-button class="button" @click="copyAddress(litecoinAddress, 'Litecoin')">Скопировать</el-button><br>
<div class="para">{{ litecoinAddress }}</div>
<div class="para">{{ litecoinAddress }}
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Скопировать
</template>
<i class="el-icon-copy-document copy-icon" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')"></i>
</el-tooltip>
</div>
</div>
<div class="address">
<img class="logo" src="./assets/monero.png">
<el-button class="button" @click="copyAddress(moneroAddress, 'Monero')">Скопировать</el-button><br>
<div class="para">{{ moneroAddress }}</div>
<div class="para">{{ moneroAddress }}
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Скопировать
</template>
<i class="el-icon-copy-document copy-icon" @click="copyAddress(moneroAddress, 'Monero-адрес')"></i>
</el-tooltip>
</div>
</div>
</div>
</div>
@@ -54,7 +78,7 @@ class DonateHelpPage extends Vue {
async copyAddress(address, prefix) {
const result = await copyTextToClipboard(address);
if (result)
this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
this.$notify.success({message: `${prefix} ${address} успешно скопирован в буфер обмена`});
else
this.$notify.error({message: 'Копирование не удалось'});
}
@@ -106,4 +130,10 @@ h5 {
position: relative;
top: 10px;
}
.copy-icon {
margin-left: 10px;
cursor: pointer;
font-size: 120%;
}
</style>

View File

@@ -112,7 +112,7 @@ class LoaderPage extends Vue {
submitUrl() {
if (this.bookUrl) {
this.$emit('load-book', {url: this.bookUrl});
this.$emit('load-book', {url: this.bookUrl, force: true});
this.bookUrl = '';
}
}

View File

@@ -29,6 +29,7 @@ const ruMessage = {
'start': ' ',
'finish': ' ',
'error': ' ',
'queue': 'очередь',
'download': 'скачивание',
'decompress': 'распаковка',
'convert': 'конвертирование',
@@ -59,11 +60,17 @@ class ProgressPage extends Vue {
hide() {
this.visible = false;
this.text = '';
}
setState(state) {
if (state.state)
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
if (state.state) {
if (state.state == 'queue') {
this.text = (state.place ? 'Номер в очереди: ' + state.place : '');
} else {
this.text = (ruMessage[state.state] ? ruMessage[state.state] : state.state);
}
}
this.step = (state.step ? state.step : this.step);
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
this.progress = state.progress || 0;

View File

@@ -90,6 +90,53 @@
</span>
</el-dialog>
<el-dialog
title="Здравствуйте, уважаемые читатели!"
:visible.sync="donationVisible"
width="90%">
<div style="word-break: normal">
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
Для оплаты годового хостинга читалки, необходимо собрать около 2000 рублей.
В настоящий момент у автора эта сумма есть в наличии. Однако будет справедливо, если каждый
сможет проголосовать рублем за то, чтобы читалка так и оставалась:
<ul>
<li>непрерывно улучшаемой</li>
<li>без рекламы</li>
<li>без регистрации</li>
<li>Open Source</li>
</ul>
Автор также обращается с просьбой о помощи в распространении
<a href="https://omnireader.ru" target="_blank">ссылки</a>
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Скопировать
</template>
<i class="el-icon-copy-document" style="cursor: pointer; font-size: 100%" @click="copyLink('https://omnireader.ru')"></i>
</el-tooltip>
на читалку через тематические форумы, соцсети, мессенджеры и пр.
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
<br><br>
Если соберется бóльшая сумма, то разработка децентрализованной библиотеки для свободного обмена книгами будет по возможности ускорена.
<br><br>
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
<br><br>
<el-row type="flex" justify="center">
<el-button type="success" round @click="openDonate">Помочь проекту</el-button>
</el-row>
</div>
<span slot="footer" class="dialog-footer">
<span class="clickable" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
<br><br>
<el-button @click="donationDialogRemind">Напомнить позже</el-button>
</span>
</el-dialog>
</el-main>
</el-container>
</template>
@@ -200,6 +247,7 @@ class Reader extends Vue {
whatsNewVisible = false;
whatsNewContent = '';
donationVisible = false;
created() {
this.loading = true;
@@ -258,9 +306,10 @@ class Reader extends Vue {
this.checkActivateDonateHelpPage();
this.loading = false;
await this.showWhatsNew();
this.updateRoute();
await this.showWhatsNew();
await this.showDonation();
})();
}
@@ -272,6 +321,7 @@ class Reader extends Vue {
this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showDonationDialog2020 = settings.showDonationDialog2020;
this.showToolButton = settings.showToolButton;
this.enableSitesFilter = settings.enableSitesFilter;
@@ -337,6 +387,41 @@ class Reader extends Vue {
}
}
async showDonation() {
await utils.sleep(3000);
const today = utils.formatDate(new Date(), 'coDate');
if (this.mode == 'omnireader' && today < '2020-03-01' && this.showDonationDialog2020 && this.donationRemindDate != today) {
this.donationVisible = true;
}
}
donationDialogDisable() {
this.donationVisible = false;
if (this.showDonationDialog2020) {
const newSettings = Object.assign({}, this.settings, { showDonationDialog2020: false });
this.commit('reader/setSettings', newSettings);
}
}
donationDialogRemind() {
this.donationVisible = false;
this.commit('reader/setDonationRemindDate', utils.formatDate(new Date(), 'coDate'));
}
openDonate() {
this.donationVisible = false;
this.donateToggle();
}
async copyLink(link) {
const result = await utils.copyTextToClipboard(link);
if (result)
this.$notify.success({message: `Ссылка ${link} успешно скопирована в буфер обмена`});
else
this.$notify.error({message: 'Копирование не удалось'});
}
openVersionHistory() {
this.whatsNewVisible = false;
this.versionHistoryToggle();
@@ -455,6 +540,10 @@ class Reader extends Vue {
return this.$store.state.reader.whatsNewContentHash;
}
get donationRemindDate() {
return this.$store.state.reader.donationRemindDate;
}
addAction(pos) {
let a = this.actionList;
if (!a.length || a[a.length - 1] != pos) {
@@ -719,15 +808,16 @@ class Reader extends Vue {
case 'scrolling':
case 'search':
case 'copyText':
case 'recentBooks':
case 'refresh':
case 'offlineMode':
case 'recentBooks':
case 'settings':
if (this[`${button}Active`])
if (this.progressActive) {
classResult = classDisabled;
} else if (this[`${button}Active`]) {
classResult = classActive;
}
break;
}
switch (button) {
case 'undoAction':
if (this.actionCur <= 0)
classResult = classDisabled;

View File

@@ -272,7 +272,7 @@ class RecentBooksPage extends Vue {
async downloadBook(fb2path) {
try {
await readerApi.checkUrl(fb2path);
await readerApi.checkCachedBook(fb2path);
const d = this.$refs.download;
d.href = fb2path;

View File

@@ -471,6 +471,14 @@
<el-checkbox v-model="showWhatsNewDialog">Показывать уведомление "Что нового"</el-checkbox>
</el-tooltip>
</el-form-item>
<el-form-item label="Уведомление">
<el-tooltip :open-delay="500" effect="light">
<template slot="content">
Показывать уведомление "Оплатим хостинг вместе"
</template>
<el-checkbox v-model="showDonationDialog2020">Показывать "Оплатим хостинг вместе"</el-checkbox>
</el-tooltip>
</el-form-item>
</el-form>
<el-form :model="form" size="mini" label-width="120px" @submit.native.prevent>

View File

@@ -464,7 +464,7 @@ class BookManager {
addEventListener(listener) {
if (this.eventListeners.indexOf(listener) < 0)
this.eventListeners.push(listener);
this.eventListeners.push(listener);
}
removeEventListener(listener) {

View File

@@ -1,4 +1,27 @@
export const versionHistory = [
{
showUntil: '2020-01-27',
header: '0.8.3 (2020-01-28)',
content:
`
<ul>
<li>добавлено всплывающее окно с акцией "Оплатим хостинг вместе"</li>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-19',
header: '0.8.2 (2020-01-20)',
content:
`
<ul>
<li>внутренние оптимизации</li>
</ul>
`
},
{
showUntil: '2020-01-06',
header: '0.8.1 (2020-01-07)',

View File

@@ -19,6 +19,7 @@ import ElCheckbox from 'element-ui/lib/checkbox';
import ElTabs from 'element-ui/lib/tabs';
import ElTabPane from 'element-ui/lib/tab-pane';
import ElTooltip from 'element-ui/lib/tooltip';
import ElRow from 'element-ui/lib/row';
import ElCol from 'element-ui/lib/col';
import ElContainer from 'element-ui/lib/container';
import ElAside from 'element-ui/lib/aside';
@@ -43,7 +44,7 @@ import MessageBox from 'element-ui/lib/message-box';
const components = {
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
ElCol, ElContainer, ElAside, ElMain, ElHeader,
ElRow, ElCol, ElContainer, ElAside, ElMain, ElHeader,
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
ElProgress, ElSlider, ElForm, ElFormItem,
ElColorPicker, ElDialog,

View File

@@ -182,6 +182,7 @@ const settingDefaults = {
imageFitWidth: true,
showServerStorageMessages: true,
showWhatsNewDialog: true,
showDonationDialog2020: true,
enableSitesFilter: true,
fontShifts: {},
@@ -204,6 +205,7 @@ const state = {
profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '',
donationRemindDate: '',
currentProfile: '',
settings: Object.assign({}, settingDefaults),
settingsRev: {},
@@ -238,6 +240,9 @@ const mutations = {
setWhatsNewContentHash(state, value) {
state.whatsNewContentHash = value;
},
setDonationRemindDate(state, value) {
state.donationRemindDate = value;
},
setCurrentProfile(state, value) {
state.currentProfile = value;
},