Compare commits

...

145 Commits

Author SHA1 Message Date
Book Pauk
759ff46c92 Merge branch 'release/0.7.1d' 2019-09-21 00:25:08 +07:00
Book Pauk
41957cdceb Актуализирована справка, доделки 2019-09-21 00:24:27 +07:00
Book Pauk
d418e3a1c9 Merge tag '0.7.1c' into develop
0.7.1c
2019-09-20 23:54:50 +07:00
Book Pauk
f650124428 Merge branch 'release/0.7.1c' 2019-09-20 23:54:38 +07:00
Book Pauk
795d109c76 Поправил описание 2019-09-20 23:54:02 +07:00
Book Pauk
6868b3effc Добавлена кнопка offlineMode 2019-09-20 23:52:45 +07:00
Book Pauk
26747b7013 Небольшие поправки 2019-09-20 22:44:36 +07:00
Book Pauk
5198f8aa60 Merge tag '0.7.1b' into develop
0.7.1b
2019-09-20 22:18:09 +07:00
Book Pauk
552da48a32 Merge branch 'release/0.7.1b' 2019-09-20 22:17:58 +07:00
Book Pauk
db8a688620 Манипуляции с appcache 2019-09-20 22:17:28 +07:00
Book Pauk
3088028d05 Поправки багов 2019-09-20 21:45:29 +07:00
Book Pauk
fd62ef865d Merge tag '0.7.1a' into develop
0.7.1a
2019-09-20 20:37:33 +07:00
Book Pauk
ed74ed00ed Merge branch 'release/0.7.1a' 2019-09-20 20:37:23 +07:00
Book Pauk
741317aaaf К предыдущему 2019-09-20 20:36:31 +07:00
Book Pauk
9b6ecd4e6b Убрал вычисление диффа 2019-09-20 20:35:12 +07:00
Book Pauk
7863b3358e Убрал appcache 2019-09-20 20:34:42 +07:00
Book Pauk
e1be68ec3d Поправка бага 2019-09-20 20:20:11 +07:00
Book Pauk
a054186d4b Merge tag '0.7.1' into develop
Версия 0.7.1
2019-09-20 19:58:36 +07:00
Book Pauk
2d5c549c83 Merge branch 'release/0.7.1' 2019-09-20 19:58:22 +07:00
Book Pauk
9f6072dfe1 Версия 0.7.1 2019-09-20 19:54:59 +07:00
Book Pauk
69c44fe1ab Откатил новые версии pkg и sqlite, новый pkg глючит 2019-09-20 19:53:55 +07:00
Book Pauk
4fa7b2443e Добавил дебаг-лог в periodicCleanDir 2019-09-20 19:35:22 +07:00
Book Pauk
25a69592bb Правка багов 2019-09-20 19:14:14 +07:00
Book Pauk
44e0b26990 Поправка бага 2019-09-20 18:58:36 +07:00
Book Pauk
c4496f8dc8 Улучшение механизма синхронизации 2019-09-20 18:53:21 +07:00
Book Pauk
9e296231d9 Поправки багов 2019-09-20 16:54:03 +07:00
Book Pauk
49b3f05d65 Поправки багов 2019-09-20 16:38:33 +07:00
Book Pauk
f124b9c050 Переделки синхронизации, замена diff на delta 2019-09-20 16:29:19 +07:00
Book Pauk
63a86f7c06 Версия 0.7.1 2019-09-20 14:04:17 +07:00
Book Pauk
fd0f523c64 Улучшение синхронизации 2019-09-19 20:39:01 +07:00
Book Pauk
487e605520 Поправлен баг 2019-09-19 18:19:14 +07:00
Book Pauk
9e169e1f4b Улучшение синхронизации 2019-09-19 17:51:04 +07:00
Book Pauk
9612e7ebcd Merge tag '0.7.0' into develop
0.7.0
2019-09-07 22:15:32 +07:00
Book Pauk
f66162efe7 Merge branch 'release/0.7.0' 2019-09-07 22:15:14 +07:00
Book Pauk
656642697b Версия 0.7.0 2019-09-07 22:13:13 +07:00
Book Pauk
feb70f85f8 Merge branch 'feature/ss_fix' into develop 2019-09-07 22:00:35 +07:00
Book Pauk
ab1981559b Поправка бага 2019-09-07 21:59:00 +07:00
Book Pauk
c8852d9a8e Небольшая доработка 2019-09-07 20:40:48 +07:00
Book Pauk
9ac8dc7fd1 Доработки отображения диалогов на смартфонах 2019-09-07 17:39:00 +07:00
Book Pauk
c9419d99e6 К предыдущему 2019-09-07 16:44:00 +07:00
Book Pauk
a1f4a83e72 Работа над saveRecent 2019-09-07 16:39:29 +07:00
Book Pauk
a8abd5d427 Эталонный работающий вариант ServerStorage без оптимизации, с дебагом 2019-09-07 12:25:14 +07:00
Book Pauk
629d1b0630 Поправка рассылки сообщений 2019-09-07 12:17:08 +07:00
Book Pauk
97c368f63a Поправки уведомления 2019-09-07 11:02:52 +07:00
Book Pauk
3266a444d0 Доработка 2019-09-06 22:36:48 +07:00
Book Pauk
1c246f71f8 Доделки ServerStorage 2019-09-06 22:07:15 +07:00
Book Pauk
96945dfc4a Начало переделки ServerStorage 2019-09-06 18:47:07 +07:00
Book Pauk
30eb3001ef Переход на https 2019-09-06 15:10:40 +07:00
Book Pauk
bdd8636390 Переход на https-версию, небольшой рефакторинг, улучшения 2019-09-06 15:06:58 +07:00
Book Pauk
f762d2a271 Сделан npm update, поправлены ошибки 2019-09-03 22:45:00 +07:00
Book Pauk
cf2efc2b92 Добавлены уведомления о выходе новой httpS версии сайта 2019-09-03 19:59:43 +07:00
Book Pauk
7670da4cba Мелкие поправки 2019-08-30 02:35:56 +07:00
Book Pauk
d87f9f2a21 Добавил конфиг для https с помощью certbot 2019-08-30 02:14:25 +07:00
Book Pauk
6e690f3fea Добавил cache.manifest 2019-08-29 16:30:24 +07:00
Book Pauk
6321002617 Коррекция размеров окна 2019-08-29 15:57:47 +07:00
Book Pauk
15ec362428 Поправлен баг - не распознавались картинки, если в fb2 указан binaryType == 'application/octet-stream' 2019-08-29 15:25:50 +07:00
Book Pauk
454004e705 Поправил дефолт 2019-08-28 18:45:13 +07:00
Book Pauk
e14b414fc1 Поправил баг 2019-08-28 18:41:29 +07:00
Book Pauk
c4b47a5915 Обновил element-ui 2019-08-28 18:34:51 +07:00
Book Pauk
957c252cd7 Отключил пока ServerStorage 2019-08-28 18:33:26 +07:00
Book Pauk
d6a6c21762 К предыдущему 2019-08-28 18:15:12 +07:00
Book Pauk
834580cfdf Поправил описание 2019-08-28 17:36:31 +07:00
Book Pauk
de13cfb555 К предыдущему 2019-08-28 17:30:29 +07:00
Book Pauk
4f87508834 К предыдущему 2019-08-28 17:14:58 +07:00
Book Pauk
682a044f32 Добавлена возможность двигать окна, небольшое облагораживание отображения 2019-08-28 16:48:16 +07:00
Book Pauk
bdb5d90b1d Переименование HistoryPage -> RecentBooksPage 2019-08-28 11:03:09 +07:00
Book Pauk
01880f4456 Добавил описание 0.7.0 2019-08-28 10:48:04 +07:00
Book Pauk
39f78ce7e8 Добавлен параметр compactTextPerc 2019-08-23 19:48:55 +07:00
Book Pauk
755c6b92da Мелкие поправки 2019-08-23 18:55:14 +07:00
Book Pauk
2eab9c2837 Улучшение анимации листания 2019-08-23 15:38:12 +07:00
Book Pauk
63861789de Мелкая поправка 2019-08-23 13:38:23 +07:00
Book Pauk
086c353eff Поправки багов 2019-08-23 13:32:18 +07:00
Book Pauk
4fe5b44655 Мелкие поправки 2019-08-23 12:48:32 +07:00
Book Pauk
036547e260 Мелкая поправка 2019-08-22 23:53:16 +07:00
Book Pauk
696f434c90 Улучшение отображения загрузки списка недавних 2019-08-22 23:37:55 +07:00
Book Pauk
0c654d9346 К предыдущему 2019-08-22 20:01:48 +07:00
Book Pauk
a2c393b06b Рефакторинг, упрощение, начало переделки ServerStorage 2019-08-22 15:37:15 +07:00
Book Pauk
eae2c2b102 Merge tag '0.6.10' into develop
0.6.10
2019-07-21 14:44:57 +07:00
Book Pauk
d9e49e3484 Merge branch 'release/0.6.10' 2019-07-21 14:44:43 +07:00
Book Pauk
a28d4c2f1c Версия 0.6.10 2019-07-21 14:42:54 +07:00
Book Pauk
9af055ec54 Поправки порядка загрузки компонентов и сопутствующих багов 2019-07-21 14:39:06 +07:00
Book Pauk
0d41171e9d Улучшение отзывчивости прогрессбаров 2019-07-21 12:15:33 +07:00
Book Pauk
08af826ae9 Merge tag '0.6.9s' into develop
0.6.9s
2019-06-26 20:04:45 +07:00
Book Pauk
4fd577d7c5 Merge branch 'release/0.6.9s' 2019-06-26 20:04:25 +07:00
Book Pauk
2c8efebe98 Поправлен баг клика в статус баре 2019-06-26 20:03:19 +07:00
Book Pauk
93c9fb53ac Merge tag '0.6.9' into develop
Версия 0.6.9
2019-06-23 18:51:11 +07:00
Book Pauk
5a4d249cf9 Merge branch 'release/0.6.9' 2019-06-23 18:50:57 +07:00
Book Pauk
4cc7bdee37 Версия 0.6.9 2019-06-23 18:50:28 +07:00
Book Pauk
a6af568411 Ускорил сжатие книги при сохранении в BookStore 2019-06-23 18:49:41 +07:00
Book Pauk
576a6a094a Merge tag '0.6.8' into develop
Версия 0.6.8
2019-06-23 17:20:02 +07:00
Book Pauk
e671e4b6f5 Merge branch 'release/0.6.8' 2019-06-23 17:19:48 +07:00
Book Pauk
a66b2a4c70 Версия 0.6.8 2019-06-23 17:19:30 +07:00
Book Pauk
f1ae409535 На страницу загрузки добавлен блок "Поделиться" 2019-06-23 17:18:04 +07:00
Book Pauk
a4b56b477d Исправление автоформирования заголовка при вставке из буфера обмена 2019-06-23 16:29:36 +07:00
Book Pauk
d9c389812a Добавлен новый вариант анимации перелистывания - листание 2019-06-23 15:51:55 +07:00
Book Pauk
074ef3645f Добавлен вариант перелистывания - rotate 2019-06-23 14:13:59 +07:00
Book Pauk
cc3aa413e8 Исправил сообщение о загрузке шрифтов 2019-06-09 18:23:04 +07:00
Book Pauk
7f90c09227 Улучшение прогрессбара загрузки/сохранения книги 2019-06-09 16:44:11 +07:00
Book Pauk
f6f4d8ccc9 Исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8 2019-06-09 15:03:04 +07:00
Book Pauk
31afce8304 Исправление бага - падение сервера при распаковке битых архивов 2019-06-04 17:35:32 +07:00
Book Pauk
2c4ff856cd Merge tag '0.6.7' into develop
Версия 0.6.7
2019-05-30 16:16:19 +07:00
Book Pauk
f59974e310 Merge branch 'release/0.6.7' 2019-05-30 16:16:08 +07:00
Book Pauk
70e2c12a6b Версия 0.6.7 2019-05-30 16:15:46 +07:00
Book Pauk
11f3c6ce6f Мелкие поправки 2019-05-30 16:14:41 +07:00
Book Pauk
e213c4640b Добавлен GET-параметр вида "reader?__pp=50.5&url=..." для указания позиции в книге в процентах 2019-05-30 16:00:47 +07:00
Book Pauk
959c5eaa59 Добавлен GET-параметр вида "reader?__refresh=1&url=..." для принудительного обновления загружаемого текста 2019-05-30 14:54:55 +07:00
Book Pauk
66fa510b26 Добавлена возможность указать название текста 2019-05-28 16:32:54 +07:00
Book Pauk
f26a3b31ac На страницу загрузки добавлена возможность загрузки книги из буфера обмена 2019-05-27 16:25:51 +07:00
Book Pauk
724fbf579e Мелкое форматирование 2019-05-27 15:10:40 +07:00
Book Pauk
f192f8e3cd Мелкий рефакторинг 2019-05-27 15:09:55 +07:00
Book Pauk
f13c3d19fb - добавлена возможность настройки отображаемых кнопок на панели управления
- некоторые кнопки на панели управления были скрыты по-умолчанию
2019-05-26 16:16:20 +07:00
Book Pauk
b51a09efcc В справку добавлена история версий проекта 2019-05-26 14:01:56 +07:00
Book Pauk
6004043782 Мелкие поправки 2019-05-23 13:47:36 +07:00
Book Pauk
f9fd0dc2c3 Поправлен баг 2019-05-23 13:47:12 +07:00
Book Pauk
eb5411cd20 К предыдущему 2019-04-27 19:13:56 +07:00
Book Pauk
da3c7a02f0 Небольшие поправки отображения загрузки шрифта 2019-04-27 18:34:23 +07:00
Book Pauk
e67d05007f Поправлен баг 2019-04-27 17:21:49 +07:00
Book Pauk
b0a9a6a08e Добавлена настройка showWhatsNewDialog 2019-04-27 17:04:34 +07:00
Book Pauk
d848ea35f4 Поправки верстки 2019-04-27 16:58:04 +07:00
Book Pauk
350f20effe Добавлена история версий 2019-04-27 16:40:48 +07:00
Book Pauk
b6dc8f98fe Добавлен диалог whatsNew 2019-04-27 15:40:11 +07:00
Book Pauk
1b762ee48d Merge tag '0.6.6' into develop
0.6.6
2019-03-28 14:48:17 +07:00
Book Pauk
cc3d7f1eac Merge branch 'release/0.6.6' 2019-03-28 14:47:52 +07:00
Book Pauk
4107282fbf Версия 0.6.6 2019-03-28 14:47:28 +07:00
Book Pauk
c29ffc3fcd Поправки багов 2019-03-28 14:45:42 +07:00
Book Pauk
f648bcda13 Доработки, оптимизация сохранения recentLast 2019-03-28 14:05:13 +07:00
Book Pauk
aa0044eed2 package-lock.json 2019-03-28 13:15:29 +07:00
Book Pauk
2312a721ae Поправлен текст помощи для автономной загрузки читалки 2019-03-28 13:14:57 +07:00
Book Pauk
b93fc39b00 Мелкая поправка 2019-03-28 12:44:27 +07:00
Book Pauk
2dc2cd700f Merge tag '0.6.5' into develop
0.6.5
2019-03-25 14:04:51 +07:00
Book Pauk
d69e534f8b Merge branch 'release/0.6.5' 2019-03-25 14:04:43 +07:00
Book Pauk
1de9ddd394 Версия 0.6.5 2019-03-25 14:04:16 +07:00
Book Pauk
77c68d4e11 Небольшие поправки 2019-03-25 14:03:50 +07:00
Book Pauk
2a0d1dcfce Поправка бага 2019-03-25 13:06:48 +07:00
Book Pauk
5a19cca407 Поправка текста 2019-03-25 12:53:50 +07:00
Book Pauk
4e8773ecde Мелкая поправка 2019-03-25 12:51:01 +07:00
Book Pauk
4c7dada809 Merge tag '0.6.4' into develop
0.6.4
2019-03-24 14:33:19 +07:00
Book Pauk
65690b15da Merge branch 'release/0.6.4' 2019-03-24 14:33:09 +07:00
Book Pauk
8ba07812ce Оптимизация 2019-03-24 14:32:08 +07:00
Book Pauk
2dd8f35001 Версия 0.6.4 2019-03-24 14:04:46 +07:00
Book Pauk
2d15aa88d4 Исправления багов 2019-03-24 14:04:21 +07:00
Book Pauk
e4257e50f0 Merge tag '0.6.3' into develop
0.6.3
2019-03-24 12:52:10 +07:00
Book Pauk
33ebc07915 Merge branch 'release/0.6.3' 2019-03-24 12:51:55 +07:00
Book Pauk
bc07299626 Версия 0.6.3 2019-03-24 12:51:27 +07:00
Book Pauk
25e8aeef53 Merge tag '0.6.2' into develop
0.6.2
2019-03-24 12:28:43 +07:00
43 changed files with 5463 additions and 3588 deletions

View File

@@ -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({exclude: ['../index.html']})
] ]
}); });

View File

@@ -5,11 +5,11 @@ import {Buffer} from 'safe-buffer';
import * as utils from '../share/utils'; import * as utils from '../share/utils';
const api = axios.create({ const api = axios.create({
baseURL: '/api/reader' baseURL: '/api/reader'
}); });
const workerApi = axios.create({ const workerApi = axios.create({
baseURL: '/api/worker' baseURL: '/api/worker'
}); });
class Reader { class Reader {

View File

@@ -47,14 +47,12 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import Vue from 'vue';
import Component from 'vue-class-component'; import Component from 'vue-class-component';
import * as utils from '../share/utils';
export default @Component({ export default @Component({
watch: { watch: {
rootRoute: function() {
this.setAppTitle();
this.redirectIfNeeded();
},
mode: function() { mode: function() {
this.setAppTitle();
this.redirectIfNeeded(); this.redirectIfNeeded();
} }
}, },
@@ -113,13 +111,19 @@ class App extends Vue {
this.dispatch('config/loadConfig'); this.dispatch('config/loadConfig');
this.$watch('apiError', function(newError) { this.$watch('apiError', function(newError) {
if (newError) { if (newError) {
let mes = newError.message;
if (newError.response && newError.response.config)
mes = newError.response.config.url + '<br>' + newError.response.statusText;
this.$notify.error({ this.$notify.error({
title: 'Ошибка API', title: 'Ошибка API',
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
message: newError.response.config.url + '<br>' + newError.response.statusText message: mes
}); });
} }
}); });
this.setAppTitle();
this.redirectIfNeeded();
} }
toggleCollapse() { toggleCollapse() {
@@ -198,15 +202,18 @@ class App extends Vue {
} }
redirectIfNeeded() { redirectIfNeeded() {
if ((this.mode == 'reader' || this.mode == 'omnireader') && (this.rootRoute != '/reader')) { if ((this.mode == 'reader' || this.mode == 'omnireader') && (!this.isReaderActive)) {
//старый url //старый url
const search = window.location.search.substr(1); const search = window.location.search.substr(1);
const url = search.split('url=')[1] || ''; const s = search.split('url=');
const url = s[1] || '';
const q = utils.parseQuery(s[0] || '');
if (url) { if (url) {
window.location = `/#/reader?url=${url}`; q.url = decodeURIComponent(url);
} else {
this.$router.replace('/reader');
} }
window.history.replaceState({}, '', '/');
this.$router.replace({ path: '/reader', query: q });
} }
//yandex-метрика для omnireader //yandex-метрика для omnireader

View File

@@ -1,17 +1,13 @@
<template> <template>
<div ref="main" class="main" @click="close"> <Window @close="close">
<div class="mainWindow" @click.stop> <template slot="header">
<Window @close="close"> Скопировать текст
<template slot="header"> </template>
Скопировать текст
</template>
<div ref="text" class="text" tabindex="-1"> <div ref="text" class="text" tabindex="-1">
<div v-html="text"></div> <div v-html="text"></div>
</div>
</Window>
</div> </div>
</div> </Window>
</template> </template>
<script> <script>
@@ -109,23 +105,6 @@ class CopyTextPage extends Vue {
</script> </script>
<style scoped> <style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
height: 100%;
display: flex;
}
.text { .text {
flex: 1; flex: 1;
overflow-wrap: anywhere; overflow-wrap: anywhere;

View File

@@ -3,8 +3,10 @@
<h4>Возможности читалки:</h4> <h4>Возможности читалки:</h4>
<ul> <ul>
<li>загрузка любой страницы интернета</li> <li>загрузка любой страницы интернета</li>
<li>работа в автономном режиме (без связи)</li>
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li> <li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
<li>установка и запоминание текущей позиции и настроек в браузере (в будущем планируется сохранение и на сервер)</li> <li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
<li>кэширование файлов книг на клиенте и на сервере</li> <li>кэширование файлов книг на клиенте и на сервере</li>
<li>открытие книг с локального диска</li> <li>открытие книг с локального диска</li>
<li>плавный скроллинг текста</li> <li>плавный скроллинг текста</li>
@@ -12,16 +14,23 @@
<li>поиск по тексту и копирование фрагмента</li> <li>поиск по тексту и копирование фрагмента</li>
<li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li> <li>запоминание недавних книг, скачивание книги из читалки в формате fb2</li>
<li>управление кликом и с клавиатуры</li> <li>управление кликом и с клавиатуры</li>
<li>подключение к интернету не обязательно для чтения книги после ее загрузки</li>
<li>регистрация не требуется</li> <li>регистрация не требуется</li>
<li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li> <li>поддерживаемые браузеры: Google Chrome, Mozilla Firefox последних версий</li>
</ul> </ul>
<p>В качестве URL можно задавать html-страничку с книгой, либо прямую ссылку <p>В качестве URL книги можно задавать html-страничку с книгой, либо прямую ссылку
на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p> на файл из онлайн-библиотеки (например, скопировав адрес ссылки или кнопки "скачать fb2").</p>
<p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие</p> <p>Поддерживаемые форматы: <b>fb2, fb2.zip, html, txt</b> и другие.</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.
<br>В Chrome для Android можно вызывать такую закладку по имени прямо в адресной строке браузера (имя стоит сделать попроще).
</p>
</div>
<p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p> <p>Связаться с разработчиком: <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a></p>
</div> </div>
</template> </template>
@@ -31,21 +40,25 @@
import Vue from 'vue'; import Vue from 'vue';
import Component from 'vue-class-component'; import Component from 'vue-class-component';
import {copyTextToClipboard} from '../../../../share/utils';
export default @Component({ export default @Component({
}) })
class CommonHelpPage extends Vue { class CommonHelpPage extends Vue {
created() { created() {
this.config = this.$store.state.config;
} }
get automationHtml() { get mode() {
if (this.config.mode == 'omnireader') { return this.$store.state.config.mode;
return `<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код: }
<br><strong>javascript:location.href='http://omnireader.ru/?url='+location.href;</strong>
<br>Тогда, нажав на получившуюся кнопку на любой странице интернета, вы автоматически откроете ее в Omni Reader.</p>`; async copyText(text, mes) {
} else { const result = await copyTextToClipboard(text);
return ''; const msg = (result ? mes : 'Копирование не удалось');
} if (result)
this.$notify.success({message: msg});
else
this.$notify.error({message: msg});
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@@ -63,4 +76,10 @@ class CommonHelpPage extends Vue {
h4 { h4 {
margin: 0; margin: 0;
} }
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style> </style>

View File

@@ -53,11 +53,10 @@ class DonateHelpPage extends Vue {
async copyAddress(address, prefix) { async copyAddress(address, prefix) {
const result = await copyTextToClipboard(address); const result = await copyTextToClipboard(address);
const msg = (result ? `${prefix}-адрес ${address} успешно скопирован в буфер обмена` : 'Копирование не удалось');
if (result) if (result)
this.$notify.success({message: msg}); this.$notify.success({message: `${prefix}-адрес ${address} успешно скопирован в буфер обмена`});
else else
this.$notify.error({message: msg}); this.$notify.error({message: 'Копирование не удалось'});
} }
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

@@ -1,29 +1,27 @@
<template> <template>
<div ref="main" class="main" @click="close"> <Window @close="close">
<div class="mainWindow" @click.stop> <template slot="header">
<Window @close="close"> Справка
<template slot="header"> </template>
Справка
</template>
<el-tabs type="border-card" v-model="selectedTab"> <el-tabs type="border-card" v-model="selectedTab">
<el-tab-pane class="tab" label="Общее"> <el-tab-pane class="tab" label="Общее">
<CommonHelpPage></CommonHelpPage> <CommonHelpPage></CommonHelpPage>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Клавиатура"> <el-tab-pane label="Клавиатура">
<HotkeysHelpPage></HotkeysHelpPage> <HotkeysHelpPage></HotkeysHelpPage>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Мышь/тачпад"> <el-tab-pane label="Мышь/тачпад">
<MouseHelpPage></MouseHelpPage> <MouseHelpPage></MouseHelpPage>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Помочь проекту" name="donate"> <el-tab-pane label="История версий" name="releases">
<DonateHelpPage></DonateHelpPage> <VersionHistoryPage></VersionHistoryPage>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Помочь проекту" name="donate">
</el-tabs> <DonateHelpPage></DonateHelpPage>
</Window> </el-tab-pane>
</div> </el-tabs>
</div> </Window>
</template> </template>
<script> <script>
@@ -36,6 +34,7 @@ import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue'; import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue'; import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue'; import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
export default @Component({ export default @Component({
components: { components: {
@@ -44,6 +43,7 @@ export default @Component({
HotkeysHelpPage, HotkeysHelpPage,
MouseHelpPage, MouseHelpPage,
DonateHelpPage, DonateHelpPage,
VersionHistoryPage,
}, },
}) })
class HelpPage extends Vue { class HelpPage extends Vue {
@@ -57,6 +57,10 @@ class HelpPage extends Vue {
this.selectedTab = 'donate'; this.selectedTab = 'donate';
} }
activateVersionHistoryHelpPage() {
this.selectedTab = 'releases';
}
keyHook(event) { keyHook(event) {
if (event.type == 'keydown' && (event.code == 'Escape')) { if (event.type == 'keydown' && (event.code == 'Escape')) {
this.close(); this.close();
@@ -68,23 +72,6 @@ class HelpPage extends Vue {
</script> </script>
<style scoped> <style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
height: 100%;
display: flex;
}
.el-tabs { .el-tabs {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -20,6 +20,7 @@
<li><b>Ctrl+C</b> - скопировать текст со страницы</li> <li><b>Ctrl+C</b> - скопировать текст со страницы</li>
<li><b>R</b> - принудительно обновить книгу в обход кэша</li> <li><b>R</b> - принудительно обновить книгу в обход кэша</li>
<li><b>X</b> - открыть недавние</li> <li><b>X</b> - открыть недавние</li>
<li><b>O</b> - автономный режим</li>
<li><b>S</b> - открыть окно настроек</li> <li><b>S</b> - открыть окно настроек</li>
</ul> </ul>
</div> </div>

View File

@@ -0,0 +1,81 @@
<template>
<div id="versionHistoryPage" class="page">
<span class="clickable" v-for="(item, index) in versionHeader" :key="index" @click="showRelease(item)">
<p>
{{ item }}
</p>
</span>
<br>
<h4>История версий:</h4>
<br>
<div v-for="item in versionContent" :id="item.key" :key="item.key">
<span v-html="item.content"></span>
<br>
</div>
</div>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import {versionHistory} from '../../versionHistory';
export default @Component({
})
class VersionHistoryPage extends Vue {
versionHeader = [];
versionContent = [];
created() {
}
mounted() {
let vh = [];
for (const version of versionHistory) {
vh.push(version.header);
}
this.versionHeader = vh;
let vc = [];
for (const version of versionHistory) {
vc.push({key: version.header, content: 'Версия ' + version.header + version.content});
}
this.versionContent = vc;
}
showRelease(id) {
let el = document.getElementById(id);
if (el) {
document.getElementById('versionHistoryPage').scrollTop = el.offsetTop;
}
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.page {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 120%;
line-height: 130%;
}
h4 {
margin: 0;
}
p {
line-height: 15px;
}
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style>

View File

@@ -1,287 +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) {
return (url.indexOf('file://') != 0);
}
close() {
this.$emit('history-toggle');
}
keyHook(event) {
if (event.type == 'keydown' && event.code == 'Escape') {
this.close();
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
}
.mainWindow {
height: 100%;
display: flex;
}
.desc {
cursor: pointer;
}
</style>

View File

@@ -18,7 +18,19 @@
Загрузить файл с диска Загрузить файл с диска
</el-button> </el-button>
<div class="space"></div> <div class="space"></div>
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Комментарии</span> <el-button size="mini" @click="loadBufferClick">
Из буфера обмена
</el-button>
<div class="space"></div>
<div class="space"></div>
<div v-if="mode == 'omnireader'" ref="yaShare2" class="ya-share2"
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
data-description="Чтение fb2-книг онлайн. Загрузка любой страницы интернета одним кликом, синхронизация между устройствами, удобное управление, регистрация не требуется."
data-title="Omni Reader - браузерная онлайн-читалка"
data-url="https://omnireader.ru">
</div>
<div class="space"></div>
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
</div> </div>
<div class="part bottom"> <div class="part bottom">
@@ -26,6 +38,8 @@
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span> <span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
<span class="bottom-span">{{ version }}</span> <span class="bottom-span">{{ version }}</span>
</div> </div>
<PasteTextPage v-if="pasteTextActive" ref="pasteTextPage" @paste-text-toggle="pasteTextToggle" @load-buffer="loadBuffer"></PasteTextPage>
</div> </div>
</template> </template>
@@ -33,12 +47,17 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
import Vue from 'vue'; import Vue from 'vue';
import Component from 'vue-class-component'; import Component from 'vue-class-component';
import PasteTextPage from './PasteTextPage/PasteTextPage.vue';
export default @Component({ export default @Component({
components: {
PasteTextPage,
},
}) })
class LoaderPage extends Vue { class LoaderPage extends Vue {
bookUrl = null; bookUrl = null;
loadPercent = 0; loadPercent = 0;
pasteTextActive = false;
created() { created() {
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -46,6 +65,8 @@ class LoaderPage extends Vue {
mounted() { mounted() {
this.progress = this.$refs.progress; this.progress = this.$refs.progress;
if (this.mode == 'omnireader')
Ya.share2(this.$refs.yaShare2);// eslint-disable-line no-undef
} }
activated() { activated() {
@@ -53,7 +74,7 @@ class LoaderPage extends Vue {
} }
get title() { get title() {
if (this.$store.state.config.mode == 'omnireader') if (this.mode == 'omnireader')
return 'Omni Reader - браузерная онлайн-читалка.'; return 'Omni Reader - браузерная онлайн-читалка.';
return 'Универсальная читалка книг и ресурсов интернета.'; return 'Универсальная читалка книг и ресурсов интернета.';
@@ -83,12 +104,27 @@ class LoaderPage extends Vue {
} }
loadFile() { loadFile() {
const file = this.$refs.file.files[0]; const file = this.$refs.file.files[0];
this.$refs.file.value = ''; this.$refs.file.value = '';
if (file) if (file)
this.$emit('load-file', {file}); this.$emit('load-file', {file});
} }
loadBufferClick() {
this.pasteTextToggle();
}
loadBuffer(opts) {
if (opts.buffer.length) {
const file = new File([opts.buffer], 'dummyName-PasteFromClipboard');
this.$emit('load-file', {file});
}
}
pasteTextToggle() {
this.pasteTextActive = !this.pasteTextActive;
}
openHelp() { openHelp() {
this.$emit('help-toggle'); this.$emit('help-toggle');
} }
@@ -102,6 +138,10 @@ class LoaderPage extends Vue {
} }
keyHook(event) { keyHook(event) {
if (this.pasteTextActive) {
return this.$refs.pasteTextPage.keyHook(event);
}
//недостатки сторонних ui //недостатки сторонних ui
const input = this.$refs.input.$refs.input; const input = this.$refs.input.$refs.input;
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') { if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
@@ -130,7 +170,7 @@ class LoaderPage extends Vue {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 340px; min-height: 400px;
} }
.part { .part {

View File

@@ -0,0 +1,123 @@
<template>
<Window @close="close">
<template slot="header">
<span style="position: relative; top: -3px">
Вставьте текст и нажмите
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
или F2
</span>
</template>
<div>
<el-input placeholder="Введите название текста" class="input" v-model="bookTitle"></el-input>
</div>
<hr/>
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
</Window>
</template>
<script>
//-----------------------------------------------------------------------------
import Vue from 'vue';
import Component from 'vue-class-component';
import Window from '../../../share/Window.vue';
import _ from 'lodash';
import * as utils from '../../../../share/utils';
export default @Component({
components: {
Window,
},
})
class PasteTextPage extends Vue {
bookTitle = '';
created() {
}
mounted() {
this.$refs.textArea.focus();
}
getNonEmptyLine3words(text, count) {
let result = '';
const lines = text.split("\n");
let i = 0;
while (i < lines.length) {
if (lines[i].trim() != '') {
count--;
if (count <= 0) {
result = lines[i];
break;
}
}
i++;
}
result = result.trim().split(' ');
return result.slice(0, 3).join(' ');
}
calcTitle(event) {
if (this.bookTitle == '') {
let text = event.clipboardData.getData('text');
this.bookTitle = `Из буфера обмена ${utils.formatDate(new Date(), 'noDate')}: ` + _.compact([
this.getNonEmptyLine3words(text, 1),
this.getNonEmptyLine3words(text, 2)
]).join(' - ');
}
}
loadBuffer() {
this.$emit('load-buffer', {buffer: `<cut-title>${this.bookTitle}</cut-title>${this.$refs.textArea.value}`});
this.close();
}
close() {
this.$emit('paste-text-toggle');
}
keyHook(event) {
if (event.type == 'keydown') {
switch (event.code) {
case 'F2':
this.loadBuffer();
break;
case 'Escape':
this.close();
break;
}
}
return true;
}
}
//-----------------------------------------------------------------------------
</script>
<style scoped>
.text {
flex: 1;
overflow-wrap: anywhere;
overflow-y: auto;
padding: 0 10px 0 10px;
position: relative;
font-size: 120%;
min-width: 400px;
}
.text:focus {
outline: none;
}
hr {
margin: 0;
padding: 0;
}
.clickable {
color: blue;
cursor: pointer;
}
</style>

View File

@@ -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>

View File

@@ -1,42 +1,45 @@
<template> <template>
<el-container> <el-container>
<el-header v-show="toolBarActive" height='50px'> <el-header v-show="toolBarActive" height='50px'>
<div class="header"> <div ref="header" class="header">
<el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light"> <el-tooltip content="Загрузить книгу" :open-delay="1000" effect="light">
<el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button> <el-button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')"><i class="el-icon-back"></i></el-button>
</el-tooltip> </el-tooltip>
<div> <div>
<el-tooltip content="Действие назад" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['undoAction']" content="Действие назад" :open-delay="1000" effect="light">
<el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button> <el-button ref="undoAction" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" ><i class="el-icon-arrow-left"></i></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="Действие вперед" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['redoAction']" content="Действие вперед" :open-delay="1000" effect="light">
<el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button> <el-button ref="redoAction" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" ><i class="el-icon-arrow-right"></i></el-button>
</el-tooltip> </el-tooltip>
<div class="space"></div> <div class="space"></div>
<el-tooltip content="На весь экран" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['fullScreen']" content="На весь экран" :open-delay="1000" effect="light">
<el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button> <el-button ref="fullScreen" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')"><i class="el-icon-rank"></i></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="Плавный скроллинг" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['scrolling']" content="Плавный скроллинг" :open-delay="1000" effect="light">
<el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button> <el-button ref="scrolling" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')"><i class="el-icon-sort"></i></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="Перелистнуть" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['setPosition']" content="На страницу" :open-delay="1000" effect="light">
<el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button> <el-button ref="setPosition" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')"><i class="el-icon-d-arrow-right"></i></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="Найти в тексте" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['search']" content="Найти в тексте" :open-delay="1000" effect="light">
<el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button> <el-button ref="search" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')"><i class="el-icon-search"></i></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="Скопировать текст со страницы" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['copyText']" content="Скопировать текст со страницы" :open-delay="1000" effect="light">
<el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button> <el-button ref="copyText" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')"><i class="el-icon-edit-outline"></i></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['refresh']" content="Принудительно обновить книгу в обход кэша" :open-delay="1000" effect="light">
<el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')"> <el-button ref="refresh" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')">
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i> <i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<div class="space"></div> <div class="space"></div>
<el-tooltip content="Открыть недавние" :open-delay="1000" effect="light"> <el-tooltip v-show="showToolButton['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 v-show="showToolButton['offlineMode']" content="Автономный режим (без интернета)" :open-delay="1000" effect="light">
<el-button ref="offlineMode" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')"><i class="el-icon-connection"></i></el-button>
</el-tooltip> </el-tooltip>
</div> </div>
@@ -68,13 +71,108 @@
@start-text-search="startTextSearch" @start-text-search="startTextSearch"
@stop-text-search="stopTextSearch"> @stop-text-search="stopTextSearch">
</SearchPage> </SearchPage>
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage> <CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
<HistoryPage v-show="historyActive" ref="historyPage" @load-book="loadBook" @history-toggle="historyToggle"></HistoryPage> <RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage> <SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage> <HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage> <ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage> <ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
<el-dialog
title="Что нового:"
:visible.sync="whatsNewVisible"
width="80%">
<div style="line-height: 20px" v-html="whatsNewContent"></div>
<span class="clickable" @click="openVersionHistory">Посмотреть историю версий</span>
<span slot="footer" class="dialog-footer">
<el-button @click="whatsNewDisable">Больше не показывать</el-button>
</span>
</el-dialog>
<el-dialog
title="Внимание!"
:visible.sync="migrationVisible1"
width="90%">
<div>
Появилась httpS-версия сайта по адресу <a href="https://omnireader.ru" target="_blank">https://omnireader.ru</a><br>
Работа по httpS-протоколу, помимо безопасности соединения, позволяет воспользоваться всеми возможностями
современных браузеров, а именно, применительно к нашему ресурсу:
<ul>
<li>возможность автономной работы с читалкой (без доступа к интернету), кеширование сайта через appcache</li>
<li>безопасная передача на сервер данных о настройках и читаемых книгах при включенной синхронизации; все данные шифруются на стороне
браузера ключом доступа и никто (в т.ч. администратор) не имеет возможности их прочитать
<li>использование встроенных в JS функций шифрования и других</li>
</ul>
Для того, чтобы перейти на новую версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
<ul>
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
после этого все данные будут автоматически сохранены на сервер
</span>
</li>
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
произойдет переход на https-версию читалки и откроется окно для ввода ключа
</span><br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
</span>
</li>
</ul>
Старая http-версия сайта будет доступна до конца 2019 года.<br>
Приносим извинения за доставленные неудобства.
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
</span>
</el-dialog>
<el-dialog
title="Внимание!"
:visible.sync="migrationVisible2"
width="90%">
<div>
Информация для пользователей старой версии читалки по адресу <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a><br>
Для того, чтобы перейти на новую httpS-версию с сохранением настроек и читаемых книг необходимо синхронизировать обе читалки:
<ul>
<li>перейти на старую версию ресурса <a href="http://omnireader.ru" target="_blank">http://omnireader.ru</a></li>
<li>зайти в "Настройки"->"Профили" и поставить галочку "Включить синхронизацию с сервером"</li>
<li>там же добавить профиль устройства с любым именем для синхронизации настроек<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
после этого все данные будут автоматически сохранены на сервер
</span>
</li>
<li>далее нажать на кнопку "Показать ключ доступа" и кликнуть по ссылке "Ссылка для ввода ключа"<br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
произойдет переход на https-версию читалки и откроется окно для ввода ключа
</span><br>
<span style="margin-left: 20px"><i style="font-size: 90%" class="el-icon-info"></i>
подтвердив ввод ключа нажатием "OK", включив синхронизацию с сервером и выбрав профиль устройства, вы восстановите все ваши настройки в новой версии
</span>
</li>
</ul>
Старая http-версия сайта будет доступна до конца 2019 года.<br>
Приносим извинения за доставленные неудобства.
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="migrationDialogDisable">Больше не показывать</el-button>
<el-button @click="migrationDialogRemind">Напомнить позже</el-button>
</span>
</el-dialog>
</el-main> </el-main>
</el-container> </el-container>
</template> </template>
@@ -92,7 +190,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';
@@ -101,6 +199,7 @@ import ServerStorage from './ServerStorage/ServerStorage.vue';
import bookManager from './share/bookManager'; import bookManager from './share/bookManager';
import readerApi from '../../api/reader'; import readerApi from '../../api/reader';
import * as utils from '../../share/utils'; import * as utils from '../../share/utils';
import {versionHistory} from './versionHistory';
export default @Component({ export default @Component({
components: { components: {
@@ -111,7 +210,7 @@ export default @Component({
SetPositionPage, SetPositionPage,
SearchPage, SearchPage,
CopyTextPage, CopyTextPage,
HistoryPage, RecentBooksPage,
SettingsPage, SettingsPage,
HelpPage, HelpPage,
ClickMapPage, ClickMapPage,
@@ -142,10 +241,12 @@ export default @Component({
this.updateRoute(); this.updateRoute();
}, },
loaderActive: function(newValue) { loaderActive: function(newValue) {
const recent = this.mostRecentBook(); (async() => {
if (!newValue && !this.loading && recent && !bookManager.hasBookParsed(recent)) { const recent = this.mostRecentBook();
this.loadBook(recent); if (!newValue && !this.loading && recent && !await bookManager.hasBookParsed(recent)) {
} this.loadBook(recent);
}
})();
}, },
}, },
}) })
@@ -158,7 +259,8 @@ class Reader extends Vue {
setPositionActive = false; setPositionActive = false;
searchActive = false; searchActive = false;
copyTextActive = false; copyTextActive = false;
historyActive = false; recentBooksActive = false;
offlineModeActive = false;
settingsActive = false; settingsActive = false;
helpActive = false; helpActive = false;
clickMapActive = false; clickMapActive = false;
@@ -167,11 +269,17 @@ class Reader extends Vue {
allowUrlParamBookPos = false; allowUrlParamBookPos = false;
showRefreshIcon = true; showRefreshIcon = true;
mostRecentBookReactive = null; mostRecentBookReactive = null;
showToolButton = {};
actionList = []; actionList = [];
actionCur = -1; actionCur = -1;
hidden = false; hidden = false;
whatsNewVisible = false;
whatsNewContent = '';
migrationVisible1 = false;
migrationVisible2 = false;
created() { created() {
this.loading = true; this.loading = true;
this.commit = this.$store.commit; this.commit = this.$store.commit;
@@ -197,40 +305,44 @@ 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);
if (this.$root.rootRoute == '/reader') { if (this.$root.rootRoute == '/reader') {
if (this.routeParamUrl) { if (this.routeParamUrl) {
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos}); await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
} else { } else {
this.loaderActive = true; this.loaderActive = true;
} }
} }
this.checkSetStorageAccessKey(); this.checkSetStorageAccessKey();
this.checkActivateDonateHelpPage();
this.loading = false; this.loading = false;
await this.$refs.serverStorage.init();
await this.showWhatsNew();
await this.showMigration();
})(); })();
} }
@@ -241,6 +353,17 @@ class Reader extends Vue {
this.showClickMapPage = settings.showClickMapPage; this.showClickMapPage = settings.showClickMapPage;
this.clickControl = settings.clickControl; this.clickControl = settings.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad; this.blinkCachedLoad = settings.blinkCachedLoad;
this.showWhatsNewDialog = settings.showWhatsNewDialog;
this.showMigrationDialog = settings.showMigrationDialog;
this.showToolButton = settings.showToolButton;
this.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() {
@@ -257,6 +380,83 @@ class Reader extends Vue {
} }
} }
checkActivateDonateHelpPage() {
const q = this.$route.query;
if (q['donate']) {
this.$router.replace(`/reader`);
this.helpToggle();
this.$nextTick(() => {
this.$refs.helpPage.activateDonateHelpPage();
});
}
}
checkBookPosPercent() {
const q = this.$route.query;
if (q['__pp']) {
let pp = q['__pp'];
if (pp) {
pp = parseFloat(pp) || 0;
const recent = this.mostRecentBook();
(async() => {
await utils.sleep(100);
this.bookPos = Math.floor(recent.textLength*pp/100);
})();
}
}
}
async showWhatsNew() {
await utils.sleep(2000);
const whatsNew = versionHistory[0];
if (this.showWhatsNewDialog &&
whatsNew.showUntil >= utils.formatDate(new Date(), 'coDate') &&
whatsNew.header != this.whatsNewContentHash) {
this.whatsNewContent = 'Версия ' + whatsNew.header + whatsNew.content;
this.whatsNewVisible = true;
}
}
async showMigration() {
await utils.sleep(3000);
if (!this.settingsActive &&
this.mode == 'omnireader' && this.showMigrationDialog && this.migrationRemindDate != utils.formatDate(new Date(), 'coDate')) {
if (window.location.protocol == 'http:') {
this.migrationVisible1 = true;
} else if (window.location.protocol == 'https:') {
this.migrationVisible2 = true;
}
}
}
migrationDialogDisable() {
this.migrationVisible1 = false;
this.migrationVisible2 = false;
if (this.showMigrationDialog) {
const newSettings = Object.assign({}, this.settings, { showMigrationDialog: false });
this.commit('reader/setSettings', newSettings);
}
}
migrationDialogRemind() {
this.migrationVisible1 = false;
this.migrationVisible2 = false;
this.commit('reader/setMigrationRemindDate', utils.formatDate(new Date(), 'coDate'));
}
openVersionHistory() {
this.whatsNewVisible = false;
this.versionHistoryToggle();
}
whatsNewDisable() {
this.whatsNewVisible = false;
const whatsNew = versionHistory[0];
this.commit('reader/setWhatsNewContentHash', whatsNew.header);
}
get routeParamPos() { get routeParamPos() {
let result = undefined; let result = undefined;
const q = this.$route.query; const q = this.$route.query;
@@ -276,12 +476,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;
@@ -293,6 +497,11 @@ class Reader extends Vue {
return decodeURIComponent(result); return decodeURIComponent(result);
} }
get routeParamRefresh() {
const q = this.$route.query;
return !!q['__refresh'];
}
bookPosChanged(event) { bookPosChanged(event) {
if (event.bookPosSeen !== undefined) if (event.bookPosSeen !== undefined)
this.bookPosSeen = event.bookPosSeen; this.bookPosSeen = event.bookPosSeen;
@@ -301,22 +510,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 (eventName == 'set-recent' || eventName == 'recent-deleted') {
if (this.historyActive) {
this.$refs.historyPage.updateTableData();
}
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;
@@ -330,12 +532,6 @@ class Reader extends Vue {
this.bookPosChanged({bookPos: newBook.bookPos}); this.bookPosChanged({bookPos: newBook.bookPos});
} }
} }
if (eventName == 'recent-changed') {
this.debouncedSaveRecentLast();
} else {
this.debouncedSaveRecent();
}
} }
} }
@@ -353,6 +549,14 @@ class Reader extends Vue {
return this.$store.state.reader.settings; return this.$store.state.reader.settings;
} }
get whatsNewContentHash() {
return this.$store.state.reader.whatsNewContentHash;
}
get migrationRemindDate() {
return this.$store.state.reader.migrationRemindDate;
}
addAction(pos) { addAction(pos) {
let a = this.actionList; let a = this.actionList;
if (!a.length || a[a.length - 1] != pos) { if (!a.length || a[a.length - 1] != pos) {
@@ -393,7 +597,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();
@@ -485,22 +689,31 @@ class Reader extends Vue {
} }
} }
historyToggle() { recentBooksToggle() {
this.historyActive = !this.historyActive; this.recentBooksActive = !this.recentBooksActive;
if (this.historyActive) { if (this.recentBooksActive) {
this.closeAllTextPages(); this.closeAllTextPages();
this.$refs.historyPage.init(); this.$refs.recentBooksPage.init();
this.historyActive = true; this.recentBooksActive = true;
} else { } else {
this.historyActive = false; this.recentBooksActive = false;
} }
} }
offlineModeToggle() {
this.offlineModeActive = !this.offlineModeActive;
this.$refs.serverStorage.offlineModeActive = this.offlineModeActive;
}
settingsToggle() { settingsToggle() {
this.settingsActive = !this.settingsActive; this.settingsActive = !this.settingsActive;
if (this.settingsActive) { if (this.settingsActive) {
this.closeAllTextPages(); this.closeAllTextPages();
this.settingsActive = true; this.settingsActive = true;
this.$nextTick(() => {
this.$refs.settingsPage.init();
});
} else { } else {
this.settingsActive = false; this.settingsActive = false;
} }
@@ -523,6 +736,15 @@ class Reader extends Vue {
} }
} }
versionHistoryToggle() {
this.helpToggle();
if (this.helpActive) {
this.$nextTick(() => {
this.$refs.helpPage.activateVersionHistoryHelpPage();
});
}
}
refreshBook() { refreshBook() {
if (this.mostRecentBook()) { if (this.mostRecentBook()) {
this.loadBook({url: this.mostRecentBook().url, force: true}); this.loadBook({url: this.mostRecentBook().url, force: true});
@@ -568,12 +790,15 @@ class Reader extends Vue {
case 'copyText': case 'copyText':
this.copyTextToggle(); this.copyTextToggle();
break; break;
case 'history':
this.historyToggle();
break;
case 'refresh': case 'refresh':
this.refreshBook(); this.refreshBook();
break; break;
case 'recentBooks':
this.recentBooksToggle();
break;
case 'offlineMode':
this.offlineModeToggle();
break;
case 'settings': case 'settings':
this.settingsToggle(); this.settingsToggle();
break; break;
@@ -592,7 +817,8 @@ class Reader extends Vue {
case 'scrolling': case 'scrolling':
case 'search': case 'search':
case 'copyText': case 'copyText':
case 'history': case 'recentBooks':
case 'offlineMode':
case 'settings': case 'settings':
if (this[`${button}Active`]) if (this[`${button}Active`])
classResult = classActive; classResult = classActive;
@@ -610,7 +836,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':
@@ -620,9 +846,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;
} }
@@ -667,7 +893,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;
@@ -701,14 +928,14 @@ 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;
} }
this.progressActive = true; this.progressActive = true;
await this.$nextTick() await this.$nextTick();
const progress = this.$refs.page; const progress = this.$refs.page;
@@ -743,6 +970,7 @@ class Reader extends Vue {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage(); this.blinkCachedLoadMessage();
this.checkBookPosPercent();
await this.activateClickMapPage(); await this.activateClickMapPage();
return; return;
} }
@@ -762,7 +990,6 @@ class Reader extends Vue {
} }
progress.setState({totalSteps: 5}); progress.setState({totalSteps: 5});
// не удалось, скачиваем книгу полностью с конвертацией // не удалось, скачиваем книгу полностью с конвертацией
let loadCached = true; let loadCached = true;
if (!book) { if (!book) {
@@ -791,6 +1018,7 @@ class Reader extends Vue {
} else } else
this.stopBlink = true; this.stopBlink = true;
this.checkBookPosPercent();
await this.activateClickMapPage(); await this.activateClickMapPage();
} catch (e) { } catch (e) {
progress.hide(); this.progressActive = false; progress.hide(); this.progressActive = false;
@@ -860,8 +1088,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);
@@ -911,10 +1139,13 @@ class Reader extends Vue {
this.refreshBook(); this.refreshBook();
break; break;
case 'KeyX': case 'KeyX':
this.historyToggle(); this.recentBooksToggle();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
break; break;
case 'KeyO':
this.offlineModeToggle();
break;
case 'KeyS': case 'KeyS':
this.settingsToggle(); this.settingsToggle();
break; break;
@@ -942,11 +1173,10 @@ class Reader extends Vue {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
} }
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
min-width: 550px;
} }
.el-main { .el-main {
@@ -970,6 +1200,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;
} }
@@ -1010,4 +1244,10 @@ i {
.clear { .clear {
color: rgba(0,0,0,0); color: rgba(0,0,0,0);
} }
</style>
.clickable {
color: blue;
text-decoration: underline;
cursor: pointer;
}
</style>

View 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>

View File

@@ -1,28 +1,24 @@
<template> <template>
<div ref="main" class="main" @click="close"> <Window ref="window" height="125px" max-width="600px" :top-shift="-50" @close="close">
<div class="mainWindow" @click.stop> <template slot="header">
<Window @close="close"> {{ header }}
<template slot="header"> </template>
{{ header }}
</template>
<div class="content"> <div class="content">
<span v-show="initStep">{{ initPercentage }}%</span> <span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input"> <div v-show="!initStep" class="input">
<input ref="input" class="el-input__inner" <input ref="input" class="el-input__inner"
placeholder="что ищем" placeholder="что ищем"
:value="needle" @input="needle = $event.target.value"/> :value="needle" @input="needle = $event.target.value"/>
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div> <div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
</div> </div>
<el-button-group v-show="!initStep" class="button-group"> <el-button-group v-show="!initStep" class="button-group">
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button> <el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button> <el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
</el-button-group> </el-button-group>
</div>
</Window>
</div> </div>
</div> </Window>
</template> </template>
<script> <script>
@@ -61,6 +57,8 @@ class SearchPage extends Vue {
} }
async init(parsed) { async init(parsed) {
this.$refs.window.init();
if (this.parsed != parsed) { if (this.parsed != parsed) {
this.initStep = true; this.initStep = true;
this.stopInit = false; this.stopInit = false;
@@ -178,32 +176,13 @@ class SearchPage extends Vue {
</script> </script>
<style scoped> <style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 40;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainWindow {
width: 100%;
max-width: 500px;
height: 125px;
display: flex;
position: relative;
top: -50px;
}
.content { .content {
flex: 1; flex: 1;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
min-width: 430px;
} }
.input { .input {

View File

@@ -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() {
@@ -37,6 +35,7 @@ export default @Component({
class ServerStorage extends Vue { class ServerStorage extends Vue {
created() { created() {
this.inited = false; this.inited = false;
this.keyInited = false;
this.commit = this.$store.commit; this.commit = this.$store.commit;
this.prevServerStorageKey = null; this.prevServerStorageKey = null;
this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()}); this.$root.$on('generateNewServerStorageKey', () => {this.generateNewServerStorageKey()});
@@ -45,10 +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 = {};
} }
async init() { async init() {
@@ -59,13 +64,34 @@ 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 (!this.serverSyncEnabled)
return;
if (eventName == 'recent-changed') {
if (itemKey) {
if (!this.recentDeltaInited) {
await this.loadRecent();
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);
@@ -88,11 +114,12 @@ class ServerStorage extends Vue {
if (this.prevServerStorageKey != this.serverStorageKey) { if (this.prevServerStorageKey != this.serverStorageKey) {
this.prevServerStorageKey = this.serverStorageKey; this.prevServerStorageKey = this.serverStorageKey;
this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey)); this.hashedStorageKey = utils.toBase58(cryptoUtils.sha256(this.serverStorageKey));
this.keyInited = true;
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();
} }
@@ -143,27 +170,23 @@ 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});
} }
warning(message) { warning(message) {
if (this.showServerStorageMessages) if (this.showServerStorageMessages && !this.offlineModeActive)
this.$notify.warning({message}); this.$notify.warning({message});
} }
error(message) { error(message) {
if (this.showServerStorageMessages) if (this.showServerStorageMessages && !this.offlineModeActive)
this.$notify.error({message}); this.$notify.error({message});
} }
async loadSettings(force) { async loadSettings(force = false, doNotifySuccess = true) {
if (!this.serverSyncEnabled || !this.currentProfile) if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile)
return; return;
const setsId = `settings-${this.currentProfile}`; const setsId = `settings-${this.currentProfile}`;
@@ -199,14 +222,15 @@ 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}`);
} }
} }
async saveSettings() { async saveSettings() {
if (!this.serverSyncEnabled || !this.currentProfile || this.savingSettings) if (!this.keyInited || !this.serverSyncEnabled || !this.currentProfile || this.savingSettings)
return; return;
const diff = utils.getObjDiff(this.oldSettings, this.settings); const diff = utils.getObjDiff(this.oldSettings, this.settings);
@@ -217,32 +241,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;
try {
result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
} catch(e) {
this.savingSettings = false;
this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
return;
}
if (result.state == 'reject') { const oldRev = this.settingsRev[setsId] || 0;
await this.loadSettings(true); try {
const newSettings = utils.applyObjDiff(this.settings, diff); result = await this.storageSet({[setsId]: {rev: oldRev + 1, data: this.settings}});
this.commit('reader/setSettings', newSettings); } catch(e) {
} this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
tries++;
} }
if (tries >= maxSetTries) { if (result.state == 'reject') {
//отменять изменения не будем, просто предупредим await this.loadSettings(true, false);
//this.commit('reader/setSettings', this.oldSettings); this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
console.error(result); } else if (result.state == 'success') {
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});
} }
@@ -251,8 +261,8 @@ class ServerStorage extends Vue {
} }
} }
async loadProfiles(force) { async loadProfiles(force = false, doNotifySuccess = true) {
if (!this.serverSyncEnabled) if (!this.keyInited || !this.serverSyncEnabled)
return; return;
const oldRev = this.profilesRev; const oldRev = this.profilesRev;
@@ -286,22 +296,24 @@ 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}`);
} }
} }
async saveProfiles() { async saveProfiles() {
if (!this.serverSyncEnabled || this.savingProfiles) if (!this.keyInited || !this.serverSyncEnabled || this.savingProfiles)
return; return;
const diff = utils.getObjDiff(this.oldProfiles, this.profiles); const diff = utils.getObjDiff(this.oldProfiles, this.profiles);
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;
@@ -310,33 +322,16 @@ class ServerStorage extends Vue {
this.savingProfiles = true; this.savingProfiles = true;
try { try {
let result = {state: ''}; let result = {state: ''};
let tries = 0; try {
while (result.state != 'success' && tries < maxSetTries) { result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}});
try { } catch(e) {
result = await this.storageSet({profiles: {rev: this.profilesRev + 1, data: this.profiles}}); this.error(`Ошибка соединения с сервером (${e.message}). Данные не сохранены и могут быть перезаписаны.`);
} catch(e) {
this.savingProfiles = false;
this.commit('reader/setProfiles', this.oldProfiles);
this.checkCurrentProfile();
this.error(`Ошибка соединения с сервером: (${e.message}). Изменения отменены.`);
return;
}
if (result.state == 'reject') {
await this.loadProfiles(true);
const newProfiles = utils.applyObjDiff(this.profiles, diff);
this.commit('reader/setProfiles', newProfiles);
}
tries++;
} }
if (tries >= maxSetTries) { if (result.state == 'reject') {
this.commit('reader/setProfiles', this.oldProfiles); await this.loadProfiles(true, false);
this.checkCurrentProfile(); this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
console.error(result); } else if (result.state == 'success') {
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);
} }
@@ -345,180 +340,201 @@ class ServerStorage extends Vue {
} }
} }
async loadRecent(force) { async initRecentDelta() {
if (!this.serverSyncEnabled) let recentDelta = null;
try {
recentDelta = await this.storageGet({recentDelta: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return; return;
const oldRev = bookManager.recentRev;
const oldLastRev = bookManager.recentLastRev;
//проверим ревизию на сервере
let revs = null;
if (!force) {
try {
revs = await this.storageCheck({recent: {}, recentLast: {}});
if (revs.state == 'success' && revs.items.recent.rev == oldRev &&
revs.items.recentLast.rev == oldLastRev) {
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
} }
if (force || revs.items.recent.rev != oldRev) { 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(skipRevCheck = false, doNotifySuccess = true) {
if (!this.keyInited || !this.serverSyncEnabled || this.loadingRecent)
return;
this.loadingRecent = true;
try {
const oldRecentRev = bookManager.recentRev;
const oldRecentDeltaRev = bookManager.recentDeltaRev;
//проверим ревизию на сервере
let revs = null;
if (!skipRevCheck) {
try {
revs = await this.storageCheck({recent: {}, recentDelta: {}});
if (revs.state == 'success' && revs.items.recent.rev == oldRecentRev &&
revs.items.recentDelta.rev == oldRecentDeltaRev) {
if (!this.recentDeltaInited)
await this.initRecentDelta();
return;
}
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
}
let recent = null; let recent = null;
try { try {
recent = await this.storageGet({recent: {}}); recent = await this.storageGet({recent: {}, recentDelta: {}});
} 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();
} finally {
this.loadingRecent = false;
} }
if (force || revs.items.recentLast.rev != oldLastRev) {
let recentLast = null;
try {
recentLast = await this.storageGet({recentLast: {}});
} catch(e) {
this.error(`Ошибка соединения с сервером: ${e.message}`);
return;
}
if (recentLast.state == 'success') {
recentLast = recentLast.items.recentLast;
if (recentLast.rev == 0)
recentLast.data = {};
this.oldRecentLast = _.cloneDeep(recentLast.data);
await bookManager.setRecentLast(recentLast.data);
await bookManager.setRecentLastRev(recentLast.rev);
} else {
this.warning(`Неверный ответ сервера: ${recentLast.state}`);
}
}
this.notifySuccess();
} }
async saveRecent() { async saveRecent(itemKey, recurse) {
if (!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;
}
this.savingRecent = true; 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;
try { try {
let result = {state: ''}; if (forceSaveRecent) {//сохраняем recent целиком
let tries = 0; let result = {state: ''};
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(true, false);
/*const newRecent = utils.applyObjDiff(bm.recent, diff);
await bm.setRecent(newRecent);*/ this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
if (!recurse) {
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]);
this.recentDelta = {};
await bm.setRecentRev(bm.recentRev + 1);
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}). Данные не сохранены и могут быть перезаписаны.`);
} }
tries++; if (result.state == 'reject') {
}
if (tries >= maxSetTries) { await this.loadRecent(true, false);
console.error(result);
this.error('Не удалось отправить данные на сервер. Данные не сохранены и могут быть перезаписаны.'); this.warning(`Последние изменения отменены. Данные синхронизированы с сервером.`);
} else { if (!recurse) {
this.oldRecent = _.cloneDeep(bm.recent); this.savingRecent = false;
await bm.setRecentRev(bm.recentRev + 1); this.recentDelta[itemKey] = _.cloneDeep(bm.recent[itemKey]);
await this.saveRecentLast(true); 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.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;
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);
}
} finally {
this.savingRecentLast = false;
}
}
async storageCheck(items) { async storageCheck(items) {
return await this.storageApi('check', items); return await this.storageApi('check', items);
} }

View File

@@ -1,17 +1,13 @@
<template> <template>
<div ref="main" class="main" @click="close"> <Window ref="window" height="140px" max-width="600px" :top-shift="-50" @close="close">
<div class="mainWindow" @click.stop> <template slot="header">
<Window @close="close"> Установить позицию
<template slot="header"> </template>
Установить позицию
</template>
<div class="slider"> <div class="slider">
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider> <el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
</div>
</Window>
</div> </div>
</div> </Window>
</template> </template>
<script> <script>
@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -317,4 +317,56 @@ export default class DrawHelper {
await animation1Finish(duration); await animation1Finish(duration);
} }
} }
async doPageAnimationRotate(page1, page2, duration, isDown, animation1Finish, animation2Finish) {
if (isDown) {
page1.style.transform = `rotateY(90deg)`;
await sleep(30);
page2.style.transition = `${duration/2}ms ease-in`;
page2.style.transform = `rotateY(-90deg)`;
await animation2Finish(duration/2);
page1.style.transition = `${duration/2}ms ease-out`;
page1.style.transform = `rotateY(0deg)`;
await animation1Finish(duration/2);
} else {
page1.style.transform = `rotateY(-90deg)`;
await sleep(30);
page2.style.transition = `${duration/2}ms ease-in`;
page2.style.transform = `rotateY(90deg)`;
await animation2Finish(duration/2);
page1.style.transition = `${duration/2}ms ease-out`;
page1.style.transform = `rotateY(0deg)`;
await animation1Finish(duration/2);
}
}
async doPageAnimationFlip(page1, page2, duration, isDown, animation1Finish, animation2Finish, backgroundColor) {
page2.style.background = backgroundColor;
if (isDown) {
page2.style.transformOrigin = '5%';
await sleep(30);
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `rotateY(-120deg) translateX(${this.w/4}px)`;
await animation2Finish(duration);
} else {
page2.style.transformOrigin = '95%';
await sleep(30);
page2.style.transition = `${duration}ms ease-in-out`;
page2.style.transform = `rotateY(120deg) translateX(-${this.w/4}px)`;
await animation2Finish(duration);
}
page2.style.transformOrigin = 'center';
page2.style.background = '';
}
} }

View File

@@ -23,7 +23,6 @@
oncontextmenu="return false;"> oncontextmenu="return false;">
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop <div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div> @click.prevent.stop="onStatusBarClick"></div>
<div v-show="fontsLoading" ref="fontsLoading"></div>
</div> </div>
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop <div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
@click.prevent.stop="onStatusBarClick"></div> @click.prevent.stop="onStatusBarClick"></div>
@@ -77,7 +76,6 @@ class TextPage extends Vue {
page2 = null; page2 = null;
statusBar = null; statusBar = null;
statusBarClickable = null; statusBarClickable = null;
fontsLoading = null;
lastBook = null; lastBook = null;
bookPos = 0; bookPos = 0;
@@ -133,7 +131,6 @@ class TextPage extends Vue {
}, 10); }, 10);
this.$root.$on('resize', () => {this.$nextTick(this.onResize)}); this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
this.mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
} }
mounted() { mounted() {
@@ -171,6 +168,12 @@ class TextPage extends Vue {
this.fontShift = this.fontVertShift/100; this.fontShift = this.fontVertShift/100;
this.textShift = this.textVertShift/100 + this.fontShift; this.textShift = this.textVertShift/100 + this.fontShift;
//statusBar
this.$refs.statusBar.style.left = '0px';
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
//drawHelper //drawHelper
this.drawHelper.realWidth = this.realWidth; this.drawHelper.realWidth = this.realWidth;
this.drawHelper.realHeight = this.realHeight; this.drawHelper.realHeight = this.realHeight;
@@ -196,14 +199,8 @@ class TextPage extends Vue {
this.drawHelper.lineHeight = this.lineHeight; this.drawHelper.lineHeight = this.lineHeight;
this.drawHelper.context = this.context; this.drawHelper.context = this.context;
//сообщение "Загрузка шрифтов..." //statusBar
const flText = 'Загрузка шрифта...'; this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
this.$refs.fontsLoading.innerHTML = flText;
const fontsLoadingStyle = this.$refs.fontsLoading.style;
fontsLoadingStyle.position = 'absolute';
fontsLoadingStyle.fontSize = this.fontSize + 'px';
fontsLoadingStyle.top = (this.realHeight/2 - 2*this.fontSize) + 'px';
fontsLoadingStyle.left = (this.realWidth - this.drawHelper.measureText(flText, {}))/2 + 'px';
//parsed //parsed
if (this.parsed) { if (this.parsed) {
@@ -223,15 +220,9 @@ class TextPage extends Vue {
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter; this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
this.parsed.imageHeightLines = this.imageHeightLines; this.parsed.imageHeightLines = this.imageHeightLines;
this.parsed.imageFitWidth = this.imageFitWidth; this.parsed.imageFitWidth = this.imageFitWidth;
this.parsed.compactTextPerc = this.compactTextPerc;
} }
//statusBar
this.$refs.statusBar.style.left = '0px';
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
this.statusBarClickable = this.drawHelper.statusBarClickable(this.statusBarTop, this.statusBarHeight);
//scrolling page //scrolling page
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight; const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
let y = pageSpace/2; let y = pageSpace/2;
@@ -239,6 +230,10 @@ class TextPage extends Vue {
y += this.statusBarHeight*(this.statusBarTop ? 1 : 0); y += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
let page1 = this.$refs.scrollBox1; let page1 = this.$refs.scrollBox1;
let page2 = this.$refs.scrollBox2; let page2 = this.$refs.scrollBox2;
page1.style.perspective = '3072px';
page2.style.perspective = '3072px';
page1.style.width = this.w + this.indentLR + 'px'; page1.style.width = this.w + this.indentLR + 'px';
page2.style.width = this.w + this.indentLR + 'px'; page2.style.width = this.w + this.indentLR + 'px';
page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px'; page1.style.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
@@ -268,6 +263,18 @@ class TextPage extends Vue {
async loadFonts() { async loadFonts() {
this.fontsLoading = true; this.fontsLoading = true;
let inst = null;
(async() => {
await sleep(500);
if (this.fontsLoading)
inst = this.$notify({
title: '',
dangerouslyUseHTMLString: true,
message: 'Загрузка шрифта &nbsp;<i class="el-icon-loading"></i>',
duration: 0
});
})();
if (!this.fontsLoaded) if (!this.fontsLoaded)
this.fontsLoaded = {}; this.fontsLoaded = {};
//загрузка дин.шрифта //загрузка дин.шрифта
@@ -298,6 +305,8 @@ class TextPage extends Vue {
} }
this.fontsLoading = false; this.fontsLoading = false;
if (inst)
inst.close();
} }
getSettings() { getSettings() {
@@ -625,7 +634,7 @@ class TextPage extends Vue {
const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation'); const animation1Finish = this.generateWaitingFunc('resolveAnimation1Finish', 'stopAnimation');
const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation'); const animation2Finish = this.generateWaitingFunc('resolveAnimation2Finish', 'stopAnimation');
const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation'); const transition1Finish = this.generateWaitingFunc('resolveTransition1Finish', 'stopAnimation');
//const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation'); const transition2Finish = this.generateWaitingFunc('resolveTransition2Finish', 'stopAnimation');
const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100)); const duration = Math.round(3000*(1 - this.pageChangeAnimationSpeed/100));
let page1 = this.$refs.scrollingPage1; let page1 = this.$refs.scrollingPage1;
@@ -654,6 +663,14 @@ class TextPage extends Vue {
page1.style.height = this.scrollHeight + this.lineHeight + 'px'; page1.style.height = this.scrollHeight + this.lineHeight + 'px';
page2.style.height = this.scrollHeight + this.lineHeight + 'px'; page2.style.height = this.scrollHeight + this.lineHeight + 'px';
break; break;
case 'rotate':
await this.drawHelper.doPageAnimationRotate(page1, page2,
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish);
break;
case 'flip':
await this.drawHelper.doPageAnimationFlip(page1, page2,
duration, this.pageChangeDirectionDown, transition1Finish, transition2Finish, this.backgroundColor);
break;
} }
this.resolveAnimation1Finish = null; this.resolveAnimation1Finish = null;
@@ -989,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) {
@@ -1005,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) {
@@ -1033,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();
@@ -1120,6 +1137,10 @@ class TextPage extends Vue {
overflow: hidden; overflow: hidden;
} }
.on-top {
z-index: 100;
}
.back { .back {
z-index: 5; z-index: 5;
} }
@@ -1185,4 +1206,5 @@ class TextPage extends Vue {
0% { opacity: 1; } 0% { opacity: 1; }
100% { opacity: 0; } 100% { opacity: 0; }
} }
</style> </style>

View File

@@ -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

View File

@@ -18,48 +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 = [];
this.books = {};
this.recent = {};
//bmCacheStore нужен только для ускорения загрузки читалки this.recentLast = await bmRecentStore.getItem('recent-last');
this.booksCached = await bmCacheStore.getItem('books'); if (this.recentLast) {
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.recent[this.recentLast.key] = this.recentLast;
this.recentRev = await bmRecentStore.getItem('recent-rev') || 0; const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
this.recentLastRev = await bmRecentStore.getItem('recent-last-rev') || 0; if (_.isObject(meta)) {
this.books = Object.assign({}, this.booksCached); this.books[meta.key] = meta;
}
this.recentChanged2 = true;
if (!this.books || !this.recent) {
this.books = {};
this.recent = {};
await this.loadMeta(true);
} else {
this.loadMeta(false);
} }
this.recentRev = await bmRecentStore.getItem('recent-rev') || 0;
this.recentDeltaRev = await bmRecentStore.getItem('recent-delta-rev') || 0;
this.recentChanged = true;
this.loadStored();//no await
} }
//долгая загрузка из хранилища, //Долгая асинхронная загрузка из хранилища.
//хранение в отдельных записях дает относительно //Хранение в отдельных записях дает относительно
//нормальное поведение при нескольких вкладках с читалкой в браузере //нормальное поведение при нескольких вкладках с читалкой в браузере.
async loadMeta(immediate) { async loadStored() {
if (!immediate) //даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
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('-');
@@ -67,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;
@@ -81,7 +75,7 @@ class BookManager {
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);
@@ -97,21 +91,16 @@ class BookManager {
/*if (key) { /*if (key) {
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 1000; i++) {
const k = this.keyFromUrl(i.toString()); const k = this.keyFromUrl(i.toString());
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000}); this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
} }
}*/ }*/
await this.cleanBooks(); await this.cleanBooks();
await this.cleanRecentBooks();
//очистка позже this.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() {
@@ -131,22 +120,93 @@ class BookManager {
} }
if (size > maxDataSize && toDel) { if (size > maxDataSize && toDel) {
await this._delBook(toDel); await this.delBook(toDel);
} else { } else {
break; break;
} }
} }
} }
async addBook(newBook, callback) { async deflateWithProgress(data, callback) {
if (!this.books) const chunkSize = 128*1024;
await this.init(); const deflator = new utils.pako.Deflate({level: 5});
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
let chunkNum = 0;
let perc = 0;
let prevPerc = 0;
for (var i = 0; i < data.length; i += chunkSize) {
if ((i + chunkSize) >= data.length) {
deflator.push(data.substring(i, i + chunkSize), true);
} else {
deflator.push(data.substring(i, i + chunkSize), false);
}
chunkNum++;
perc = Math.round(chunkNum/chunkTotal*100);
if (perc != prevPerc) {
callback(perc);
await utils.sleep(1);
prevPerc = perc;
}
}
if (deflator.err) {
throw new Error(deflator.msg);
}
callback(100);
return deflator.result;
}
async inflateWithProgress(data, callback) {
const chunkSize = 64*1024;
const inflator = new utils.pako.Inflate({to: 'string'});
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
let chunkNum = 0;
let perc = 0;
let prevPerc = 0;
for (var i = 0; i < data.length; i += chunkSize) {
if ((i + chunkSize) >= data.length) {
inflator.push(data.subarray(i, i + chunkSize), true);
} else {
inflator.push(data.subarray(i, i + chunkSize), false);
}
chunkNum++;
perc = Math.round(chunkNum/chunkTotal*100);
if (perc != prevPerc) {
callback(perc);
await utils.sleep(1);
prevPerc = perc;
}
}
if (inflator.err) {
throw new Error(inflator.msg);
}
callback(100);
return inflator.result;
}
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path}; let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url); meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now(); meta.addTime = Date.now();
const cb = (perc) => { const cb = (perc) => {
const p = Math.round(80*perc/100); const p = Math.round(30*perc/100);
callback(p);
};
const cb2 = (perc) => {
const p = Math.round(30 + 65*perc/100);
callback(p); callback(p);
}; };
@@ -155,53 +215,75 @@ 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);
result.dataCompressedLength = data.byteLength; result.dataCompressedLength = data.byteLength;
} }
callback(90); 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) {
data = utils.pako.inflate(data, {to: 'string'}); try {
//data = utils.pako.inflate(data, {to: 'string'});
data = await this.inflateWithProgress(data, cb);
} catch (e) {
this.delBook(meta);
throw e;
}
} }
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);
}; };
@@ -213,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);
@@ -257,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;
@@ -276,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;
@@ -347,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;
} }
@@ -361,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;
} }
@@ -369,21 +434,24 @@ class BookManager {
const mergedRecent = _.cloneDeep(this.recent); const mergedRecent = _.cloneDeep(this.recent);
Object.assign(mergedRecent, value); Object.assign(mergedRecent, value);
const newRecent = {};
for (const rec of Object.values(mergedRecent)) { //"ленивое" обновление хранилища
if (rec.key) { (async() => {
await bmRecentStore.setItem(rec.key, rec); for (const rec of Object.values(mergedRecent)) {
newRecent[rec.key] = rec; if (rec.key) {
await bmRecentStore.setItem(rec.key, rec);
await utils.sleep(1);
}
} }
} })();
this.recent = newRecent; this.recent = mergedRecent;
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');
} }
@@ -392,25 +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) {
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) {
bmRecentStore.setItem('recent-last-rev', value);
this.recentLastRev = value;
} }
addEventListener(listener) { addEventListener(listener) {
@@ -425,8 +477,12 @@ class BookManager {
} }
emit(eventName, value) { emit(eventName, value) {
for (const listener of this.eventListeners) if (this.eventListeners) {
listener(eventName, value); for (const listener of this.eventListeners) {
//console.log(eventName);
listener(eventName, value);
}
}
} }
} }

View File

@@ -0,0 +1,153 @@
export const versionHistory = [
{
showUntil: '2019-09-19',
header: '0.7.1 (2019-09-20)',
content:
`
<ul>
<li>исправления багов</li>
<li>на панель управления добавлена кнопка "Автономный режим"</li>
<li>актуализирована справка</li>
</ul>
`
},
{
showUntil: '2019-10-01',
header: '0.7.0 (2019-09-07)',
content:
`
<ul>
<li>налажена работа https-версии сайта, рекомендуется плавный переход</li>
<li>добавлена возможность загрузки и работы https-версии читалки в оффлайн-режиме (при отсутствии интернета)</li>
<li>упрощение механизма серверной синхронизации с целью повышения надежности и избавления от багов</li>
<li>окна теперь можно перемещать за заголовок</li>
<li>немного улучшен внешний вид и управление на смартфонах</li>
<li>добавлен параметр "Компактность" в раздел "Вид"->"Текст" в настройках</li>
</ul>
`
},
{
showUntil: '2019-07-20',
header: '0.6.10 (2019-07-21)',
content:
`
<ul>
<li>исправления багов</li>
</ul>
`
},
{
showUntil: '2019-06-22',
header: '0.6.9 (2019-06-23)',
content:
`
<ul>
<li>исправлен баг - падение сервера при распаковке битых архивов книг</li>
<li>исправлен баг - не распознавались некоторые книги формата fb2 в кодировке utf8</li>
<li>добавлены новые варианты анимации перелистывания</li>
<li>на страницу загрузки добавлен блок "Поделиться"</li>
<li>улучшены прогрессбары</li>
<li>исправления недочетов, небольшие оптимизации</li>
</ul>
`
},
{
showUntil: '2019-06-05',
header: '0.6.7 (2019-05-30)',
content:
`
<ul>
<li>добавлен диалог "Что нового"</li>
<li>в справку добавлена история версий проекта</li>
<li>добавлена возможность настройки отображаемых кнопок на панели управления</li>
<li>некоторые кнопки на панели управления были скрыты по умолчанию</li>
<li>на страницу загрузки добавлена возможность загрузки книги из буфера обмена</li>
<li>добавлен GET-параметр вида "/reader?__refresh=1&url=..." для принудительного обновления загружаемого текста</li>
<li>добавлен GET-параметр вида "/reader?__pp=50.5&url=..." для указания позиции в книге в процентах</li>
<li>исправления багов и недочетов</li>
</ul>
`
},
{
showUntil: '2019-03-29',
header: '0.6.6 (2019-03-29)',
content:
`
<ul>
<li>в справку добавлено описание настройки браузеров для автономной работы читалки (без доступа к интернету)</li>
<li>оптимизации процесса синхронизации, внутренние переделки</li>
</ul>
`
},
{
showUntil: '2019-03-24',
header: '0.6.4 (2019-03-24)',
content:
`
<ul>
<li>исправления багов, оптимизации</li>
<li>добавлена возможность синхронизации данных между устройствами</li>
</ul>
`
},
{
showUntil: '2019-03-04',
header: '0.5.4 (2019-03-04)',
content:
`
<ul>
<li>добавлена поддержка форматов pdf, epub, mobi</li>
<li>(0.5.2) добавлена поддержка форматов rtf, doc, docx</li>
<li>(0.4.2) фильтр для СИ больше не вырезает изображения</li>
<li>(0.4.0) добавлено отображение картинок в fb2</li>
</ul>
`
},
{
showUntil: '2019-02-17',
header: '0.3.0 (2019-02-17)',
content:
`
<ul>
<li>поправки багов</li>
<li>улучшено распознавание текста</li>
<li>изменена верстка страницы - убрано позиционирование каждого слова</li>
</ul>
`
},
{
showUntil: '2019-02-14',
header: '0.1.7 (2019-02-14)',
content:
`
<ul>
<li>увеличены верхние границы отступов и др.размеров</li>
<li>добавлена настройка для удаления/вставки пустых параграфов</li>
<li>добавлена настройка включения/отключения управления кликом</li>
<li>добавлена возможность сброса настроек</li>
<li>убран автоматический редирект на последнюю загруженную книгу, если не задан url в маршруте</li>
</ul>
`
},
{
showUntil: '2019-02-12',
header: '0.1.0 (2019-02-12)',
content:
`
<ul>
<li>первый деплой проекта, длительность разработки - 2 месяца</li>
</ul>
`
},
]

View File

@@ -1,10 +1,15 @@
<template> <template>
<div class="window"> <div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
<div class="header"> <div ref="windowBox" class="windowBox" @click.stop>
<span class="header-text"><slot name="header"></slot></span> <div class="window">
<span class="close-button" @click="close"><i class="el-icon-close"></i></span> <div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove">
<span class="header-text"><slot name="header"></slot></span>
<span class="close-button" @mousedown.stop @click="close"><i class="el-icon-close"></i></span>
</div>
<slot></slot>
</div>
</div> </div>
<slot></slot>
</div> </div>
</template> </template>
@@ -14,17 +19,116 @@ import Vue from 'vue';
import Component from 'vue-class-component'; import Component from 'vue-class-component';
export default @Component({ export default @Component({
props: {
height: { type: String, default: '100%' },
width: { type: String, default: '100%' },
maxWidth: { type: String, default: '' },
topShift: { type: Number, default: 0 },
}
}) })
class Window extends Vue { class Window extends Vue {
close() { init() {
this.$emit('close'); this.$nextTick(() => {
this.$refs.windowBox.style.height = this.height;
this.$refs.windowBox.style.width = this.width;
if (this.maxWidth)
this.$refs.windowBox.style.maxWidth = this.maxWidth;
const left = (this.$refs.main.offsetWidth - this.$refs.windowBox.offsetWidth)/2;
const top = (this.$refs.main.offsetHeight - this.$refs.windowBox.offsetHeight)/2 + this.topShift;
this.$refs.windowBox.style.left = (left > 0 ? left : 0) + 'px';
this.$refs.windowBox.style.top = (top > 0 ? top : 0) + 'px';
});
} }
onMouseDown(event) {
if (this.$isMobileDevice)
return;
if (event.button == 0) {
this.$refs.header.style.cursor = 'move';
this.startX = event.screenX;
this.startY = event.screenY;
this.moving = true;
}
}
onMouseUp(event) {
if (event.button == 0) {
this.$refs.header.style.cursor = 'default';
this.moving = false;
}
}
onMouseMove(event) {
if (this.moving) {
const deltaX = event.screenX - this.startX;
const deltaY = event.screenY - this.startY;
this.startX = event.screenX;
this.startY = event.screenY;
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
}
}
onTouchStart(event) {
if (!this.$isMobileDevice)
return;
if (event.touches.length == 1) {
const touch = event.touches[0];
this.$refs.header.style.cursor = 'move';
this.startX = touch.screenX;
this.startY = touch.screenY;
this.moving = true;
}
}
onTouchMove(event) {
if (!this.$isMobileDevice)
return;
if (event.touches.length == 1 && this.moving) {
const touch = event.touches[0];
const deltaX = touch.screenX - this.startX;
const deltaY = touch.screenY - this.startY;
this.startX = touch.screenX;
this.startY = touch.screenY;
this.$refs.windowBox.style.left = (this.$refs.windowBox.offsetLeft + deltaX) + 'px';
this.$refs.windowBox.style.top = (this.$refs.windowBox.offsetTop + deltaY) + 'px';
}
}
onTouchEnd() {
if (!this.$isMobileDevice)
return;
this.$refs.header.style.cursor = 'default';
this.moving = false;
}
close() {
if (!this.moving)
this.$emit('close');
}
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
</script> </script>
<style scoped> <style scoped>
.main {
position: absolute;
width: 100%;
height: 100%;
z-index: 50;
}
.windowBox {
position: absolute;
display: flex;
height: 100%;
width: 100%;
}
.window { .window {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -39,9 +143,9 @@ class Window extends Vue {
.header { .header {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
background-color: #e5e7ea; background-color: #59B04F;
align-items: center; align-items: center;
height: 40px; height: 30px;
} }
.header-text { .header-text {
@@ -54,8 +158,12 @@ class Window extends Vue {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 40px; width: 30px;
height: 40px; height: 30px;
cursor: pointer; cursor: pointer;
} }
.close-button:hover {
background-color: #69C05F;
}
</style> </style>

View File

@@ -86,8 +86,8 @@ import './theme/form-item.css';
import ElColorPicker from 'element-ui/lib/color-picker'; import ElColorPicker from 'element-ui/lib/color-picker';
import './theme/color-picker.css'; import './theme/color-picker.css';
//import ElDialog from 'element-ui/lib/dialog'; import ElDialog from 'element-ui/lib/dialog';
//import './theme/dialog.css'; import './theme/dialog.css';
import Notification from 'element-ui/lib/notification'; import Notification from 'element-ui/lib/notification';
import './theme/notification.css'; import './theme/notification.css';
@@ -106,7 +106,7 @@ const components = {
ElCol, ElContainer, ElAside, ElMain, ElHeader, ElCol, ElContainer, ElAside, ElMain, ElHeader,
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn, ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
ElProgress, ElSlider, ElForm, ElFormItem, ElProgress, ElSlider, ElForm, ElFormItem,
ElColorPicker, ElColorPicker, ElDialog,
}; };
for (let name in components) { for (let name in components) {

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html manifest="/app/manifest.appcache">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
@@ -9,5 +9,6 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="https://yastatic.net/share2/share.js" async="async"></script>
</body> </body>
</html> </html>

View File

@@ -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,

View File

@@ -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'],

View File

@@ -2,13 +2,12 @@ import _ from 'lodash';
import baseX from 'base-x'; import baseX from 'base-x';
import PAKO from 'pako'; import PAKO from 'pako';
import {Buffer} from 'safe-buffer'; import {Buffer} from 'safe-buffer';
import sjclWrapper from './sjclWrapper';
export const pako = PAKO; export const pako = PAKO;
const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const bs58 = baseX(BASE58); const bs58 = baseX(BASE58);
const bs64 = baseX(BASE64);
export function sleep(ms) { export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
@@ -40,6 +39,10 @@ export function formatDate(d, format) {
case 'normal': case 'normal':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` + return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` +
`${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
case 'coDate':
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
case 'noDate':
return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`;
} }
} }
@@ -86,12 +89,18 @@ export function fromBase58(data) {
return bs58.decode(data); return bs58.decode(data);
} }
//base-x слишком тормозит, используем sjcl
export function toBase64(data) { export function toBase64(data) {
return bs64.encode(Buffer.from(data)); return sjclWrapper.codec.base64.fromBits(
sjclWrapper.codec.bytes.toBits(Buffer.from(data))
);
} }
//base-x слишком тормозит, используем sjcl
export function fromBase64(data) { export function fromBase64(data) {
return bs64.decode(data); return Buffer.from(sjclWrapper.codec.bytes.fromBits(
sjclWrapper.codec.base64.toBits(data)
));
} }
export function getObjDiff(oldObj, newObj) { export function getObjDiff(oldObj, newObj) {
@@ -120,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 &&
@@ -157,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;
}

View File

@@ -1,3 +1,17 @@
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
const toolButtons = [
{name: 'undoAction', show: true, text: 'Действие назад'},
{name: 'redoAction', show: true, text: 'Действие вперед'},
{name: 'fullScreen', show: true, text: 'На весь экран'},
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
{name: 'setPosition', show: true, text: 'На страницу'},
{name: 'search', show: true, text: 'Найти в тексте'},
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
];
const fonts = [ const fonts = [
{name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0}, {name: 'ReaderDefault', label: 'По-умолчанию', fontVertShift: 0},
{name: 'GEO_1', label: 'BPG Arial', fontVertShift: 10}, {name: 'GEO_1', label: 'BPG Arial', fontVertShift: 10},
@@ -132,7 +146,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,// выравнивание по ширине
@@ -140,7 +154,7 @@ const settingDefaults = {
indentLR: 15,// px, отступ всего текста слева и справа indentLR: 15,// px, отступ всего текста слева и справа
indentTB: 0,// px, отступ всего текста сверху и снизу indentTB: 0,// px, отступ всего текста сверху и снизу
wordWrap: true,//перенос по слогам wordWrap: true,//перенос по слогам
keepLastToFirst: true,// перенос последней строки в первую при листании keepLastToFirst: false,// перенос последней строки в первую при листании
showStatusBar: true, showStatusBar: true,
statusBarTop: false,// top, bottom statusBarTop: false,// top, bottom
@@ -150,7 +164,7 @@ const settingDefaults = {
scrollingDelay: 3000,// замедление, ms scrollingDelay: 3000,// замедление, ms
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
pageChangeAnimationSpeed: 80, //0-100% pageChangeAnimationSpeed: 80, //0-100%
allowUrlParamBookPos: false, allowUrlParamBookPos: false,
@@ -163,17 +177,23 @@ 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,
showMigrationDialog: true,
fontShifts: {}, fontShifts: {},
showToolButton: {},
}; };
for (const font of fonts) for (const font of fonts)
settingDefaults.fontShifts[font.name] = font.fontVertShift; settingDefaults.fontShifts[font.name] = font.fontVertShift;
for (const font of webFonts) for (const font of webFonts)
settingDefaults.fontShifts[font.name] = font.fontVertShift; settingDefaults.fontShifts[font.name] = font.fontVertShift;
for (const button of toolButtons)
settingDefaults.showToolButton[button.name] = button.show;
// initial state // initial state
const state = { const state = {
@@ -183,6 +203,8 @@ const state = {
profiles: {}, profiles: {},
profilesRev: 0, profilesRev: 0,
allowProfilesSave: false,//подстраховка для разработки allowProfilesSave: false,//подстраховка для разработки
whatsNewContentHash: '',
migrationRemindDate: '',
currentProfile: '', currentProfile: '',
settings: Object.assign({}, settingDefaults), settings: Object.assign({}, settingDefaults),
settingsRev: {}, settingsRev: {},
@@ -214,6 +236,12 @@ const mutations = {
setAllowProfilesSave(state, value) { setAllowProfilesSave(state, value) {
state.allowProfilesSave = value; state.allowProfilesSave = value;
}, },
setWhatsNewContentHash(state, value) {
state.whatsNewContentHash = value;
},
setMigrationRemindDate(state, value) {
state.migrationRemindDate = value;
},
setCurrentProfile(state, value) { setCurrentProfile(state, value) {
state.currentProfile = value; state.currentProfile = value;
}, },
@@ -226,6 +254,7 @@ const mutations = {
}; };
export default { export default {
toolButtons,
fonts, fonts,
webFonts, webFonts,
settingDefaults, settingDefaults,

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -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;

5049
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "Liberama", "name": "Liberama",
"version": "0.6.2", "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",
"element-ui": "^2.4.11", "decompress-zip": "^0.2.2",
"express": "^4.16.4", "element-ui": "^2.12.0",
"extract-zip": "^1.6.7", "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"
} }
} }

View File

@@ -28,7 +28,7 @@ class ConvertHtml extends ConvertBase {
} else { } else {
isText = opts.isText; isText = opts.isText;
} }
const {cutTitle} = opts; let {cutTitle} = opts;
let titleInfo = {}; let titleInfo = {};
let desc = {_n: 'description', 'title-info': titleInfo}; let desc = {_n: 'description', 'title-info': titleInfo};
@@ -123,8 +123,11 @@ class ConvertHtml extends ConvertBase {
} }
} }
if (tag == 'title') if (tag == 'title' || tag == 'cut-title') {
inTitle = true; inTitle = true;
if (tag == 'cut-title')
cutTitle = true;
}
if (tag == 'fb2-image') { if (tag == 'fb2-image') {
inImage = true; inImage = true;
@@ -153,7 +156,7 @@ class ConvertHtml extends ConvertBase {
} }
} }
if (tag == 'title') if (tag == 'title' || tag == 'cut-title')
inTitle = false; inTitle = false;
if (tag == 'fb2-image') if (tag == 'fb2-image')

View File

@@ -2,9 +2,9 @@ const fs = require('fs-extra');
const zlib = require('zlib'); const zlib = require('zlib');
const crypto = require('crypto'); const crypto = require('crypto');
const path = require('path'); const path = require('path');
const extractZip = require('extract-zip');
const unbzip2Stream = require('unbzip2-stream'); const unbzip2Stream = require('unbzip2-stream');
const tar = require('tar-fs') const tar = require('tar-fs');
const DecompressZip = require('decompress-zip');
const utils = require('./utils'); const utils = require('./utils');
const FileDetector = require('./FileDetector'); const FileDetector = require('./FileDetector');
@@ -114,16 +114,24 @@ class FileDecompressor {
async unZip(filename, outputDir) { async unZip(filename, outputDir) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const files = []; const files = [];
extractZip(filename, { const unzipper = new DecompressZip(filename);
dir: outputDir,
onEntry: (entry) => { unzipper.on('error', function(err) {
files.push({path: entry.fileName, size: entry.uncompressedSize}); reject(err);
} });
}, (err) => {
if (err) unzipper.on('extract', function() {
reject(err);
resolve(files); resolve(files);
}); });
unzipper.extract({
path: outputDir,
filter: function(file) {
if (file.type == 'File')
files.push({path: file.path, size: file.uncompressedSize});
return true;
}
});
}); });
} }
@@ -184,6 +192,10 @@ class FileDecompressor {
resolve([file]); resolve([file]);
}); });
stream.on('error', (err) => {
reject(err);
});
inputStream.on('error', (err) => { inputStream.on('error', (err) => {
reject(err); reject(err);
}); });

View File

@@ -707,7 +707,8 @@
"rules": [ "rules": [
{ "type": "or", "rules": { "type": "or", "rules":
[ [
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" } { "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" }
] ]
} }
] ]

View File

@@ -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;
@@ -131,32 +132,40 @@ class ReaderWorker {
return `file://${hash}`; return `file://${hash}`;
} }
async periodicCleanDir(dir, maxSize, timeout) { async periodicCleanDir(dir, maxSize, timeout) {
const list = await fs.readdir(dir); try {
log(`Start clean dir: ${dir}, maxSize=${maxSize}`);
const list = await fs.readdir(dir);
let size = 0; let size = 0;
let files = []; let files = [];
for (const name of list) { for (const name of list) {
const stat = await fs.stat(`${dir}/${name}`); const stat = await fs.stat(`${dir}/${name}`);
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
size += stat.size; size += stat.size;
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);
let i = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
log(`rm ${dir}/${file.name}`);
await fs.remove(`${dir}/${file.name}`);
size -= file.stat.size;
i++;
}
log(`removed ${i} files`);
} catch(e) {
log(LM_ERR, e.message);
} finally {
setTimeout(() => {
this.periodicCleanDir(dir, maxSize, timeout);
}, timeout);
} }
files.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
let i = 0;
while (i < files.length && size > maxSize) {
const file = files[i];
await fs.remove(`${dir}/${file.name}`);
size -= file.stat.size;
i++;
}
setTimeout(() => {
this.periodicCleanDir(dir, maxSize, timeout);
}, timeout);
} }
} }

View File

@@ -20,7 +20,7 @@ function webpackDevMiddleware(app) {
function logQueries(app) { function logQueries(app) {
app.use(function(req, res, next) { app.use(function(req, res, next) {
const start = Date.now(); const start = Date.now();
log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body)}`); log(`${req.method} ${req.originalUrl} ${JSON.stringify(req.body).substr(0, 2000)}`);
//log(`${JSON.stringify(req.headers, null, 2)}`) //log(`${JSON.stringify(req.headers, null, 2)}`)
res.once('finish', () => { res.once('finish', () => {
log(`${Date.now() - start}ms`); log(`${Date.now() - start}ms`);

View File

@@ -51,7 +51,7 @@ async function main() {
} }
app.use(compression({ level: 1 })); app.use(compression({ level: 1 }));
app.use(express.json()); app.use(express.json({limit: '10mb'}));
if (devModule) if (devModule)
devModule.logQueries(app); devModule.logQueries(app);